zoukankan      html  css  js  c++  java
  • 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:
    
        // * 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;

    博客园的bug可真多,尤其是这个老旧的编辑器

    您可以考虑给树发个小额微信红包以资鼓励
  • 相关阅读:
    shell脚本
    vim使用快捷键
    logback.xml_appender配置
    vim配置文件
    sed
    使用linux服务器安装wordpress博客详细教程
    JDBC为什么要使用PreparedStatement而不是Statement
    Jsp技术介绍
    jsp的appilication.getInitParameter()方法无法获取到值的问题
    ubuntu 插网线无法上网解决方案
  • 原文地址:https://www.cnblogs.com/dojo-lzz/p/14617706.html
Copyright © 2011-2022 走看看