react源码分析(三)render-render阶段
如果以下代码没有特殊标记,路径都是 src/react/packages/react-reconciler/src/ReactFiberWorkLoop.js
这个 performConcurrentWorkOnRoot
函数中包含两个重要的阶段,render
和 commit
阶段。这是每个并发任务的入口点。
1 | // This is the entry point for every concurrent task, i.e. anything that goes through Scheduler. |
1. render阶段 - renderRootSync
进入 render
阶段的,同步渲染模式。
1 | function renderRootSync(root: FiberRoot, lanes: Lanes) { |
1.1 记录 render 阶段 开始
1 | const prevExecutionContext = executionContext; |
1.2 workInProgressTransitions赋值
1 | workInProgressTransitions = getTransitionsForLanes(root, lanes); |
1.3 prepareFreshStack 初始化
初始化 workInProgressRoot
、workInProgress
、workInProgressRootRenderLanes
等值。
1 | function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { |
1.4.1 workLoopSync
1 | function workLoopSync() { |
来自 renderRootSync,while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress === null。
所以同步渲染模式,一旦开始这个Fiber Reconciler协调流程,就会进入一直循环的逻辑,直到创建出完整的FiberTree,这个过程无法中断。
下面我们来看看并发渲染模式的方法。
1.4.2 workLoopConcurrent
1 | function workLoopConcurrent() { |
来自 renderRootConcurrent,可以看见和同步渲染模式基本一样,唯一的区别就是while循环的多了一个判断条件:当前程序运行时间是否小于帧间隔时间frameInterval【默认5ms】。
- 如果小于:则返回false,代表还有剩余可执行时间。取反后为true,表示可以继续创建FiberTree的工作循环。
- 如果大于:则返回true,代表没有剩余可执行时间。取反后为false,则while循环的条件将不再满足,会暂停创建FiberTree的工作,结束本次宏任务,剩下的工作会留待下一次宏任务再处理。
所以并发渲染模式和同步渲染模式的主要区别就是在这里:它可以中断FiberTree的创建过程,而同步渲染模式是无法中断这个过程的,它只能从开始到创建完成。
并且在这里我们也可以看出,它们的while循环里面都是执行了一个相同的performUnitOfWork方法,所以后面的逻辑也就没有什么模式的区分了,都是执行的一样的内容。
1.4.3 并发模式下暂停工作与恢复执行
packages\react-reconciler\src\ReactFiberWorkLoop.js
1 | // 并发渲染模式 |
当 workLoopConcurrent
并发渲染的工作被中断后,就会退出 do while
循环,然后就会检查当前的 workInProgress
是否为 null
,很明显被暂停的情况,workInProgress
是一定有值的,它的值为下一个即将处理的 Fiber
节点。
此时 workInProgress
不为 null
,就会返回 RootInProgress
的渲染状态,表示还有剩下的工作。
这里 return 之后,就会回到 performConcurrentWorkOnRoot
方法之中。
1 | function performConcurrentWorkOnRoot(root, didTimeout) { |
1 | function workLoop() { |
在workLoop方法之中,就会将返回的内容再次赋值给当前任务currentTask的callback属性,表示还有未完成的工作。workLoop方法中的while循环就会被中断,原因同样是shouldYieldToHost方法。
在workLoop方法最后就会判断当前任务currentTask是否为null,很明显被中断的情况,currentTask是有值的,所以就会返回true,这里会一直向上return:
最后会将结果返回performWorkUntilDeadline方法之中:
1 | const performWorkUntilDeadline = () => { |
此时hasMoreWork为true,就会调用schedulePerformWorkUntilDeadline方法,生成一个新的宏任务,在下个宏任务中继续执行剩下的任务。
恢复执行的原理也很简单:在下次的宏任务中继续创建FiberTree的时候,因为workInProgress是一个全局变量,一直保存在内存之中,并且它内容为下一个即将处理的Fiber节点,所以下次再回到这里时,就会自动从这个Fiber节点开始继续执行剩下的创建工作。
1.5 performUnitOfWork
1 | function performUnitOfWork(unitOfWork: Fiber): void { |
Fiber 树结构
1 | // index.js |
1 | // App.js |
1 | // MyFun.js |
1 | // MyClass.js |
1.5.1 beginWork
简单来说,就是生成 fiber 树。
packages\react-reconciler\src\ReactFiberBeginWork.js
1 | let didReceiveUpdate: boolean = false; |
1.5.1.1 updateHostRoot
- 更新当前 fiber,比如 props/state 更新,生命周期函数执行、Hooks 函数执行等。
- 返回一个下一个 fiber。
packages\react-reconciler\src\ReactFiberBeginWork.js
1 | function updateHostRoot( |
1.5.1.2 updateHostComponent
packages\react-reconciler\src\ReactFiberBeginWork.js
1 | function updateHostComponent( |
1.5.1.3 bailout
bailout阶段,返回null。
不会发生在组件初次渲染阶段,仅仅发生在组件更新阶段。当组件⼦节点没有发⽣变化,或者是被⼿动挡住(如类组件的 shouldComponentUpdate、memo 等),组件⼦节点不需要协调的时候。
1.5.1.4 协调子节点 reconcileChildren
1 | export function reconcileChildren( |
其实最终执行的函数是同一个,都是 createChildReconciler
,而 createChildReconciler
是一个 wrapper function。最终会返回内部的 reconcileChildFibers
函数。
1 | export const reconcileChildFibers: ChildReconciler = |
1.5.1.5 reconcileChildFibers
1 | // * 协调子节点,构建新的子fiber结构,并且返回新的子fiber |
1.5.1.6 reconcileChildFibersImpl
1 | function reconcileChildFibersImpl( |
1.5.1.6 reconcileSingleElement
1 | function reconcileSingleElement( |
1.5.1.7 createFiberFromElement
1 | export function createFiberFromElement( |
createFiberFromElement
方法很重要,react
所有 reactElement
元素都是通过这个方法来创建对应的 Fiber
节点。比如常见的 class组件,function组件,原生dom元素等。我们在项目中定义的组件和元素在被编译之后,最终在加载时都会通过jsxRuntime.jsx方法转换为一个reactElement元素对象。
1.5.1.8 createFiberFromTypeAndProps
1 | export function createFiberFromTypeAndProps( |
在react应用中我们定义的class组件,都是通过继承react的内部组件Component来定义的,所以通过它原型上的isReactComponent属性可以来区分当前组件是不是类组件,返回true则是类组件,返回false则是函数组件。
如果是类组件就会更新fiberTag为ClassComponent的值。
如果是函数组件,这里并没有更新fiberTag,所以函数组件的fiberTag还是为待定的IndeterminateComponent的值。
最后是针对普通的dom元素的处理,更新为HostComponent的值。
1.5.1.9 多子节点的创建 - reconcileChildrenArray
1 | function reconcileChildrenArray( |
总结
从 hostFiber 根节点开始,react 的深度优先遍历算法,会一直创建到再无 Child 子节点。
此时叶子结点所对应的Fiber节点创建完成后,它会继续开启属于它的 beginWork 工作,来创建它的子节点,但是因为它已经没有子节点了,所以它最终会返回一个null。
然后一直向上return,直到回到performUnitOfWork方法中:将进入此 Fiber 节点的 completeWork 工作,也就是归的阶段
1.5.2 completeUnitOfWork
completeWork 代表一个 Fiber 节点的结束工作,completeUnitOfWork 是 completeWork 阶段的入口函数。简单来说,就是生成 dom 树。
1 | function completeUnitOfWork(unitOfWork: Fiber): void { |
completeUnitOfWork方法中重点就是一个do while循环:completedWork !== null
。这个循环的条件是completedWork不等于null,即它是一个有效的Fiber节点。
1.5.2.1 completeWork
根据tag值区别不同fiber
1.5.2.2 bubbleProperties
向上冒泡 subtreeFlags 和 childLanes
1 | function bubbleProperties(completedWork: Fiber) { |
1.5.2.3 createInstance, 创建 DOM 节点,并添加到父节点中
如果组件是HostComponent,即原生标签
1 | const instance = createInstance( |
总结执行流程图
1.6 重置 workInProgressX
这里没有重置 workInProgress,因为 workInProgress 已经在 performUnitOfWork 阶段更新。
1 | // ! 5. 重置 |
1.7 再遍历一遍更新队列
1 | finishQueueingConcurrentUpdates() |
管理更新队列 finishQueueingConcurrentUpdates
finishQueueingConcurrentUpdates
把 concurrentQueues
的内容添加到 fiber
的 queue
中,后续才是根据 VDOM
更新 DOM
在 render
阶段,有两处调⽤ finishQueueingConcurrentUpdates
,分别是
render
开始的时候,在prepareFreshStack
函数中;- 在
render
结束的时候,最后再调⽤⼀遍。
packages\react-reconciler\src\ReactFiberConcurrentUpdates.js
1 | export function finishQueueingConcurrentUpdates(): void { |
markUpdateLaneFromFiberToRoot
1 | function markUpdateLaneFromFiberToRoot( |
处理更新队列 processUpdateQueue
这个函数⽤来处理更新队列。
processUpdateQueue
在 beginWork
阶段会被两个地⽅调⽤:
updateHostRoot packages\react-reconciler\src\ReactFiberBeginWork.js
1
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
updateClassInstance packages\react-reconciler\src\ReactFiberClassComponent.js
在类组件的 mount、resumeMount、更新阶段,均会调⽤。
1
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
processUpdateQueue 流程
检查是否有 pending update。
- 如果有,将它们转移到 baseQueue。
- pending update 是个单向循环链表,转移到 单链表 firstBaseUpdate->…->lastBaseUpdate 中去。
遍历 queue,根据这些 update 计算出最后的结果
1 | // 处理这个更新 |
- 更新到 fiber 上
1
2workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState; // 更新状态