PReact10.5.13源碼理解之hook

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:

    // * //github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs

    // * //github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs

    // Other implementations to look at:

    // * //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上逼啊!!!!!!!!!!!!!!!!!!