摘要
本文聚焦 React 框架 render 阶段的 completeWork 函数,阐述不同 fiber.tag 处理逻辑、mount 和 update 时的 props 处理、DOM 节点操作,还介绍了 effectList 的作用,最后提及渲染阶段结束后的流程走向。
一、整体流程背景
在 React 组件的 render 阶段包含 beginWork 与 completeWork。之前了解到 beginWork 会创建子 Fiber 节点,这部分可能存在 effectTag。现在重点关注 completeWork 的工作内容。
二、completeWork 函数基本结构
completeWork
函数接收current
、workInProgress
和renderLanes
作为参数。它会根据workInProgress.tag
的不同调用不同处理逻辑。
function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; 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: { //...省略 return null; } //...省略
三、重点关注 HostComponent
(一)处理逻辑判断依据
对于HostComponent
(原生 DOM 组件对应的 Fiber 节点),和beginWork
一样,根据current === null
判断是mount
还是update
。并且针对HostComponent
在判断update
时,还需考虑workInProgress.stateNode!= null
(即该 Fiber 节点是否存在对应的 DOM 节点)。
case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current!== null && workInProgress.stateNode!= null) { // update 的情况 //...省略 } else { // mount 的情况 //...省略 } return null; }
(二)update 时的操作
- 主要操作内容
当
update
时,Fiber 节点已存在对应 DOM 节点,无需生成 DOM 节点,主要处理props
。例如注册onClick
、onChange
等回调函数,处理style prop
、DANGEROUSLYSETINNERHTML prop
(dangerouslysetinnerhtml
)、children prop
等。 - updateHostComponent 方法
最主要逻辑是调用
updateHostComponent
方法。在updateHostComponent
内部,被处理完的props
会被赋值给workInProgress.updateQueue
,并最终在commit
阶段渲染到页面上(updatePayload
为数组形式,偶数索引为变化的prop key
,奇数索引为变化的prop value
)。
if (current!== null && workInProgress.stateNode!= null) { // update 的情况 updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance ); }
(三)mount 时的操作
- 主要操作内容
mount
时主要有三个逻辑:为Fiber
节点生成对应的DOM
节点;将子孙DOM
节点插入刚生成的DOM
节点中;进行与update
逻辑中的updateHostComponent
类似的props
处理过程。 - 具体代码实现
// 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; // 与 update 逻辑中的 updateHostComponent 类似的处理 props 的过程 if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext ) ) { markUpdate(workInProgress); }
因为completeWork
属于“归”阶段调用的函数,每次调用appendAllChildren
都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下。当“归”到rootFiber
时,就有一个构建好的离屏 DOM 树。而mount
时只会在rootFiber
存在Placement effectTag
,commit
阶段可通过一次插入 DOM 操作(对应一个Placement effectTag
)将整棵 DOM 树插入页面。
四、effectList 相关
- 作用
commit
阶段需要依据effectList
找到所有有effectTag
的Fiber
节点并执行相应操作。 - 构建过程
为避免
commit
阶段遍历Fiber
树寻找effectTag!== null
的Fiber
节点的低效操作,在completeWork
的上层函数completeUnitOfWork
中,每个执行完completeWork
且存在effectTag
的Fiber
节点会被保存在effectList
单向链表中。effectList
中第一个Fiber
节点保存在fiber.firstEffect
,最后一个元素保存在fiber.lastEffect
。在“归”阶段,所有有effectTag
的Fiber
节点都会被追加在effectList
中,最终形成以rootFiber.firstEffect
为起点的单向链表。
五、流程结尾
渲染阶段全部完成后,在performSyncWorkOnRoot
函数中fiberRootNode
被传递给commitRoot
方法,开启commit
阶段工作流程。
commitRoot(root);