本文是深入淺出 ahooks 原始碼系列文章的第三篇,該系列已整理成檔案-地址,覺得還不錯,給個 star 支持一下哈,Thanks,
本文來探索一下 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 做一次記錄,以保證在任何地方都能訪問到最新的函式,
參考
- 從react hooks“閉包陷阱”切入,淺談react hooks
- React官方團隊出手,補齊原生Hook短板
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/501643.html
標籤:其他
上一篇:ES5及ES6的新增特性
