React源码解析之FunctionComponent(上)

  • 2019 年 12 月 2 日
  • 筆記

前言

在 React源码解析之workLoop 中讲到当workInProgress.tagFunctionComponent时,会进行FunctionComponent的更新:

    //FunctionComponent的更新      case FunctionComponent: {        //React 组件的类型,FunctionComponent的类型是 function,ClassComponent的类型是 class        const Component = workInProgress.type;        //下次渲染待更新的 props        const unresolvedProps = workInProgress.pendingProps;        // pendingProps        const resolvedProps =          workInProgress.elementType === Component            ? unresolvedProps            : resolveDefaultProps(Component, unresolvedProps);        //更新 FunctionComponent        //可以看到大部分是workInProgress的属性        //之所以定义变量再传进去,是为了“冻结”workInProgress的属性,防止在 function 里会改变workInProgress的属性        return updateFunctionComponent(          //workInProgress.alternate          current,          workInProgress,          //workInProgress.type          Component,          //workInProgress.pendingProps          resolvedProps,          renderExpirationTime,        );      }

本文就来分析FunctionComponent是如何更新的

一、updateFunctionComponent

作用: 执行FunctionComponent的更新

源码:

//更新 functionComponent  //current:workInProgress.alternate  //Component:workInProgress.type  //resolvedProps:workInProgress.pendingProps  function updateFunctionComponent(    current,    workInProgress,    Component,    nextProps: any,    renderExpirationTime,  ) {    //删掉了 dev 代码    //后面讲 context 的时候再作说明    const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);    const context = getMaskedContext(workInProgress, unmaskedContext);      let nextChildren;    //做update 标记可不看    prepareToReadContext(workInProgress, renderExpirationTime);    prepareToReadEventComponents(workInProgress);    //删掉了 dev 代码      //在渲染的过程中,对里面用到的 hook函数做一些操作      nextChildren = renderWithHooks(        current,        workInProgress,        Component,        nextProps,        context,        renderExpirationTime,      );      //如果不是第一次渲染,并且没有接收到更新的话    //didReceiveUpdate:更新上的优化    if (current !== null && !didReceiveUpdate) {      bailoutHooks(current, workInProgress, renderExpirationTime);      return bailoutOnAlreadyFinishedWork(        current,        workInProgress,        renderExpirationTime,      );    }      // React DevTools reads this flag.    //表明当前组件在渲染的过程中有被更新到    workInProgress.effectTag |= PerformedWork;    //将 ReactElement 变成 fiber对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上    reconcileChildren(      current,      workInProgress,      nextChildren,      renderExpirationTime,    );    return workInProgress.child;  }

解析: (1) 在「前言」的代码里也可以看到,传入updateFunctionComponent的大部分参数都是workInProgress这个 fiber 对象的属性

我在看这段的时候,忽然冒出一个疑问,为什么不直接传一个workInProgress对象呢? 我自己的猜测是在外面「冻结」这些属性,防止在updateFunctionComponent()中,修改这些属性

(2) 在updateFunctionComponent()中,主要是执行了两个函数: ① renderWithHooks() ② reconcileChildren()

执行完这两个方法后,最终返回workInProgress.child,即正在执行更新的 fiber 对象的第一个子节点

(3) bailoutOnAlreadyFinishedWork()在 React源码解析之workLoop 中已经解析过,其作用是 跳过该节点及该节点上所有子节点的更新

(4) bailoutHooks() 的源码不多,作用是 跳过 hooks 函数的更新:

//跳过hooks更新  export function bailoutHooks(    current: Fiber,    workInProgress: Fiber,    expirationTime: ExpirationTime,  ) {    workInProgress.updateQueue = current.updateQueue;    workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect);    //置为NoWork 不更新    if (current.expirationTime <= expirationTime) {      current.expirationTime = NoWork;    }  }

二、renderWithHooks

作用: 在渲染的过程中,对里面用到的 hooks 函数做一些操作

源码:

//渲染的过程中,对里面用到的 hook函数做一些操作  export function renderWithHooks(    current: Fiber | null,    workInProgress: Fiber,    Component: any,    props: any,    refOrContext: any,    nextRenderExpirationTime: ExpirationTime,  ): any {    renderExpirationTime = nextRenderExpirationTime;    //当前正要渲染的 fiber 对象    currentlyRenderingFiber = workInProgress;    //第一次的 state 状态    nextCurrentHook = current !== null ? current.memoizedState : null;    //删除了 dev 代码      // The following should have already been reset    // currentHook = null;    // workInProgressHook = null;      // remainingExpirationTime = NoWork;    // componentUpdateQueue = null;      // didScheduleRenderPhaseUpdate = false;    // renderPhaseUpdates = null;    // numberOfReRenders = 0;    // sideEffectTag = 0;      // TODO Warn if no hooks are used at all during mount, then some are used during update.    // Currently we will identify the update render as a mount because nextCurrentHook === null.    // This is tricky because it's valid for certain types of components (e.g. React.lazy)      // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.    // Non-stateful hooks (e.g. context) don't get added to memoizedState,    // so nextCurrentHook would be null during updates and mounts.      //删除了 dev 代码        //第一次渲染调用HooksDispatcherOnMount      //多次渲染调用HooksDispatcherOnUpdate        //用来存放 useState、useEffect 等 hook 函数的对象      ReactCurrentDispatcher.current =        nextCurrentHook === null          ? HooksDispatcherOnMount          : HooksDispatcherOnUpdate;      //workInProgress.type,这里能当做 function 使用,说明 type 是 function    let children = Component(props, refOrContext);    //判断在执行 render的过程中是否有预定的更新      //当有更新要渲染时    if (didScheduleRenderPhaseUpdate) {      do {        //置为 false 说明该循环只会执行一次        didScheduleRenderPhaseUpdate = false;        //重新渲染时fiber 的节点数        numberOfReRenders += 1;          // Start over from the beginning of the list        //记录 state,以便重新执行这个 FunctionComponent 内部的几个 useState 函数        nextCurrentHook = current !== null ? current.memoizedState : null;        nextWorkInProgressHook = firstWorkInProgressHook;        //释放当前 state        currentHook = null;        workInProgressHook = null;        componentUpdateQueue = null;          if (__DEV__) {          // Also validate hook order for cascading updates.          hookTypesUpdateIndexDev = -1;        }        //HooksDispatcherOnUpdate        ReactCurrentDispatcher.current = __DEV__          ? HooksDispatcherOnUpdateInDEV          : HooksDispatcherOnUpdate;          children = Component(props, refOrContext);      } while (didScheduleRenderPhaseUpdate);        renderPhaseUpdates = null;      numberOfReRenders = 0;    }      // We can assume the previous dispatcher is always this one, since we set it    // at the beginning of the render phase and there's no re-entrancy.    ReactCurrentDispatcher.current = ContextOnlyDispatcher;      //定义新的 fiber 对象    const renderedWork: Fiber = (currentlyRenderingFiber: any);    //为属性赋值    renderedWork.memoizedState = firstWorkInProgressHook;    renderedWork.expirationTime = remainingExpirationTime;    renderedWork.updateQueue = (componentUpdateQueue: any);    renderedWork.effectTag |= sideEffectTag;      if (__DEV__) {      renderedWork._debugHookTypes = hookTypesDev;    }      // This check uses currentHook so that it works the same in DEV and prod bundles.    // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.    const didRenderTooFewHooks =      currentHook !== null && currentHook.next !== null;      //重置    renderExpirationTime = NoWork;    currentlyRenderingFiber = null;      currentHook = null;    nextCurrentHook = null;    firstWorkInProgressHook = null;    workInProgressHook = null;    nextWorkInProgressHook = null;      if (__DEV__) {      currentHookNameInDev = null;      hookTypesDev = null;      hookTypesUpdateIndexDev = -1;    }      remainingExpirationTime = NoWork;    componentUpdateQueue = null;    sideEffectTag = 0;      // These were reset above    // didScheduleRenderPhaseUpdate = false;    // renderPhaseUpdates = null;    // numberOfReRenders = 0;      invariant(      !didRenderTooFewHooks,      'Rendered fewer hooks than expected. This may be caused by an accidental ' +        'early return statement.',    );      return children;  }

解析: 在开发者使用FunctionComponent来写 React 组件的时候,是不能用setState的,取而代之的是useState()useEffect等 Hook API

所以在更新FunctionComponent的时候,会先执行renderWithHooks()方法,来处理这些 hooks

(1) nextCurrentHook 是根据current来赋值的,所以 nextCurrentHook 也可以用来判断是否是 组件第一次渲染

(2) 无论是HooksDispatcherOnMount还是HooksDispatcherOnUpdate,它们都是 存放 useState、useEffect 等 hook 函数的对象:

const HooksDispatcherOnMount: Dispatcher = {    readContext,      useCallback: mountCallback,    useContext: readContext,    useEffect: mountEffect,    useImperativeHandle: mountImperativeHandle,    useLayoutEffect: mountLayoutEffect,    useMemo: mountMemo,    useReducer: mountReducer,    useRef: mountRef,    useState: mountState,    useDebugValue: mountDebugValue,    useEvent: updateEventComponentInstance,  };    const HooksDispatcherOnUpdate: Dispatcher = {    readContext,      useCallback: updateCallback,    useContext: readContext,    useEffect: updateEffect,    useImperativeHandle: updateImperativeHandle,    useLayoutEffect: updateLayoutEffect,    useMemo: updateMemo,    useReducer: updateReducer,    useRef: updateRef,    useState: updateState,    useDebugValue: updateDebugValue,    useEvent: updateEventComponentInstance,  };

可以看到,每个 Hook API 都对应一个更新的方法,这些我们后面再细说

(3) let children = Component(props, refOrContext);这行我其实没看懂,因为ComponentworkInProgress.type,它的值可以是function或是class,但我没想到可以当做方法去调用Component(props, refOrContext)

所以我现在暂时还不知道 children 到底是个啥,后面如果有新发现的话,会在「前言」中提到。

(4) 然后是当didScheduleRenderPhaseUpdatetrue时,执行一个while循环,在循环中,会保存 state 的状态,并重置 hook、组件更新队列为 null,最终再次执行Component(props, refOrContext),得出新的 children

didScheduleRenderPhaseUpdate:

// Whether an update was scheduled during the currently executing render pass.  //判断在执行 render的过程中是否有预定的更新  let didScheduleRenderPhaseUpdate: boolean = false;

这个循环,我的一个疑惑是,while中将didScheduleRenderPhaseUpdate置为false,那么这个循环只会执行一次,为什么要用while? 为什么没用if...else

暂时也是没有答案

(5) 定义新的 fiber 对象来保留操作 hooks 后得到的一些变量,最后再将有关 hooks 的变量都置为 null,return children

三、reconcileChildren

作用: 将 ReactElement 变成fiber对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上

源码:

export function reconcileChildren(    current: Fiber | null,    workInProgress: Fiber,    nextChildren: any,    renderExpirationTime: ExpirationTime,  ) {    if (current === null) {      // If this is a fresh new component that hasn't been rendered yet, we      // won't update its child set by applying minimal side-effects. Instead,      // we will add them all to the child before it gets rendered. That means      // we can optimize this reconciliation pass by not tracking side-effects.        //因为是第一次渲染,所以不存在current.child,所以第二个参数传的 null      //React第一次渲染的顺序是先父节点,再是子节点        workInProgress.child = mountChildFibers(        workInProgress,        null,        nextChildren,        renderExpirationTime,      );    } else {      // If the current child is the same as the work in progress, it means that      // we haven't yet started any work on these children. Therefore, we use      // the clone algorithm to create a copy of all the current children.        // If we had any progressed work already, that is invalid at this point so      // let's throw it out.      workInProgress.child = reconcileChildFibers(        workInProgress,        current.child,        nextChildren,        renderExpirationTime,      );    }  }

解析: mountChildFibers()reconcileChildFibers()调用的是同一个函数ChildReconciler

//true false  export const reconcileChildFibers = ChildReconciler(true);  export const mountChildFibers = ChildReconciler(false);

false 表示是第一次渲染,true 反之

四、ChildReconciler

作用:reconcileChildren()

这个方法有 1100 多行,前面全是 function 的定义,最后返回reconcileChildFibers,所以我们从后往前看

源码:

//是否跟踪副作用  function ChildReconciler(shouldTrackSideEffects) {    xxx    xxx    xxx        function reconcileChildFibers(): Fiber | null {      }      return reconcileChildFibers;  }

解析: 第一次渲染时无副作用(sideEffect)的,所以shouldTrackSideEffects=false,多次渲染是有副作用的,所以shouldTrackSideEffects=true

这个方法太长了,先看最后 return 的reconcileChildFibers

五、reconcileChildFibers

作用: 针对不同类型的节点,进行不同的节点操作

源码:

 // This API will tag the children with the side-effect of the reconciliation    // itself. They will be added to the side-effect list as we pass through the    // children and the parent.    function reconcileChildFibers(      returnFiber: Fiber,      currentFirstChild: Fiber | null,      //新计算出来的 children      newChild: any,      expirationTime: ExpirationTime,    ): Fiber | null {      // This function is not recursive.      // If the top level item is an array, we treat it as a set of children,      // not as a fragment. Nested arrays on the other hand will be treated as      // fragment nodes. Recursion happens at the normal flow.        // Handle top level unkeyed fragments as if they were arrays.      // This leads to an ambiguity between <>{[...]}</> and <>...</>.      // We treat the ambiguous cases above the same.      const isUnkeyedTopLevelFragment =        typeof newChild === 'object' &&        newChild !== null &&        //在开发中写<div>{ arr.map((a,b)=>xxx) }</div>,这种节点称为 REACT_FRAGMENT_TYPE        newChild.type === REACT_FRAGMENT_TYPE &&        newChild.key === null;      //type 为REACT_FRAGMENT_TYPE是不需要任何更新的,直接渲染子节点即可      if (isUnkeyedTopLevelFragment) {        newChild = newChild.props.children;      }        // Handle object types      const isObject = typeof newChild === 'object' && newChild !== null;      //element 节点      if (isObject) {        switch (newChild.$$typeof) {          // ReactElement节点          case REACT_ELEMENT_TYPE:            return placeSingleChild(              reconcileSingleElement(                returnFiber,                currentFirstChild,                newChild,                expirationTime,              ),            );          //ReactDOM.createPortal(child, container)          //https://zh-hans.reactjs.org/docs/react-dom.html#createportal          case REACT_PORTAL_TYPE:            return placeSingleChild(              reconcileSinglePortal(                returnFiber,                currentFirstChild,                newChild,                expirationTime,              ),            );        }      }      //文本节点      if (typeof newChild === 'string' || typeof newChild === 'number') {        return placeSingleChild(          reconcileSingleTextNode(            returnFiber,            currentFirstChild,            '' + newChild,            expirationTime,          ),        );      }      //数组节点      if (isArray(newChild)) {        return reconcileChildrenArray(          returnFiber,          currentFirstChild,          newChild,          expirationTime,        );      }      //IteratorFunction      if (getIteratorFn(newChild)) {        return reconcileChildrenIterator(          returnFiber,          currentFirstChild,          newChild,          expirationTime,        );      }      //如果未符合上述的 element 节点的要求,则报错      if (isObject) {        throwOnInvalidObjectType(returnFiber, newChild);      }          //删除了 dev 代码        //报出警告,可不看      if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {        // If the new child is undefined, and the return fiber is a composite        // component, throw an error. If Fiber return types are disabled,        // we already threw above.          //即workInProgress,正在更新的节点        switch (returnFiber.tag) {          case ClassComponent: {            //删除了 dev 代码            }          // Intentionally fall through to the next case, which handles both          // functions and classes          // eslint-disable-next-lined no-fallthrough          case FunctionComponent: {            const Component = returnFiber.type;            invariant(              false,              '%s(...): Nothing was returned from render. This usually means a ' +              'return statement is missing. Or, to render nothing, ' +              'return null.',              Component.displayName || Component.name || 'Component',            );          }        }      }        // Remaining cases are all treated as empty.        //如果旧节点存在,但是更新的节点是 null 的话,需要删除旧节点的内容      return deleteRemainingChildren(returnFiber, currentFirstChild);    }

解析: ① isUnkeyedTopLevelFragment 当我们在开发中写了 如

<div>{ arr.map((a,b)=>xxx) }</div>

的代码的时候,这种节点类型会被判定为REACT_FRAGMENT_TYPE,React 会直接渲染它的子节点:

newChild = newChild.props.children;

② 如果 element type 是 object 的话,也就是ClassComponentFunctionComponent会有两种情况: 一个是REACT_ELEMENT_TYPE,即我们常见的 ReactElement 节点; 另一个是REACT_PORTAL_TYPE,portal 节点,通常被应用于 对话框、悬浮卡、提示框上,具体请参考官方文档:Portals(https://zh-hans.reactjs.org/docs/portals.html)

REACT_ELEMENT_TYPE 的话,会执行reconcileSingleElement方法

③ 如果是文本节点的话,会执行reconcileSingleTextNode方法

④ 如果执行到最后的deleteRemainingChildren话,说明待更新的节点是 null,需要删除原有旧节点的内容

可以看到ChildReconciler中的reconcileChildFibers方法的作用就是根据新节点newChild的节点类型,来执行不同的操作节点函数

下篇文章,会讲reconcileSingleElementreconcileSingleTextNodedeleteRemainingChildren

GitHub

ReactFiberBeginWork:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberBeginWork.js

ReactFiberHooks:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberHooks.js

ReactChildFiber:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactChildFiber.js