PReact10.5.13原始碼理解之hook

木的樹發表於2021-04-05
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上逼啊!!!!!!!!!!!!!!!!!!

 

 

 

 

相關文章