专栏首页前端干货和生活感悟React源码解析之FunctionComponent(上)

React源码解析之FunctionComponent(上)

前言

在 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

本文分享自微信公众号 - webchen(webchen1995),作者:webchen

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React源码解析之「错误处理」流程

    在 React源码解析之renderRoot概览 中提到了,当有异常抛出的时候,会执行completeUnitOfWork():

    进击的小进进
  • React源码解析之completeWork和HostText的更新

    前言: 在 React源码解析之completeUnitOfWork 中,提到了completeWork()的作用是更新该节点(commit阶段会将其转成真实的...

    进击的小进进
  • React源码解析之FunctionComponent(中)

    作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

    进击的小进进
  • solidity智能合约

    Solidity里的智能合约是面向对象语言里的类。它们持久存放在状态变量和函数中,(在里面)可以通过solidity修改这些变量。在不同的智能合约(实例)中调用...

    笔阁
  • React源码解析之FunctionComponent(中)

    作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

    进击的小进进
  • JavaScript贪食蛇游戏制作详解

    之前闲时开发过一个简单的网页版贪食蛇游戏程序,现在把程序的实现思路写下来,供有兴趣同学参考阅读。 代码的实现比较简单,整个程序由三个类,一组常量和一些游戏逻辑...

    用户1608022
  • 机器学习入门 8-3 过拟合与欠拟合

    本系列是《玩转机器学习教程》一个整理的视频笔记。通过之前的小节了解了多项式回归的基本思路,有了多项式就可以很轻松的对非线性数据进行拟合,进而求解非线性回归的问题...

    触摸壹缕阳光
  • 1108. IP 地址无效化

    给你一个有效的 IPv4 地址 address,返回这个 IP 地址的无效化版本。

    暮雨
  • 一致性 Hash 算法

    哈希算法: 就是对一个对象进行哈希获得的散列值。其中,值越分散,哈希的碰撞率也就越低,性能也就越好。

    奕仁
  • Java网络编程--Netty中的责任链

    责任链模式(Chain of Responsibility Pattern)是一种是行为型设计模式,它为请求创建了一个处理对象的链。其链中每一个节点都看作是一个...

    CodingDiray

扫码关注云+社区

领取腾讯云代金券