React解析--render阶段

429 阅读6分钟

React的render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。而这两个方法分别会调用workLoopSync或者workLoopConcurrent

//ReactFiberWorkLoop.old.js
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

这两函数的区别是判断条件是否存在shouldYield的执行,如果浏览器没有足够的时间,那么会终止while循环,也不会执行后面的performUnitOfWork函数,自然也不会执行后面的render阶段和commit阶段,这部分属于scheduler的知识。

  • workInProgress:新创建的workInProgress fiber

  • performUnitOfWork:workInProgress fiber和会和已经创建的Fiber连接起来形成Fiber树。这个过程类似深度优先遍历,我们暂且称它们为‘捕获阶段’和‘冒泡阶段’,或者也可以说是'递'和'归'的阶段。伪代码执行的过程大概如下:

    function performUnitOfWork(fiber) {
      if (fiber.child) {
        performUnitOfWork(fiber.child);//beginWork
      }
      
      if (fiber.sibling) {
        performUnitOfWork(fiber.sibling);//completeWork
      }
    }
    

render阶段的执行流程

render阶段整体执行流程,可以先大致看一眼,等看完下面所有的方法再回来看这个图就很好理解了。

image-20220118172710068

  • 捕获阶段 从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。
  • 冒泡阶段 在捕获阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上‘冒泡’到父节点执行completeWork,直到rootFiber。

举个例子

function App() {
  return (
    <div>
     	你好啊
      <span>李银河</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

对应的fiber树结构 image-20220118174646337

renger阶段会依次执行

1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "你好啊" Fiber beginWork
5. "你好啊" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

之所以没有 “李银河” Fiber 的 beginWork/completeWork,是因为作为一种性能优化手段,针对只有单一文本子节点的Fiber,React会特殊处理。

beginwork

beginWork主要的工作是创建或复用子fiber节点,我们先来看看源码:

function beginWork(
  current: Fiber | null, //当前存在于dom树中对应的Fiber树
  workInProgress: Fiber, //正在构建的Fiber树
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
    // 1.update时 满足条件即可复用current fiber进入bailoutOnAlreadyFinishedWork函数
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // 如果实现因热重载而改变,则强制重新渲染
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 如果道具或上下文发生变化,请将fiber标记为已执行工作。如果以后确定道具相等,则可能未设置(memo).
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;

      //2.根据tag来创建不同的fiber 最后进入reconcileChildren函数
      switch (workInProgress.tag) {
       ···
      }
      // 复用current
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
}
 // 2.根据tag来创建不同的fiber 最后进入reconcileChildren函数
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...
    case LazyComponent: 
      // ...
    case FunctionComponent: 
      // ...
    case ClassComponent: 
      // ...
    case HostRoot:
      // ...
    case HostComponent:
      // ...
    case HostText:
      // ...
}

从React的架构我们可以知道Fiber双缓存机制,我们首次渲染current是为null的,所以我们可以通过判断current是否等于null来判断是mount阶段还是update阶段。

  • mount:根据fiber.tag进入不同fiber的创建函数,最后都会调用到reconcileChildren创建子Fiber
  • update:在构建workInProgress的时候,当满足条件时,会复用current Fiber来进行优化,也就是进入bailoutOnAlreadyFinishedWork的逻辑,能复用didReceiveUpdate变量是false,复用的条件是
    1. oldProps === newProps && workInProgress.type === current.type 属性和fiber的type不变
    2. !includesSomeLane(renderLanes, updateLanes) 更新的优先级是否足够

reconcileChildren/mountChildFibers

创建子fiber的过程会进入reconcileChildren,该函数的作用是为workInProgress fiber节点生成它的child fiber即 workInProgress.child。然后继续深度优先遍历它的子节点执行相同的操作

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    // mount时
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // update时
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

reconcileChildren这个方法会区分mount和update两种情况,分别进入mountChildFibers和reconcileChildFibers,mountChildFibers和reconcileChildFibers最终其实就是ChildReconciler传递不同的参数返回的函数,这个参数用来表示是否追踪副作用

值得一提的是,mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。

在ChildReconciler中用shouldTrackSideEffects来判断是否为对应的节点打上effectTag,例如如果一个节点需要进行插入操作,需要满足两个条件:

  1. fiber.stateNode!==null 即fiber存在真实dom,真实dom保存在stateNode上
  2. (fiber.effectTag & Placement) !== 0 fiber存在Placement的effectTag
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    // 是否最终副作用
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // oldIndex < lastPlacedIndex 将结点插入到后面
        newFiber.flags = Placement;
        return lastPlacedIndex;
      } else {
        //不需要移动
        return oldIndex;
      }
    } else {
      // 这是新增插入
      newFiber.flags = Placement;
      return lastPlacedIndex;
    }
  }
}

在为Fiber打上effectTag之后在commit阶段会被执行对应dom的增删改,而且在reconcileChildren的时候,rootFiber是存在alternate的,即rootFiber存在对应的current Fiber,所以rootFiber会走reconcileChildFibers的逻辑,所以shouldTrackSideEffects等于true会追踪副作用,最后为rootFiber打上Placement的effectTag,然后将dom一次性插入,提高性能。

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

bailoutOnAlreadyFinishedWork

在beginwork的update阶段如果节点可以复用,那么就会进入这个方法我们先看看这个方法做了什么事情

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }
  if (enableProfilerTimer) {
    stopProfilerTimerIfRunning(workInProgress);
  }
  markSkippedUpdateLanes(workInProgress.lanes);
  // 判断优先级,优先级足够则进入cloneChildFibers否则返回null
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    return null;
  } else {
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

其实如果节点可以复用并且优先级足够,那么就会进入cloneChildFibers否则返回null。

completeWork

completeWork主要工作是处理fiber的props、创建dom、创建effectList。类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
	// 根据workInProgress.tag进入不同逻辑,这里我们关注HostComponent,
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;

      if (current !== null && workInProgress.stateNode != null) {
        // update时
       updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
      } else {
        // mount时
        const currentHostContext = getHostContext();
        // 创建fiber对应的dom节点
        const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        // 将后代dom节点插入刚创建的dom里
        appendAllChildren(instance, workInProgress, false, false);
        // dom节点赋值给fiber.stateNode
        workInProgress.stateNode = instance;
        // 处理props和updateHostComponent类似
        if (
          finalizeInitialChildren(
            instance,
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
          )
        ) {
          markUpdate(workInProgress);
        }
     }
      return null;
    }
  // ...省略
 }

其实函数completeWork主要做了一下几件事:

  1. 根据workInProgress.tag进入不同函数,我们以HostComponent举例
  2. update时(除了判断current===null外还需要判断workInProgress.stateNode===null),调用updateHostComponent处理props(包括onClick、style、children ...),并将处理好的props赋值给updatePayload,最后会保存在workInProgress.updateQueue上
  3. mount时 调用createInstance创建dom,将后代dom节点插入刚创建的dom中,调用finalizeInitialChildren处理props(和updateHostComponent处理的逻辑类似)

在beginwork的mount时,rootFiber存在对应的current,他会执行mountChildFibers打上Placement的effectTag,在冒泡阶段也就是执行completeWork时,我们将子孙节点通过appendAllChildren挂载到新创建的dom节点上,最后就可以一次性将内存中的节点用dom原生方法反应到真实dom中。

在beginWork 中我们知道有的节点被打上了effectTag的标记,有的没有,而在commit阶段时要遍历所有包含effectTag的Fiber来执行对应的增删改,那我们还需要从Fiber树中找到这些带effectTag的节点嘛?答案是不需要的,这里是以空间换时间,在执行completeWork的时候遇到了带effectTag的节点,会将这个节点加入一个叫effectList的单链表中,所以在commit阶段只要遍历effectList就可以了(rootFiber.firstEffect.nextEffect就可以访问带effectTag的Fiber了)

                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

effectList的指针操作发生在completeUnitOfWork函数中,例如我们的应用是这样的:

<div id="1">
  <div id="4"/>
  <div id="2">
    <div id="3"/>
  </div>
</div>

最终形成的EffectList为

firstEffect => div4
lastEffect  => div1

因为Fiber树的构建深度优先,所有div4先完成completeWork,构建firstEffect。EffectList遍历是从firstEffect开始,通过每一个节点的nextEffect找到下一个节点。

firstEffect => div4
div4.nextEffect => div3
div3.nextEffect => div2
div2.nextEffect => div1

形成环状链表的时候会从触发更新的节点向上合并effectList直到rootFiber,这一过程发生在completeUnitOfWork函数中,整个函数的工作就是:

  • 完成该 fiber 节点的构建
  • 将该 fiber 的 effectList 更新到其父 Fiber 节点上
  • 如果当前节点有 effectTag,则将其加入 effectList
  • 如果有 sibling,移动到 next sibling 进行同样的操作
  • 没有 sibling 则返回父 fiber
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    	//...
      if (
        returnFiber !== null &&
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        if (returnFiber.firstEffect === null) {
          //父节点的effectList头指针指向completedWork的effectList头指针
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            //父节点的effectList头尾指针指向completedWork的effectList头指针
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          //父节点头的effectList尾指针指向completedWork的effectList尾指针
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            //completedWork本身追加到returnFiber的effectList结尾
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            //returnFiber的effectList头节点指向completedWork
            returnFiber.firstEffect = completedWork;
          }
          //returnFiber的effectList尾节点指向completedWork
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      //...
      if (returnFiber !== null) {
        returnFiber.firstEffect = returnFiber.lastEffect = null;//重制effectList
        returnFiber.flags |= Incomplete;
      }
    }
  } while (completedWork !== null);
	//...
}

总结:

EffectList不是全局变量,只是在Fiber树创建过程中,一层层向上收集有effect的Fiber节点,最终的root节点就会收集到所有有effect到Fiber节点,我们就把这条包含effect节点的链表叫做EffectList

由于收集的过程是深度优先,子级会先被收集,所以遍历的时候也会先操作子级,所以如果有面试官问子级和父级的生命周期或者useEffect谁先执行,就很清楚的知道会先执行子级操作了

结尾

至此,render阶段全部工作完成,现在回头再来看看整个过程是不是就很清晰了,在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

// render阶段开始,这是不通过Scheduler调度器的同步任务的入口点
function performSyncWorkOnRoot(root) {
	commitRoot(root);
}