likes
comments
collection
share

梳理React架构: render阶段

作者站长头像
站长
· 阅读数 51

最近看了很多有关react底层原理的文章。梳理一下。如有问题,多谢指出。参考文章:卡颂:React技术揭秘全栈潇晨:react源码解析8.render阶段

JSX

首先从jsx说起,jsx经babel编译为React.createElement,在该函数中处理config中的参数,将参数挂到props上。对于children,把第二个参数之后的参数取出来,然后判断长度是否大于一。大于一的话就代表有多个 children,这时候 props.children 会是一个数组,否则的话只是一个对象。对于传入的defaultProps,如果此键存在于defaultProps且props上该键没有值,就对props里的该键赋值。返回ReactElement函数,最终返回虚拟DOM。

虚拟DOM

虚拟DOM是描述节点信息的一个对象,但是不包含优先级、state、以及commit时用到的更新标记等。这些信息都储存在fiber上。

从虚拟DOM到Fiber

得到的虚拟DOM作为参数传入到ReactDOM.render()中。render中调用返回legacyRenderSubtreeIntoContainer执行方法。该方法中主要做了:生成root(FiberRootNode),生成fiberNode(就是RootFiber吧)(mount),处理事件监听, 调用updateContainer方法(update)。并将root.current指向RootFiber,RootFiber.stateNode=root。梳理React架构: render阶段fiberRoot是react应用的根,使用current来指向当前页面上的fiber树。rootFiber是挂载的真实DOM的fiber(document.getElementById('root'))。同时,在createFiberRoot中还会初始化updateQueue。 传入fiber,初始化一个queue,挂到fiber.updateQueue上,最后返回root。梳理React架构: render阶段

Fiber架构

mount时,通过深度优先遍历虚拟DOM树构建链表结构的fiber树。通过child, return,sibling属性连接链表。此时的fiber树成为workInProgress Fiber树。构建时,会尝试复用current树中已有的fiber节点的属性。但是mount的时候只存在rootFiber 的current fiber。梳理React架构: render阶段构建完成后将current指向构建好的workInprogress fiber,此时变成current树。update时,就是以current fiber树为基础,构建workInprogress fiber,再改变指向。这种内存中构建并直接替换的技术就是双缓存,实现DOM对象的快速更新。

render阶段

负责构建fiber和生成effectList。由react的入口模式来决定是否调用shouldYield。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法  异步更新
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork中workInProgress fiber和会和已经创建的Fiber连接起来形成Fiber树。这个过程类似深度优先遍历(可中断),包含递和归两个部分。伪代码:

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

递阶段

从rootFiber开始,每个被遍历到的节点调用beginWork方法,该方法根据传入的fiber节点创建子fiber节点,并将这两个节点连接。当遍历到叶子节点时进入归阶段。

beginWork

创建或复用子fiber节点。使用current === null 来判断mount还是update:

  • mount: 根据fiber.tag构建不同的子fiber节点(reconcileChildren);
  • update:满足条件时,可以复用current fiber: oldProps === newProps && workInProgress.type === current.type 属性和fiber的type不变 !includesSomeLane(renderLanes, updateLanes) 更新的优先级是否足够reconcileChildren是协调阶段的核心部分,mount时创建新的子fiber节点;update时,进行diff比较,为生成的fiber节点带上effecTag属性,打标记。effectTag使用二进制表示,方便使用位操作为effectTag赋值多个effect。
// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

注意: DOM插入满足的条件:

  1. fiber.stateNode!==null 即fiber存在真实dom,真实dom保存在stateNode上
  2. (fiber.effectTag & Placement) !== 0 fiber存在Placement的effectTag

归阶段

completeWork

主要是处理fiber的props,创建dom,创建effectList。mount时,主要包含三个:

  1. 为fiber节点生成对应的DOM节点
  2. 将子孙DOM节点插入刚生成的DOM节点中
  3. 处理propsupdate时,包含以下:
  4. 处理props(包括onClick、style、children ...)
  5. 将处理好的props赋值给updatePayload
  6. 被处理完的props会被赋值给workInProgress.updateQueue其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。

effectList

至此render阶段的绝大部分工作就完成了。还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。这样,在commit阶段只需要遍历effectList就能执行所有effect了。

render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

思考:

  1. mount时,fiber.stateNode === null,且不会为fiber节点赋值effectTag,那么首屏渲染是如何完成的?fiber.stateNode会在completeWork中创建。如果在mount时赋值effectTag,则可知整颗fiber树所有节点都会有Placement effectTag,则commit阶段执行DOM时每个节点都要进行一次插入,大量的DOM操作极其低效。所以,react在mount时,只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
  2. mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?原因就在于completeWork中的appendAllChildren方法。由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。