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
阶段。 - 计算新状态
update
updateQueue
processUpdateQueue
- 准备阶段 - 整理
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 / / h1
HostComponent
和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
阶段