React设计原理-render阶段
Reconciler
工作的阶段在React内部被称为render
阶段. 类组件的render函数、函数组件本身都在该阶段调用. 根据调度器调度结果不同, render阶段可能开始于performSyncWorkOnRoot
同步更新流程或performConcurrentWorkOnRoot
并发更新流程.
// (同步更新流程) 执行该方法
function workLoopSync(){
// workInProgress(wip)
// 代表生成fiber tree工作已经进行到的wip fiberNode
while(workInProgress !== null){
// 创建下一个fiberNode并赋值给wip, 将wip与已创建的fiberNode连接构成fiberTree
performUnitOfWork(workInProgress)
}
}
// (并发更新流程) 执行该方法
function workLoopConcurrent(){
// 是否可中断
while(workInProgress !== null && !shouldYield()){
performUnitOfWork(workInProgress)
}
}
流程概览
Fiber Reconciler
通过遍历的方式实现可中断的递归, 因此performUnitOfWork
的工作可以分为“递”和“归”.
“递”阶段从HostRootFiber
开始向下以DFS的方式遍历执行beginWork
方法, 该方法根据传入的fiberNode
创建下一级fiberNode
.
“归”阶段调用completeWork
方法处理fiberNode
. 当某个fiberNode
执行完此方法后, 会进入其兄弟fiberNode
(如果存在)的“递”阶段, 或者进入父fiberNode
的“归”阶段.
“递”阶段和“归”阶段会交错执行直至HostRootFiber
的“归”阶段.
function performUnitOfWork(fiberNode){
// ...执行beginWork工作
if(fiberNode.child){
performUnitOfWork(fiberNode.child)
}
// ...执行completeWork工作
if(fiberNode.sibling){
performUnitOfWork(fiberNode.sibling)
}
}
beginWork
大致流程:
function beginWork(current, workInProgress, renderLanes){
if(current !== null){
// 首先根据current fiberNode是否存在判断当前属于`mount`还是`update`流程
// 如果是更新流程, 则 wip fiberNode 存在对应的 current fiberNode
// 如果本次更新不影响 fiberNode.child 则可以复用对应的 current fiberNode
// update时判断是否可复用
// ...
} else {
// ...
}
// 如果无法复用 current fiberNode 则mount和update流程大体一致
// 根据tag进入不同类型元素的处理分支
switch(workInProgress.tag){
case IndeterminateComponent:
// FC mount时进入...
case LazyComponent:
// ...
case FunctionComponent:
// FC update时进入...
case ClassComponent:
// ...
case HostRoot:
// ...
case HostComponent:
// 原生element类型 eq. span div ...
case HostText:
// 文本元素类型 ...
}
}
// Reconciler模块的核心部分
// 如果常见类型(FunctionComponent、ClassComponent、HostComponent)没有命中优化策略, 最终进入此方法
function reconcileChildren(current, workInProgress, nextChildren, renderLanes){
// mount
if(current === null){
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
// update
} else {
workInProgress.child = reconcileChildFibers(workInProgress, current.children, nextChildren, renderLanes)
}
}
// 区别只是传参不同
var reconcileChildFibers = ChildReconcile(true)
var mountChildFibers = ChildReconcile(false)
function ChildReconcile(shouldTrackSideEffects){
// ... shouldTrackSideEffects表示 是否追踪副作用, 即是否标记flags
}
执行mountChildFibers
时由于shouldTrackSideEffects = false
, 即使该fiberNode执行了placeChild
(前者内部方法, 标记“要插入UI”的fiberNode)也不会被标记Placement, 因此在Renderer
中也不会对“该fiberNode对应的DOM元素”执行“插入页面”的操作. reconcileChildFibers
中会标记的flags主要与元素位置有关(标记ChildDeletion代表删除、标记Placement代表插入或移动).
React中的位运算
flags的本质是二进制数的位运算.
// 未处于React上下文
const NoContext = 0b0000;
// 处于Batched上下文 batchedUpdates是一种性能优化手段
const BatchedContext = 0b0001;
// 处于Render阶段
const RenderContext = 0b0010;
// 处于Commit阶段
const CommitContext = 0b0100;
// 1. 当执行流程进入render阶段, 使用按位或标记进入对应上下文
let executionContext = NoContext
executionContext |= RenderContext
// 2. 此时可以结合按位与和NoContext来判断 是否处于某一上下文
(executionContext & RenderContext) !== NoContext // true
(executionContext & CommitContext) !== NoContext // false
// 3. 离开RenderContext上下文后, 结合按位与、按位非移除标记
executionContext &= ~RenderContext
(executionContext & RenderContext) !== NoContext // false
completeWork
根据wip.tag
区分对待, completeWork
流程大致包括:
- 创建或者标记元素更新
- flags冒泡
beginWork
的reconcileChildFibers
方法用来“标记fiberNode的插入、删除、移动”. completeWork
的步骤(1)会完成“更新的标记”(该fiberNode在本次更新中的增、删、改操作均已标记完成). 至此Reconciler
中“标记flags”相关工作基本完成.
flags冒泡
高效地收集散落在Wip Fiber Tree
各处的“被标记的fiberNode”.
// 每个fiberNode将子孙中标记的flags向上冒泡一层
let subtreeFlags = NoFlags
// 收集子fiberNode的子孙fiberNode中标记的flags
subtreeFlags |= child.subtreeFlags
// 收集子fiberNode标记的flags
subtreeFlags |= child.flags
// 附加在当前fiberNode的subtreeFlags上
completedWork.subtreeFlags |= subtreeFlags
当HostRootFiber
完成completeWork
, 整棵Wip Fiber Tree
中所有“被标记的flags”都在HostRootFiber.subtreeFlags
中定义. 在Renderer
中, 通过任意一级fiberNode.subtreeFlags
都可以快速确定“该fiberNode所在子树是否存在副作用需要执行”.
mount概览
- 根据
wip.tag
进入不同处理分支 - 通过
current !== null
判断是否处于mount/update流程 - 对于
HostComponent
, 通过createInstance
创建fiberNode对应的dom元素 - 执行
appendAllChildren
方法将下一层dom元素插入上面创建的dom元素中// 与flags冒泡一样都是处理某个元素的下一级元素 // 前者处理下一级DOM元素类型, 后者处理下一级flags function appendAllChildren(parent, workInProgress, /* ...args */){ let node = workInProgress.child // 从当前`fiberNode`向下遍历 while(node !== null) { // 1) 将第一层DOM元素类型(HostComponent、HostText)通过`appendChildren`方法插入`parent`末尾 if(node.tag === HostComponent || node.tag === HostText) { appendInitialChild(parent, node.stateNode) } else if (node.child !== null)( node.child.return = node node = node.child continue; ) // 终止情况1: 遍历到parent对应FiberNode if(node === workInProgress) return // 如果没有兄弟则向父级FiberNode遍历 while(node.sibling === null) { // 终止情况2: 回到最初执行步骤(1)所在层 if(node.return === null || node.return === workInProgress) return node = node.return } // 2) 对兄弟FiberNode执行步骤(1) node.sibling.return = node.return node = node.sibling } }
- 执行
finalizeInitialChildren
完成属性的初始化, 包括styles
、innerHTML
、文本类型children、不会在DOM中冒泡的事件(cancel、close、load、scroll、invalid、toggle等)及其他属性(setValueForProperty
)等. - 执行
bubbleProperties
完成flags冒泡.
由于appendAllChildren
存在, 当completeWork
执行到HostRootFiber
时已经形成一颗完整的离屏DOM Tree. 由于HostRootFiber
存在alternate
(HostRootFiber.current!==null), 因此HostRootFiber
在beginWork
时会进入reconcileChildFibers
而不是mountChildFibers
.
update概览
mount
流程完成了属性的初始化, update
流程将完成标记属性的更新. 其主要逻辑在diffProperties
中包括两次遍历:
- 第一次遍历, 标记删除“更新前有, 更新后没有”的属性
- 第一次遍历, 标记更新“update流程前后发生改变”的属性
所有变化属性的key、value会保存在fiberNode.updateQueue
中, 同时该fiberNode
会被标记updateworkInProgess.flags |= Update
.
ReactDOM Renderer
仿照react-dom
实现一个ReactDOM Renderer.
// 初始化 ReactReconciler
// ReactReconciler 由 react-reconciler 包提供
// hostConfig是宿主环境的配置项, 提供一系列API. 包括初始化环境信息、创建DOM Node、关键逻辑判断、DOM操作等
const ReactReconcilerInst = ReactReconciler(hostConfig);
export default {
render(reactElement, domElement, callback){
if(!document._rootContainer) {
document._rootContainer = ReactReconcilerInst.createContainer(domElement, false);
}
return ReactReconcilerInst.updateContainer(reactElement, document._rootContainer, null, callback)
}
}
总结
Reconciler
采用DFS的顺序构建Wip Fiber Tree
. 整个过程分为“递”、“归”两个阶段, 分别对应beginWork
与completeWork
方法.
beginWork
根据当前fiberNode构建下一级fiberNode, 在update时标记Placement(新增、移动)、ChildDeletion(删除).completeWork
在mount时会构建DOM Tree, 初始化属性, 在update时标记Update, 最终执行flags冒泡.
当最终HostRootFiber
完成completeWork
时, Reconciler
的工作流程结束. 此时得到了:
- 代表本次更新的 Wip Fiber Tree
- 被标记的 flags
HostRootFiber
对应的FiberRootNode
会被传递给Renderer进行下一阶段工作.
转载自:https://juejin.cn/post/7226717655906730043