React源码看过几次,每次都没有坚持下来,索性学习一下PReact部分,网上讲解源码的不少,但是基本已经过时,所以自己来梳理下

render.js部分

import { EMPTY_OBJ, EMPTY_ARR } from './constants';
import { commitRoot, diff } from './diff/index';
import { createElement, Fragment } from './create-element';
import options from './options';

/**
 * Render a Preact virtual node into a DOM element
 * @param {import('./internal').ComponentChild} vnode The virtual node to render
 * @param {import('./internal').PreactElement} parentDom The DOM element to
 * render into
 * @param {import('./internal').PreactElement | object} [replaceNode] Optional: Attempt to re-use an
 * existing DOM tree rooted at `replaceNode`
 */
export function render(vnode, parentDom, replaceNode) {
    if (options._root) options._root(vnode, parentDom);

    // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in
    // hydration mode or not by passing the `hydrate` function instead of a DOM
    // element..
    let isHydrating = typeof replaceNode === 'function';

    // To be able to support calling `render()` multiple times on the same
    // DOM node, we need to obtain a reference to the previous tree. We do
    // this by assigning a new `_children` property to DOM nodes which points
    // to the last rendered tree. By default this property is not present, which
    // means that we are mounting a new tree for the first time.
     // 为了支持多次在一个dom节点上调用render函数,需要在dom节点上添加一个饮用,用来获取指向上一次渲染的虚拟dom树。
     // 这个属性默认是指向空的,也意味着我们第一次正在装备一颗新的树
     // 所以开始时这里的oldVNode是空(不论isHydrating的值),但是如果重复在这个节点上调用render那oldVNode是有值的
    let oldVNode = isHydrating
        ? null
        : (replaceNode && replaceNode._children) || parentDom._children;

    // 用Fragment包裹一下vnode,同时给replaceNode和parentDom的_children赋值
     vnode = (
        (!isHydrating && replaceNode) ||
        parentDom
    )._children = createElement(Fragment, null, [vnode]);

    // List of effects that need to be called after diffing.
     // 用来放置diff之后需要进行各种生命周期处理的Component,比如cdm、cdu;componentWillUnmount在diffChildren的unmount函数中执行不在commitRoot时执行
    let commitQueue = [];
    diff(
        parentDom, // 这个使用parentDom的_children属性已经指向[vnode]了
        // Determine the new vnode tree and store it on the DOM element on
        // our custom `_children` property.
        vnode,
        oldVNode || EMPTY_OBJ, // 旧的树
        EMPTY_OBJ,
        parentDom.ownerSVGElement !== undefined,
              // excessDomChildren,这个参数用来做dom复用的作用
        !isHydrating && replaceNode
            ? [replaceNode]
            : oldVNode
            ? null
            : parentDom.firstChild // 如果parentDom有子节点就会把整个子节点作为待复用的节点使用
            ? EMPTY_ARR.slice.call(parentDom.childNodes)
            : null,
        commitQueue,
              // oldDom,在后续方法中用来做标记插入位置使用
        !isHydrating && replaceNode
            ? replaceNode
            : oldVNode
            ? oldVNode._dom
            : parentDom.firstChild,
        isHydrating
    );

    // Flush all queued effects
     // 调用所有commitQueue中的节点_renderCallbacks中的方法
    commitRoot(commitQueue, vnode);
}

/**
 * Update an existing DOM element with data from a Preact virtual node
 * @param {import('./internal').ComponentChild} vnode The virtual node to render
 * @param {import('./internal').PreactElement} parentDom The DOM element to
 * update
 */
export function hydrate(vnode, parentDom) {
    render(vnode, parentDom, hydrate);
}

 

create-context.js部分

Context的使用:

Provider的props中有value属性

Consumer中直接获取传值

import { createContext, h, render } from 'preact';

const FontContext = createContext(20);

function Child() {
  return <FontContext.Consumer>
    {fontSize=><div style={{fontSize:fontSize}}>child</div>}
  </FontContext.Consumer>
}
function App(){
  return <Child/>
}
render(
  <FontContext.Provider value={26}>
    <App/>
  </FontContext.Provider>,
  document.getElementById('app')
);

看一下源码:

import { enqueueRender } from './component';

export let i = 0;

export function createContext(defaultValue, contextId) {
    contextId = '__cC' + i++; // 生成一个唯一ID

    const context = {
        _id: contextId,
        _defaultValue: defaultValue,
        /** @type {import('./internal').FunctionComponent} */
        Consumer(props, contextValue) {
            // return props.children(
            //     context[contextId] ? context[contextId].props.value : defaultValue
            // );
            return props.children(contextValue);
        },
        /** @type {import('./internal').FunctionComponent} */
        Provider(props) {
            if (!this.getChildContext) { // 第一次调用时进行一些初始化操作
                let subs = [];
                let ctx = {};
                ctx[contextId] = this;
                            
                            // 在diff操作用,如果判断一个组件在Comsumer中,会调用sub进行订阅;
                            // 同时这个节点后续所有diff的地方都会带上这个context,调用sub方法进行调用
                            // context具有层级优先级,组件会先加入最近的context中
                this.getChildContext = () => ctx; 

                this.shouldComponentUpdate = function(_props) {
                    if (this.props.value !== _props.value) {
                        // I think the forced value propagation here was only needed when `options.debounceRendering` was being bypassed:
                        // https://github.com/preactjs/preact/commit/4d339fb803bea09e9f198abf38ca1bf8ea4b7771#diff-54682ce380935a717e41b8bfc54737f6R358
                        // In those cases though, even with the value corrected, we're double-rendering all nodes.
                        // It might be better to just tell folks not to use force-sync mode.
                        // Currently, using `useContext()` in a class component will overwrite its `this.context` value.
                        // subs.some(c => {
                        //     c.context = _props.value;
                        //     enqueueRender(c);
                        // });

                        // subs.some(c => {
                        //     c.context[contextId] = _props.value;
                        //     enqueueRender(c);
                        // });
                                              // enqueueRender最终会进入renderComponent函数,进行diff、commitRoot、updateParentDomPointers等操作
                        subs.some(enqueueRender);
                    }
                };

                this.sub = c => {
                    subs.push(c);// 进入订阅数组,
                    let old = c.componentWillUnmount;
                    c.componentWillUnmount = () => { // 重写componentWillUnmount
                        subs.splice(subs.indexOf(c), 1);
                        if (old) old.call(c);
                    };
                };
            }

            return props.children;
        }
    };

    // Devtools needs access to the context object when it
    // encounters a Provider. This is necessary to support
    // setting `displayName` on the context object instead
    // of on the component itself. See:
    // https://reactjs.org/docs/context.html#contextdisplayname
    // createContext最终返回的是一个context对象,带着Provider和Consumer两个函数
    // 同时Consumber函数的contextType和Provider函数的_contextRef属性都指向context
    return (context.Provider._contextRef = context.Consumer.contextType = context);
}

所以对于Provider组件,在渲染时会判断有没有getChildContext方法,如果有的话调用得到globalContext并一直向下传递下去

                    if (c.getChildContext != null) {
                globalContext = assign(assign({}, globalContext), c.getChildContext());
            }

            if (!isNew && c.getSnapshotBeforeUpdate != null) {
                snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
            }

            let isTopLevelFragment =
                tmp != null && tmp.type === Fragment && tmp.key == null;
            let renderResult = isTopLevelFragment ? tmp.props.children : tmp;

            diffChildren(
                parentDom,
                Array.isArray(renderResult) ? renderResult : [renderResult],
                newVNode,
                oldVNode,
                globalContext,
                isSvg,
                excessDomChildren,
                commitQueue,
                oldDom,
                isHydrating
            );

当渲染遇到Consumer时,即遇到contextType属性,先从Context中拿到provider,然后拿到provider的props的value值,作为组件要获取的上下文信息。

 

同时这时候会调用provider的sub方法,进行订阅,当调用到Provider的shouldComponentUpdate中发现value发生变化时就会将所有的订阅者进入enqueueRender函数。

 

 

所以源码中,globalContext对象的每一个key指向一个Context.Provider;componentContext代表组件所在的Consumer传递的上下文信息即配对的Provider的props的value;

同时Provider的shouldComponentUpdate方法中用到了 ·this.props.value !== _props.value· 那么这里的this.props是哪来的?Provider中并没有相关属性。

主要是下面这个地方,当判断没有render方法时,会先用Compoent来实例化一个对象,并将render方法设置为doRender,并将constructor指向newType(当前函数),在doRender中调用this.constructor方法

// Instantiate the new component
                if ('prototype' in newType && newType.prototype.render) {
                    // @ts-ignore The check above verifies that newType is suppose to be constructed
                    newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
                } else {
                    // @ts-ignore Trust me, Component implements the interface we want
                    newVNode._component = c = new Component(newProps, componentContext);
                    c.constructor = newType;
                    c.render = doRender;
                }
/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
    return this.constructor(props, context);
}

 

diff部分

diff部分比较复杂,整体整理了一张大图

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

不过unmount阶段,所有的useEffect返回的回调都会被执行,因为unmount函数针对的是所有的hooks而不是只进入到pendingEffects中的hook

options.unmount = vnode => {
    if (oldBeforeUnmount) oldBeforeUnmount(vnode);

    const c = vnode._component;
    if (c && c.__hooks) {
        try {
            c.__hooks._list.forEach(invokeCleanup);
        } catch (e) {
            options._catchError(e, c._vnode);
        }
    }
};

 

 

 

 

第三种是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;

 


木的树
25 声望2 粉丝