淺談 React Hooks(二)
在上一篇文章中,我們談到 Hooks 給 React 帶來的一些在開發體驗上的改變,如果你已經開始嘗試 React Hooks,也許你會跟我一樣碰到一個令人疑惑的地方,如果沒有的話,那就再好不過啦,我就權當做個記錄,以便他人之需。
如何繫結事件?
我們先以官方的例子開始:
import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } 複製程式碼
看到onClick
繫結的那個匿名函數了嗎?這樣寫的話,每次 render 的時候都會重新生成一個新的函式。這在之前可能不需要太在意,因為我們一般只是拿 Function Component 來實現一些展示型元件,在其之下不會有太多的子元件。但是如果我們擁抱 Hooks 之後,那麼就不可控了。
雖然說在一般情況下,這並不會造成太大的效能問題,而且 Function Component 本身的效能就要比 Class Component 更好一點,但是難免會碰到需要優化的時候,比方說在重構原來的 Class Component 的時候,其中有個子元件是個PureComponent
,便會使子元件的這個優化失效 ,那麼怎麼解決呢?
使用useCallback
或useMemo
來儲存函式的引用,避免重複生成新的函式
function Counter() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(count => count + 1) }, []); // 或者用useMemo // const handleClick = useMemo(() => () => {setCount(count => count + 1)}, []); return ( <div> <p>count: {count}</p> {/* Child為PureComponent */} <Child callback={handleClick} /> </div> ) } 複製程式碼
可見useCallback(fn, inputs)
等同於useMemo(() => fn, inputs)
,那麼這兩個 Hook 具體是怎麼做到的呢?我們可以從原始碼中一窺究竟,我們以useCallback
為例(useMemo
大體上都是一樣的,就返回值不同,後面會提到)。
首先,在第一次執行useCallback
時,React內部會呼叫ReactFiberHooks
中的mountCallback
,之後再次執行時呼叫的都是updateCallback
,具體程式碼可以看這裡:github.com/facebook/re…
我們一點點來看,先看下mountCallback
:
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; } 複製程式碼
發現核心在於mountWorkInProgressHook
這個方法
function mountWorkInProgressHook(): Hook { const hook: Hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list firstWorkInProgressHook = workInProgressHook = hook; } else { // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; } 複製程式碼
程式碼比較簡單,就不一一解釋了,從上面的程式碼我們可以得知 Hooks 的本體:
const hook = { memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, } 複製程式碼
我們主要關注memoizedState
和next
,memoizedState
在不同的 Hook 中存放的值會有所不同,在useCallback
中存的就是入參的值[callback, deps]
,next
的值就是下一個 hook,也就是說 Hooks 其實就是一個單向連結串列,這也就解釋了為什麼 Hooks 需要在頂層呼叫,不能在迴圈、條件語句、巢狀函式中使用,因為需要保證每次呼叫的順序一致。
再來看之後的updateCallback
:
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T { // 這個hook就是第一次mount的hook const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 所以這裡的memoizedState就是mount時候存著的[callback, deps] const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; // 比較兩次的deps,相同的話就直接返回之前存的callback,而不是新傳進來的callback if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; } 複製程式碼
useMemo
的實現與useCallback
類似,大概看一下:
function mountMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // 與useCallback不同的地方就是memoizedState中存的是nextCreate執行之後的結果 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; // 返回執行結果 return nextValue; } function updateMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } } // 這裡也一樣,存的是nextCreate執行之後的結果 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; // 返回執行結果 return nextValue; } 複製程式碼
由以上程式碼便可以看出useCallback
和useMemo
在用法上的區別了。
除了這兩個方法以外,還可以通過context
來傳遞由useReducer
生成的dispatch
方法,來避免直接傳遞callback
,因為dispatch
是不變的。這個方法跟前面兩種有本質上的區別,它從源頭上就阻止了callback的傳遞,所以也就不會有前面提到的效能方面的顧慮,這也是官方推薦的方法,特別是元件樹很大的情況下。所以上面的程式碼如果通過這種方式來寫的話,就會是下面這樣,有點像Redux
:
import React, { useReducer, useContext } from 'react'; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; default: throw new Error(); } } const TodosDispatch = React.createContext(null); function Counter() { const [state, dispatch] = useReducer(reducer, {count: 0}); return ( <div> <p>count: {state.count}</p> <TodosDispatch.Provider value={dispatch}> <Child /> </TodosDispatch.Provider> </div> ) } function Child() { const dispatch = useContext(TodosDispatch); return ( <button onClick={() => dispatch({type: 'increment'})}> click </button> ) } 複製程式碼