render阶段

概述#
render 阶段
React 应用程序首次渲染时构建
workInProgress树的过程也是将React元素树转化为Fiber树的过程,workInProgress树构建完成后会在该「树」上面完成更新计算、调用生命周期函数以及收集副作用列表等工作。应用程序渲染时在
render阶段的一个重要工作就是构建workInProgress树,构建workInProgress树的过程中React也会完成更新计算、调用生命周期函数以及收集副作用列表等工作React的更新任务主要是调用
workLoop的工作循环去构建workInProgress树, 构建过程分为两个阶段:向下遍历和向上回溯,向下和向上的过程中会对途径的每个节点进行beginWork和completeWork
beginWork
- 是处理节点更新的入口,它会依据
fiber节点的类型去调用不同的处理函数 - React对
current树的每个节点进行beginWork操作,进入beginWork后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和diff之后生成新的fiber, 然后在新的fiber上标记effectTag,最后return它的子节点,以便继续针对子节点进行beginWork。若它没有子节点,则返回null,这样说明这个节点是末端节点, 可以进行向上回溯,进入completeWork阶段。 - 计算新状态
updateupdateQueueprocessUpdateQueue- 准备阶段 - 整理
updateQueue上次遗留的 本次新增的 - 处理阶段 - 循环处理上一步整理好的更新队列 优先级
- 完成阶段 - 主要是做一些赋值和优先级标记的工作
- 准备阶段 - 整理
Diff算法- 经过状态计算后 已经获取到了新的
fiber在render之前 需要diff操作 打上effectTag(可以标识这个fiber发生了怎样的变化,例如:新增Placement,这些被打上flag的fiber会在complete阶段被收集起来,形成一个effectList链表,只包含这些需要操作的fiber,最后在commit阶段被更新掉) diff的主体是oldFiber (current.child)和new React Element (nextChildren)diff的原则 直接销毁重建 以及 依据key tag复用- 在
diff过后,workInProgress节点的beginWork节点就完成了。接下来会进入completeWork阶段
- 经过状态计算后 已经获取到了新的
completeWork
- 进入
completeWork后的workInProgress节点都是经过diff算法调和过的,节点的fiber的形态已经基本确定了,但对于原生DOM组件和文本节点的fiber来说,对应的DOM节点并未变化 - 这个阶段主要就是两个工作
- 构建或更新DOM节点
- 自下而上收集
effectList,最终收集到root上
- 依据
fiber的tag做不同处理。DOM节点的更新实则是属性的更新
收集好的副作用列表会在 commit 阶段统一映射到屏幕上
- 创建
wIP树 - 计算新状态
- 进行
diff标记effectTag - 构建或更新DOM节点
- 收集形成
effect List
beginWork#
从初始化
workInProgress对象到形成workInProgress树,应用程序会执行多次循环,每一次循环都是在解析Fiber结点并返回下一个要解析的结点。这 个过程中会使用重要的「协调」算法,对结点进行diff操作
构建 wIP 树#
此阶段
React将元素逐个转换为对应类型的Fiber结点,最终形成workInProgress树
应用程序首次渲染过程刚进入 render 阶段时 fiberRoot 对象上面只有 current 树,此时的 current 树只有一个类型为 HostRoot 的 Fiber 结点,该 Fiber 结点的更新队列中只有一个更新对象,内容是应用程序的根组件元素。紧接着 React 要做的就是初始化 workInProgress 对象。

初始化 wIP 对象#
renderRoot调用createWorkInProgress为workInProgress对象赋初始值- 函数调用后
wIP对象上有了第一个Fiber结点,结点的tag为HostRoot是整个wIP树的根节点

完善 wIP 对象#
- 要想把组件元素描述的页面结构映射到屏幕上必须先将元素转换为
Fiber结点 - 完善
wIP对象的过程就是 将React元素树转化为Fiber树的过程
工作循环#
React 通过工作循环来构建
workInProgress两个关键环节 解析工作单元 完成工作单元
循环解析工作单元#
- 工作循环主要是执行
workLoop函数来循环解析工作单元。每次循环解析的工作单元就是上一次Fiber结点的child。 - 第一次循环解析的是
HostRoot类型的结点,该结点也就是所有结点的祖先 - 工作单元具体的解析逻辑在
performUnitOfWork中调用
解析工作单元#
在 beginWork 函数内部通过匹配 Fiber 结点的 tag 值调用 updateHostRoot、updateClassComponent、updateHostComponent、updateHostText 等函数,分别解析 HostRoot、ClassComponent、HostComponent、HostText等类型的Fiber 结点。
完成工作单元#
即completeWork
解析不同类型Fiber结点#
应用程序首次渲染时,将根组件元素转换为多个层级的 Fiber 结点过程中,第一步要做的就是解析 wIP 树的根结点 HostRoot

HostRoot 类型#
updateHostRoot -> processUpdateQueue- 解析
HostRoot类型结点的关键是处理更新队列processUpdateQueue获取根组件元素element,element就是要传入到协调算法中进行解析的nextChildren
function updateHostRoot(current$$1, workInProgress, renderExpirationTime) { pushHostRootContext(workInProgress); var updateQueue = workInProgress.updateQueue;
var nextProps = workInProgress.pendingProps; var prevState = workInProgress.memoizedState; var prevChildren = prevState !== null ? prevState.element : null; processUpdateQueue(workInProgress, updateQueue, nextProps, null, renderExpirationTime); var nextState = workInProgress.memoizedState; // Caution: React DevTools currently depends on this property // being called "element". var nextChildren = nextState.element; if (nextChildren === prevChildren) { // If the state is the same as before, that's a bailout because we had // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime); } var root = workInProgress.stateNode; if ((current$$1 === null || current$$1.child === null) && root.hydrate && enterHydrationState(workInProgress)) { // If we don't have any current children this might be the first pass. // We always try to hydrate. If this isn't a hydration pass there won't // be any children to hydrate which is effectively the same thing as // not hydrating.
// This is a bit of a hack. We track the host root as a placement to // know that we're currently in a mounting state. That way isMounted // works as expected. We must reset this before committing. // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag |= Placement;
// Ensure that children mount into this root without tracking // side-effects. This ensures that we don't store Placement effects on // nodes that will be hydrated. workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime); } else { // Otherwise reset hydration state in case we aborted and resumed another // root. reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime); resetHydrationState(); } return workInProgress.child;}ClassComponent 类型#
- 应用程序首次渲染时解析
HostRoot类型结点完成后返回的一般是ClassComponent类型的结点,这种类型的Fiber结点解析过程中是要进行组件实例化的 - 解析
ClassComponent类型的结点时,先从当前Fiber结点的stateNode属性上面取组件实例instance,如果instance的值为null则说明是首次渲染,组件还没有被实例化。React 使用constructClassInstance函数完成组件实例化,同时为组件实例添加更新器(然后就可以调用setState - 组件实例生成后,它的核心作用之一就是在
finishClassComponent函数内部执行instance.render()获取要传入到协调算法中进行解析的nextChildren
function updateClassComponent(current$$1, workInProgress, Component, nextProps, renderExpirationTime) { // ...
var instance = workInProgress.stateNode; var shouldUpdate = void 0; if (instance === null) { if (current$$1 !== null) { // An class component without an instance only mounts if it suspended // inside a non- concurrent tree, in an inconsistent state. We want to // tree it like a new mount, even though an empty version of it already // committed. Disconnect the alternate pointers. current$$1.alternate = null; workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect workInProgress.effectTag |= Placement; } // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, Component, nextProps, renderExpirationTime); mountClassInstance(workInProgress, Component, nextProps, renderExpirationTime); shouldUpdate = true; } else if (current$$1 === null) { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderExpirationTime); } else { shouldUpdate = updateClassInstance(current$$1, workInProgress, Component, nextProps, renderExpirationTime); } var nextUnitOfWork = finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime);
return nextUnitOfWork;}
function finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime) { // ... return workInProgress.child;}HostComponent 类型#
- 解析
ClassComponent类型结点完成后返回的一般是HostComponent类型tag的结点,这种结点的具体类型elementType对应的是真实的DOM标签 (如div span ... - 解析
HostComponent类型结点,可从当前结点对应的元素props中获取要传入到协调算法中进行解析的nextChildren - 这里要强调一下,无论是解析 HostRoot 类型的结点,还是解析
HostComponent类型的结点,这个过程中获取的nextChildren均为React 元素。在执行协调算法的过程中,React将该元素转换成对应的Fiber结点并返回。
HostText 类型#
- 当解析到元素的叶子元素时,此时对应的
Fiber结点类型为HostText。React认为文本类型的Fiber结点已经是终点,不会再有孩子结点,直接返回null

在工作循环的过程中,React 总是先从外部根节点开始解析,当解析到叶子结点时就要开始完成工作单元。完成工作单元时如果有兄弟结点时则需要继续解析兄弟结点,然后完成兄弟结点后会逐层向上完成工作单元。
工作单元解析执行到 workInProgress 树的叶子结点时,会完成当前工作单元。完成工作单元的主要工作是收集副作用,同时处理 HostComponent 类型的结点,创建对应的 DOM 元素并将他们 append 到父结点上
completeWork#
在 Diff 之后,workInProgress节点就会进入complete阶段
这个时候拿到的workInProgress节点都是经过diff算法调和过的,也就意味着对于某个节点来说它fiber的形态已经基本确定了,但除此之外还有两点:
- 目前只有
fiber形态变了,对于原生DOM组件HostComponent和文本节点HostText的fiber来说,对应的DOM节点fiber.stateNode并未变化 - 经过
Diff生成的新的workInProgress节点持有了flag即effectTag
基于这两个特点,completeWork的工作主要有:
- 构建或更新DOM节点 (为
HostComponent类型的结点创建DOM元素实例)- 构建过程中,会自下而上将子节点的第一层第一层插入到当前节点。
- 更新过程中,会计算DOM节点的属性,一旦属性需要更新,会为DOM节点对应的
workInProgress节点标记Update的effectTag
- 自下而上收集副作用
effectList,最终收集到root上 - 错误处理

DOM节点的创建插入#
完成DOM树的创建
DOM的插入并不是将当前DOM插入它的父节点,而是将当前这个DOM节点的第一层子节点插入到它自己的下面
- 总是优先看本身可否插入,再往下找,之后才是找
sibling节点
由于fiber树和dom树的差异,每个fiber节点不一定对应一个dom节点,但一个dom节点一定对应一个fiber节点
由于一个原生DOM组件的子组件有可能是类组件或函数组件,所以会优先检查自身,发现自己不是原生DOM组件,不能被插入到父级fiber节点对应的DOM中,所以要往下找,直到找到原生DOM组件,执行插入,最后再从这一层找同级的fiber节点,同级节点也会执行先自检,再检查下级,再检查下级的同级的操作。
节点的插入也是深度优先。值得注意的是,这一整个插入的流程并没有真的将DOM插入到真实的页面上,它只是在操作fiber上的stateNode。真实的插入DOM操作发生在commit阶段。
1 App | | 2 div / / 3 <List/>--->span / / 4 p ----> 'text node' / / 5 h1
div / | \ / | \ p 'text' span / / h1HostComponent 和HostText类型的 Fiber 结点是有对应的真实 DOM 元素,因此在完成工作单元阶段 React要为它们创建自己的 DOM 实例
在完成工作单元的过程中分为了两种情况,分别是首次渲染和更新渲染。
- 应用程序首次渲染时:
React为HostComponent和HostText类型的Fiber结点创建对应的DOM实例并将其赋值到结点上的stateNode属性上 - 应用程序更新渲染时:
React不需要为它们重新创建DOM实例,而是把新的值更新到DOM元素中
updateHostComponent 函数和 updateHostText 函数分别用于更新 DOM 元素
DOM属性的处理#
属性的创建
主要就是调用setInitialDOMProperties将属性直接设置进DOM节点(事件在这个阶段绑定)
属性的更新
以HostComponent为例,会调用updateHostComponent函数对DOM节点属性进行更新
- 主要就是计算新的属性,并挂载到
workInProgress节点的updateQueue中 - 会调用
diffProperties对比lastProps和nextProps,计算出updatePayload
lastProps{ "className": "test", "title": "更新前的标题", "style": { "color": "red", "fontSize": 18}, "props": "自定义旧属性", "children": "测试div的Props变化", "onClick": () => {...}}
nextProps{ "className": "test", "title": "更新后的标题", "style": { "color":"blue", "fontSize":18 }, "children": "测试div的Props变化", "onClick": () => {...}} // index为偶数的是key,为奇数的是value[ "props", null, "title", "更新后的标题", "style", {"color":"blue"}]DOM节点属性的diff为workInProgress节点挂载了带有新属性的updateQueue,一旦节点的updateQueue不为空,它就会被标记上Update的effectTag,commit阶段会处理updateQueue。
effectTag 收集#
经过beginWork和上面对于DOM的操作,有变化的workInProgress节点已经被打上了effectTag
在收集副作用的过程中主要有两种情况
- 第一种情况就是将子树上面的副作用列表连到父结点上面
- 第二种情况就是如果当前结点也有副作用标识,则将当前结点也连接到父结点的副作用列表中
事实上,应用程序首次渲染时的副作用列表就是整个 workInProgress 树,因为整个 workInProgress 树的结点中携带的内容都需要更新到屏幕,而在应用程序更新渲染时副作用列表将会是 workInProgress 树的子集
每个workInProgress节点都有一个firstEffect和lastEffect,是一个单向链表,来表示它自身以及它的子节点上所有持有effectTag的workInProgress节点。completeWork阶段在向上遍历的过程中也会逐层收集effect链,最终收集到root上,供接下来的commit阶段使用。
在收集好的副作用列表中每个 HostComponent 类型 Fiber 结点的 stateNode 属性中存储了当前结点对应的 DOM 实例。那么,React 下一步要做的就是将副作用列表中的所有 DOM 实例更新到屏幕中
completeWork阶段处在beginWork之后,commit之前,起到的是一个承上启下的作用。它接收到的是经过diff后的fiber节点,然后他自己要将DOM节点和effectList都准备好。因为commit阶段是不能被打断的,所以充分准备有利于commit阶段做更少的工作。
一旦workInProgress树的所有节点都完成complete,则说明workInProgress树已经构建完成,所有的更新工作已经做完,接下来这棵树会进入commit阶段