[React更新流程]之Reconciler的beginWork阶段

125 阅读6分钟

我们已经知道了一个节点的解析分成了两个部分beginWorkcompleteWork,而beginWork在遍历节点的过程中采用了先序遍历,当本节点的beginWork过程结束后,会寻找自己的子节点是否存在,存在的话继续重复beginWork的过程,一直到节点的child === null时停止
beginWork最后会返回一个“新”的Fiber节点,这里的“新”并不意味着全新,事实上在整个更新的过程中,react会不停地比对该节点是否可以复用,所以可以得到返回的两个形态

  • 创建过程中通过JSX返回的element创建新的Fiber节点
  • 更新过程中通过Diff算法得到该节点是否可以复用,并标记其改变

在整个reconcile的过程中,我们并不会对DOM节点进行操作,一直到commit阶段才会进行真实的DOM操作,在对比的过程中如果遇到有修改或者新增的操作,我们对该节点打上对应的effectTag,在随后交由completeWork处理

先序遍历

beginWork的遍历过程中,都是采用先序遍历的顺序来处理节点,例如如下的节点


    const App = () => {
    return (<div id="parent">
        <div id="child-billy">
          <div id="grandson-jack"></div>
        </div>
        <div id="child-mary"></div>
    </div>)
    
  }

对应的Fiber树结构
image.png
如果所示的结构中,执行beginWork的顺序为


	如图结构 beginWork对应的顺序如下

  parent -- childBilly -- grandsonJack -- childMary

在遍历beginWork的过程中,会查询该节点是否有子节点,如果有子节点会继续遍历全部子节点,一直到所有的子节点都遍历完,开始completeWork

effectTag

虽然在beginWork的过程中的,我们并不会对有改动的Fiber节点进行Dom操作,但是会对需要更新的节点打上对应的标记,在后期交由completeWork进一步处理,在commit阶段会对将所有的改动合并到真实的Dom树中;而在beginWork阶段打上的标记就是effectTag,你可以点击这里查看所有的effectTag


  // 列举几个常用的effectTag
  1. NoEffect 一般作为effectTag的初始值,或者用于effectTag的比较判断,表示NoWork
  2. Placement 向树中插入新的节点
  3. Update 在树中更新新的节点
  4. Deletion 卸载节点时打上的标记
  5. Passive 副作用关联的标记 常用于useEffect,useLayEffect等hook
  .......

你可以点击这里了解更多关于effectTag的详解

判断能否复用

在解析节点之前并且在更新阶段的话,需要判断节点是否可以复用


  // 判断是否是更新阶段
  current !== null 代表更新阶段 反之代表创建阶段

在更新阶段判断能否服用的代码如下

if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    // 如果oldProps与newProps并不相等
    // 标记全局变量didReceiveUpdate为true 代表这次是会更新的
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
    
      // 这个fiber节点没有任何等待处理的工作,进入begin阶段之前跳出,同时需要做一些记录工作,
      // 主要是把数据推入栈
      // ....设置全局上下文的操作 忽略

      // bailout节点进行复用
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      // 特殊逻辑 先不管...
    }
}

首先我们会定义一个全局变量didReceiveUpdate,在判断进入更新阶段以后,会经历如下几个阶段的判断

  1. 判断新旧props是否相等,如果oldPropsnewProps并不相等,标记全局变量didReceiveUpdatetrue 代表会进入正式更新阶段
  2. 如果新旧props相等,进入下一个阶段,判断该节点是否存在本次更新的优先级,如果不存在进入bailout阶段,在Diff算法中介绍了bailoutOnAlreadyFinishedWork方法,你可以点击这里查看该函数的解析;如果存在更新,进入正式的更新阶段

而根据不同的节点,我们会进入不同的处理方法


    switch (workInProgress.tag) {
    case IndeterminateComponent: {
      // .....
    }
    case LazyComponent: {
      // .....
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      // .....
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
     // .... 省略
  }

根据节点的不同类型来确定是否应该采用什么的方法,因为我们使用函数组件的场景更多,接下来我们通过FunctionComponent来解析接下来的流程

updateFunctionComponent

函数组件在最后进入的是updateFunctionComponent方法


function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
	// 关于传参
  // current workInProgress映射的当前节点
  // workInProgress 经过处理得到正在被处理的节点
  // Component 来源于workInProgress.type 函数组件对应的函数本身为一个function
  // nextProps pendingProps 新的props
  // renderLanes 这次渲染的优先级

  let context;
  // 省略一些逻辑....

  let nextChildren;
  // 获取当前上下文
  prepareToReadContext(workInProgress, renderLanes);
  // 渲染hooK
  nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
  );

  // bailout复用逻辑
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  // React DevTools reads this flag.
  workInProgress.effectTag |= PerformedWork;
  // diff算法核心
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}


首先来看该方法的入参

  • current workInProgress映射的当前节点
  • workInProgress 经过处理得到正在被处理的节点
  • Component 来源于workInProgress.type函数组件对应的函数本身为一个function
  • nextProps pendingProps 新的props
  • renderLanes 本次渲染的优先级

在执行方法里面有个renderWithHooks里,renderWithHooks方法在这里有简单介绍过,它主要处理的功能是执行Component,并处理中间遇到的种种情况


    const Show = () => {
        return <div>展示组件</div>
    }

    // renderWithHooks源码
    let children = Component(props, secondArg);
    // 这里等同于 let children = Show(props, secondArg)
    

如图假设我们有一个Show组件,Componet就指代的Show这个函数,它的执行过程就是运行Show这个函数,在运行过程中,函数组件中的hooks会被一一触发,最后返回一个新的子元素children

关于didReceiveUpdate

我们已经知道didReceiveUpdate来标记是否能够复用的标记,在函数组件Componet运行的过程中,每一个hook处理过程中依然会对这个标志位发起标记

复用逻辑


    // 代表是更新阶段 & 并不需要更新
    // didReceiveUpdate在Component()运行后可能会改变
    if (current !== null && !didReceiveUpdate) {
        // 复用hook
        bailoutHooks(current, workInProgress, renderLanes);
        // 复用节点
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
     }

上面说到,在Componet运行的过程中,didReceiveUpdate标志位会被修改,所以运行后我们判断能否服用的逻辑便是

  • 当前是否是更新阶段 & didReceiveUpdate === false

如果并不要复用hook并且复用节点(进入到这个阶段,代表这个节点时完全可以复用的,其剩余的子元素也一并复用),在Diff算法中对bailoutOnAlreadyFinishedWork做了介绍,这是一个非常常见的方法,你可以点击这里查看

进入Diff

如果并不能复用,我们需要进入到beginWork最重要的阶段——通过Diff计算出新的children与原有节点的差异,并得出一个全新的节点,将该节点的子元素交由beginWork继续处理,你可以在这里看到Diff算法的解析