hook原始碼其實不多,但是實現的比較精巧;在diff/index.js中會有一些optison.diff這種鉤子函式,hook中就用到了這些鉤子函式。
在比如options._diff中將currentComponent設定為null
options._diff = vnode => { currentComponent = null; if (oldBeforeDiff) oldBeforeDiff(vnode); };
比如這裡的options._render,會拿到vnode的_component屬性,將全域性的currentComponent設定為當前呼叫hook的元件。
同時這裡將currentIndex置為0。
options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode._component; currentIndex = 0; const hooks = currentComponent.__hooks; if (hooks) { hooks._pendingEffects.forEach(invokeCleanup); hooks._pendingEffects.forEach(invokeEffect); hooks._pendingEffects = []; } };
同時注意getHookState方法,第一次如果currentComponent上沒有掛載__hooks屬性,就會新建一個__hooks,同時將_list用作儲存該hook的state(state的結構根據hook不同也不一樣),_pendingEffects主要用作存放useEffect 生成state
function getHookState(index, type) { if (options._hook) { options._hook(currentComponent, index, currentHook || type); } currentHook = 0; // 可能有別的用,目前在原始碼中沒有看到用處 // Largely inspired by: // * https://github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs // * https://github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs // Other implementations to look at: // * https://codesandbox.io/s/mnox05qp8 const hooks = // 如果沒有用過hook就在元件上新增一個__hooks屬性 currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [] }); // 如果index大於當前list長度就產生一個新的物件 // 所以除了useEffect外其他都不會用到_pendingEffects屬性 if (index >= hooks._list.length) { hooks._list.push({}); } return hooks._list[index]; // 返回當前的hook state }
上面中也可以看到hook是通過陣列的形式掛載到component中,這也是hook為什麼不能在一些if語句中存在;當第一次渲染時,currentIndex為0,隨著後續useXXX方法的使用,當初次渲染結束後已經形成了一個list陣列,每一個元素就是一個hook產生的state;那麼在後續的渲染中會重置currentIndex,那麼當本次hook的方法呼叫與上次順序不同時,currentIndex的指向就會出現問題。拿到一個錯誤的結果。
hook中有四種是比較重要的
第一種useMemo系列,衍生出useCallback、useRef
所以這裡也可以看到當引數發生改變,每一次都會產生一個新的state或者在之前的基礎上修改
export function useMemo(factory, args) { /** @type {import('./internal').MemoHookState} */ const state = getHookState(currentIndex++, 7); // 獲取一個hook的state if (argsChanged(state._args, args)) { // 可以看到只有當引數改變時,hook的state會被重新修改;舊的引數被儲存在state中 state._value = factory(); // 通過factory生成,如果args不變那麼久不會執行factory state._args = args; state._factory = factory; } return state._value; // 返回狀態值 }
通過useMemo衍生的兩個hook也就比較好理解了
export function useRef(initialValue) { currentHook = 5; // 可以看到useRef只是一個有current的一個物件; return useMemo(() => ({ current: initialValue }), []); } export function useCallback(callback, args) { currentHook = 8; return useMemo(() => callback, args); }
上面中可以看到useRef返回的是一個有current屬性的物件,同時內部呼叫useMemo時傳遞的第二個引數是空陣列,這樣就保證每次呼叫useRef返回的是同一個hook state;為什麼每次傳遞一個新陣列而返回值是不同的呢,這就要看argsChanged的實現;
/** * @param {any[]} oldArgs * @param {any[]} newArgs */ function argsChanged(oldArgs, newArgs) { return ( !oldArgs || oldArgs.length !== newArgs.length || newArgs.some((arg, index) => arg !== oldArgs[index]) ); }
可以看到這種實現方式下,及時每次傳遞一個不同的空陣列,那麼argsChanged也會返回false。這也解釋了為什麼useEffect的第二個引數傳遞空陣列就會產生類似componentDidMount效果。
第二種是useEffect和useLayoutEffect
useEffect是非同步執行在每次渲染之後執行,useLayoutEffect是同步執行在瀏覽器渲染之前執行。
可以看到兩者程式碼中最直接的差異是,useEffect將state放置到component.__hooks._pendingEffects中,而useLayoutEffect將state放置到compoent的_renderCallbacks中。_renderCallbacks會在 diff後的commitRoot中執行
/** * @param {import('./internal').Effect} callback * @param {any[]} args */ export function useEffect(callback, args) { /** @type {import('./internal').EffectHookState} */ const state = getHookState(currentIndex++, 3); if (!options._skipEffects && argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent.__hooks._pendingEffects.push(state); } } /** * @param {import('./internal').Effect} callback * @param {any[]} args */ export function useLayoutEffect(callback, args) { /** @type {import('./internal').EffectHookState} */ const state = getHookState(currentIndex++, 4); if (!options._skipEffects && argsChanged(state._args, args)) { state._value = callback; state._args = args; currentComponent._renderCallbacks.push(state); } }
當然這裡的useLayoutEffect的設定的_renderCallbacks是通過在options中重寫了_commit來實現
options._commit = (vnode, commitQueue) => { commitQueue.some(component => { try { component._renderCallbacks.forEach(invokeCleanup); component._renderCallbacks = component._renderCallbacks.filter(cb => // 如果是useLayoutEffect產生的,就直接執行,否則返回true保證其他的renderCallbacks在正常的階段執行 cb._value ? invokeEffect(cb) : true ); } catch (e) { commitQueue.some(c => { if (c._renderCallbacks) c._renderCallbacks = []; }); commitQueue = []; options._catchError(e, component._vnode); } }); if (oldCommit) oldCommit(vnode, commitQueue); };
再來看下_pendingEffects的執行時機:
涉及到pendingEffects的執行是兩個options的鉤子函式,_render和diffed;diffed在元件diff完成時觸發,_render在元件的render函式呼叫之前觸發;
options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode._component; currentIndex = 0; const hooks = currentComponent.__hooks; if (hooks) { hooks._pendingEffects.forEach(invokeCleanup); hooks._pendingEffects.forEach(invokeEffect); hooks._pendingEffects = []; } }; options.diffed = vnode => { if (oldAfterDiff) oldAfterDiff(vnode); const c = vnode._component; // 如果hooks中存在pendingEffects陣列,那麼就在渲染結束後執行 if (c && c.__hooks && c.__hooks._pendingEffects.length) { afterPaint(afterPaintEffects.push(c)); } currentComponent = previousComponent; };
這裡得先看diffed函式,如果hooks中存在pendingEffects陣列,那麼就在渲染結束後執行
afterPaint函式是用來做非同步呼叫的
function afterPaint(newQueueLength) { if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) { prevRaf = options.requestAnimationFrame; (prevRaf || afterNextFrame)(flushAfterPaintEffects); } }
afterNextFrame也是利用了requestAnimationFrame函式,其中也可以看到setTimeout函式,這是因為,如果瀏覽器切換tab頁或者變為後臺程式時,requestAnimationFrame會暫停,但是setTimeout會正常進行;同時HAS_RAF也是考慮到應用到非瀏覽器環境時能夠正常執行
let HAS_RAF = typeof requestAnimationFrame == 'function'; function afterNextFrame(callback) { const done = () => { clearTimeout(timeout); if (HAS_RAF) cancelAnimationFrame(raf); setTimeout(callback); }; const timeout = setTimeout(done, RAF_TIMEOUT); let raf; if (HAS_RAF) { raf = requestAnimationFrame(done); } }
flushAfterPaintEffects是統一來在渲染結束時,處理所有的元件;
並且一次執行完畢之後會清空元件的pendingEffects。
function flushAfterPaintEffects() { afterPaintEffects.forEach(component => { if (component._parentDom) { // 有父元件的元件才會進行,第一次渲染如果麼有掛載到父元件可能不會執行 try { component.__hooks._pendingEffects.forEach(invokeCleanup); component.__hooks._pendingEffects.forEach(invokeEffect); component.__hooks._pendingEffects = []; } catch (e) { component.__hooks._pendingEffects = []; options._catchError(e, component._vnode); } } }); afterPaintEffects = []; }
同時也看到options._render,中如果存在_hooks也會對其中的pendingEffects重新執行一次;這裡我理解是對如果渲染階段沒有component._parentDom的一個補償
options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); currentComponent = vnode._component; currentIndex = 0; const hooks = currentComponent.__hooks; if (hooks) { hooks._pendingEffects.forEach(invokeCleanup); hooks._pendingEffects.forEach(invokeEffect); hooks._pendingEffects = []; } };
從中也可以看到useEffect設計會帶來一些天然的坑,比如useEffect需要清除功能時,不能設定第二個引數為空陣列;
- 如果設定第二個引數為空陣列,這種情況下在diffed和_render中都會將pendingEffects進行清除,永遠不會執行到清除函式。
- 當useEffect沒有第二個引數,那麼第一次渲染後options.diffed函式中的state._value執行,生成state._cleanup,清除pendingEffects;如果函式任意狀態改變,在options._render階段沒有pendingEffects不會執行cleanup和state._value;在元件render階段,state._value被重新改變,將state裝入pendingEffects中;在options.diffed中執行invokeCleanup和invokeEffect
- 當useEffect設定第二個引數為非空陣列,那麼第一次渲染後options.diffed函式中的state._value執行,生成state._cleanup,清除pendingEffects;只有當useEffect的依賴項改變時(非依賴項變動不會執行該useEffect的清除函式),在options._render階段沒有pendingEffects不會執行cleanup和state._value;在元件render階段,state._value被重新改變,將state裝入pendingEffects中;在options.diffed中執行invokeCleanup和invokeEffect
第三種是useReducer,以及衍生的useState
useReducer程式碼不對,有幾個地方需要重點關注一下:
主要是action函式內部這一段:
action => { // 通過action來執行reducer獲取到下一個狀態 const nextValue = hookState._reducer(hookState._value[0], action); // 狀態不等就進行重新賦值,並且觸發渲染,新的渲染還是返回hookState._value,但是_value的值已經被修改了 if (hookState._value[0] !== nextValue) { hookState._value = [nextValue, hookState._value[1]]; // 在diff/index.js中可以看到如果是函式元件沒有render方法,那麼會對PReact.Component進行例項化 // 這時候呼叫setState方法同樣會觸發元件的渲染流程 hookState._component.setState({}); } }
export function useReducer(reducer, initialState, init) { const hookState = getHookState(currentIndex++, 2); hookState._reducer = reducer; // 掛載reducer if (!hookState._component) { // hookState麼有_component屬性代表第一次渲染 hookState._value = [ !init ? invokeOrReturn(undefined, initialState) : init(initialState), action => { // 通過action來執行reducer獲取到下一個狀態 const nextValue = hookState._reducer(hookState._value[0], action); // 狀態不等就進行重新賦值,並且觸發渲染,新的渲染還是返回hookState._value,但是_value的值已經被修改了 if (hookState._value[0] !== nextValue) { hookState._value = [nextValue, hookState._value[1]]; // 在diff/index.js中可以看到如果是函式元件沒有render方法,那麼會對PReact.Component進行例項化 // 這時候呼叫setState方法同樣會觸發元件的渲染流程 hookState._component.setState({}); } } ]; hookState._component = currentComponent; } return hookState._value; }
而useState就很簡單了,只是呼叫一下useReducer,
而useState就很簡單了,只是呼叫一下useReducer, export function useState(initialState) { currentHook = 1; return useReducer(invokeOrReturn, initialState); } function invokeOrReturn(arg, f) { return typeof f == 'function' ? f(arg) : f; }
第四種 useContext
在diff中得到了componentContext掛載到了元件的context屬性中
export function useContext(context) { // create-context中返回的是一個context物件,得到provide物件 // Provider元件在diff時,判斷沒有render方法時,會先用Compoent來例項化一個物件 // 並將render方法設定為doRender,並將constructor指向newType(當前函式),在doRender中呼叫this.constructor方法 const provider = currentComponent.context[context._id]; const state = getHookState(currentIndex++, 9); state._context = context; // 掛載到state的_context屬性中 if (!provider) return context._defaultValue; // 如果麼有provider永遠返回context的初始值。 if (state._value == null) { // 初次渲染則將元件對provider進行訂閱 state._value = true; provider.sub(currentComponent); } return provider.props.value; } useContext使用示例: import React, { useState ,,useContext, createContext} from 'react'; import './App.css'; // 建立一個 context const Context = createContext(0) // 元件一, useContext 寫法 function Item3 () { const count = useContext(Context); return ( <div>{ count }</div> ) } function App () { const [ count, setCount ] = useState(0) return ( <div> 點選次數: { count } <button onClick={() => { setCount(count + 1)}}>點我</button> <Context.Provider value={count}> {/* <Item1></Item1> <Item2></Item2> */} <Item3></Item3> </Context.Provider> </div> ) } export default App;
部落格園我也真是服了,一個以技術為主的部落格網站,竟然每次進入編輯器,插入程式碼功能只能用一次,第二次死活提交不上,必須關閉瀏覽器重新開啟,我真特麼服!!!!
這是TMD把人往掘金、簡書、知乎、segmentfault上逼啊!!!!!!!!!!!!!!!!!!