想必大家都能看得懂的源碼 ahooks 整體架構篇,且可以使用插件化機制優雅的封裝你的請求hook,現在我們就探討下ahooks 是怎么解決 React 的閉包問題的?。
React 的閉包問題
先來看一個例子:
import React, { useState, useEffect } from "react"; export default () => { const [count, setCount] = useState(0); useEffect(() => { setInterval(() => { console.log("setInterval:", count); }, 1000); }, []); return ( <div> count: {count} <br /> <button onClick={() => setCount((val) => val + 1)}>增加 1</button> </div> ); };
代碼示例
點擊按鈕的時候,發現 setInterval 中打印出來的值并沒有發生變化,始終都是 0。造成這樣就是因為 React 的閉包問題。
產生的原因
為了維護 Function Component 的 state,React 用鏈表的方式來存儲 Function Component 里面的 hooks,并為每一個 hooks 創建了一個對象。
type Hook = { memoizedState: any, baseState: any, baseUpdate: Update<any, any> | null, queue: UpdateQueue<any, any> | null, next: Hook | null, };
這個對象的memoizedState屬性就是用來存儲組件上一次更新后的state,next指向下一個 hook 對象。在組件更新的過程中,hooks 函數執行的順序是不變的,就可以根據這個鏈表拿到當前 hooks 對應的 Hook 對象,函數式組件就是這樣擁有了state的能力。
伴隨的還有一系列規則,比如不能將 hooks 寫入到if...else...中。從而保證能夠正確拿到相應 hook 的 state。
useEffect 接收了兩個參數,一個回調函數和一個數組。數組里面就是 useEffect 的依賴,當為 [] 的時候,回調函數只會在組件第一次渲染的時候執行一次。如果有依賴其他項,react 會判斷其依賴是否改變,如果改變了就會執行回調函數。
回到剛剛那個例子:
const [count, setCount] = useState(0); useEffect(() => { setInterval(() => { console.log("setInterval:", count); }, 1000); }, []);
它第一次執行的時候,執行 useState,count 為 0。執行 useEffect,執行其回調中的邏輯,啟動定時器,每隔 1s 輸出setInterval: 0。
須知當點擊按鈕使count增加 1 的時候,整個函數式組件重新渲染,這個時候前一個執行的鏈表已經存在了。useState 將 Hook 對象 上保存的狀態置為 1, 那么此時 count 也為 1 了。執行 useEffect,其依賴項為空,不執行回調函數。但是之前的回調函數還是在的,它還是會每隔 1s 執行console.log("setInterval:", count);,但這里的 count 是之前第一次執行時候的 count 值,因為在定時器的回調函數里面被引用了,形成了閉包一直被保存。
解決的方法
解決方法一:給 useEffect 設置依賴項,重新執行函數,設置新的定時器,拿到最新值。
// 解決方法一 useEffect(() => { if (timer.current) { clearInterval(timer.current); } timer.current = setInterval(() => { console.log("setInterval:", count); }, 1000); }, [count]);
解決方法二:使用 useRef。 useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(initialValue)。
useRef 創建的是一個普通 Javascript 對象,而且會在每次渲染時返回同一個 ref 對象,當我們變化它的 current 屬性的時候,對象的引用都是同一個,所以定時器中能夠讀到最新的值。
const lastCount = useRef(count); // 解決方法二 useEffect(() => { setInterval(() => { console.log("setInterval:", lastCount.current); }, 1000); }, []); return ( <div> count: {count} <br /> <button onClick={() => { setCount((val) => val + 1); // +1 lastCount.current += 1; }} > 增加 1 </button> </div> ); useRef => useLatest
現在我們來說說我們的主題ahooks 主題,基于上述的第二種解決方案,useLatest 這個 hook 隨之誕生。它返回當前最新值的 Hook,可以避免閉包問題。實現原理很簡單,只有短短的十行代碼,就是使用 useRef 包一層:
import { useRef } from 'react'; // 通過 useRef,保持每次獲取到的都是最新的值 function useLatest<T>(value: T) { const ref = useRef(value); ref.current = value; return ref; } export default useLatest; useEvent => useMemoizedFn
React 中另一個場景,是基于 useCallback 的。
const [count, setCount] = useState(0); const callbackFn = useCallback(() => { console.log(`Current count is ${count}`); }, []);
count 的值變化成多少都不會影響,執行 callbackFn 打印出來的 count 的值始終都是 0。這個是因為回調函數被 useCallback 緩存,形成閉包,從而形成閉包陷阱。
這個問題解決方法?官方提出了 useEvent。它解決的問題:如何同時保持函數引用不變與訪問到最新狀態。使用它之后,上面的例子就變成了。
const callbackFn = useEvent(() => { console.log(`Current count is ${count}`); });
你是否注意啊到,在 ahooks 中已經實現了類似的功能,那就是 useMemoizedFn。
useMemoizedFn 是持久化 function 的 Hook,理論上,可以使用 useMemoizedFn 完全代替 useCallback。使用 useMemoizedFn,可以省略第二個參數 deps,同時保證函數地址永遠不會變化。以上的問題,通過以下的方式就能輕松解決:
const memoizedFn = useMemoizedFn(() => { console.log(`Current count is ${count}`); });
Demo 地址
看看下面代碼,其還是通過 useRef 保持 function 引用地址不變,并且每次執行都可以拿到最新的 state 值。
function useMemoizedFn<T extends noop>(fn: T) { // 通過 useRef 保持其引用地址不變,并且值能夠保持值最新 const fnRef = useRef<T>(fn); fnRef.current = useMemo(() => fn, [fn]); // 通過 useRef 保持其引用地址不變,并且值能夠保持值最新 const memoizedFn = useRef<PickFunction<T>>(); if (!memoizedFn.current) { // 返回的持久化函數,調用該函數的時候,調用原始的函數 memoizedFn.current = function (this, ...args) { return fnRef.current.apply(this, args); }; } return memoizedFn.current as T; }
總結與思考
有利有弊,其實說的就是現在的情況,React 自從引入 hooks,解決了 class 組件的“弊”,但也引入了一些問題,比如閉包問題。
就是React 的 Function Component State 管理導致的,這也可以讓開發者可以通過添加依賴或者使用 useRef 的方式進行避免。
ahooks 也意識到了這個問題,通過 useLatest 保證獲取到最新的值和 useMemoizedFn 持久化 function 的方式,避免類似的閉包陷阱。
在多說以下, useMemoizedFn 是 ahooks 輸出函數的標準,因此,所有的輸出函數都使用useMemoizedFn包一層。另外輸入函數都使用 useRef 做一次記錄,以保證在任何地方都能訪問到最新的函數。
歡迎大家關注更多精彩內容。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/128263.html
這是講 ahooks 源碼的第一篇文章,簡要就是以下幾點: 加深對 React hooks 的理解。 學習如何抽象自定義 hooks。構建屬于自己的 React hooks 工具庫。 培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。 注:本系列對 ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見詳情。 第一篇主要介紹 a...
起因 社會在不斷的向前,技術也在不斷的完善進步。從 React Hooks 正式發布到現在,越來越多的項目正在使用 Function Component 替代 Class Component,Hooks 這一新特性也逐漸被廣泛的使用。 這樣的解析是不是很熟悉,在日常中時常都有用到,但也有一個可以解決這樣重復的就是對數據請求的邏輯處理,對防抖節流的邏輯處理等。 另一方面,由于 Hoo...
陷進到處都是啊!本篇文章就說說Hooks使用時存在所謂的閉包陷阱,看看下面代碼: functionChat(){ const[text,setText]=useState(''); constonClick=useCallback(()=>{ sendMessage(text); },[]); return<SendButtononClick=...
摘要:原文鏈接有大量平均水平左右的工人可被選擇參與進來這意味著好招人有成熟的大量的程序庫可供選擇這意味著大多數項目都是既有程序庫的拼裝,標準化程度高而定制化場景少開發工具測試工具問題排查工具完善,成熟基本上沒有團隊愿意在時間緊任務重的項目情況 原文鏈接:http://pfmiles.github.io/blog/java-groovy-mixed/ 有大量平均水平左右的工人可被選擇、參與...
本篇主要和大家溝通關于ahooks ,我們可以理解為加深對 React hooks 的了解。 我們先說下關于抽象自定義 hooks。構建屬于自己的 React hooks 工具庫。 其實我們應該培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。 注:本系列對 ahooks 的源碼解析是基于v3.3.13。 現在就進入主題用ahooks 來封裝 React要注意的時機? Fun...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28