react源码解析8.render阶段
source link: https://segmentfault.com/a/1190000041127379
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
react源码解析8.render阶段
视频讲解(高效学习):进入学习
往期文章:
render阶段的入口
render阶段的主要工作是构建Fiber树和生成effectList,在第5章中我们知道了react入口的两种模式会进入performSyncWorkOnRoot或者performConcurrentWorkOnRoot,而这两个方法分别会调用workLoopSync或者workLoopConcurrent
//ReactFiberWorkLoop.old.js function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); } } function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
这两函数的区别是判断条件是否存在shouldYield的执行,如果浏览器没有足够的时间,那么会终止while循环,也不会执行后面的performUnitOfWork函数,自然也不会执行后面的render阶段和commit阶段,这部分属于scheduler的知识点,我们在第15章讲解。
- workInProgress:新创建的workInProgress fiber
performUnitOfWork:workInProgress fiber和会和已经创建的Fiber连接起来形成Fiber树。这个过程类似深度优先遍历,我们暂且称它们为‘捕获阶段’和‘冒泡阶段’。伪代码执行的过程大概如下
function performUnitOfWork(fiber) { if (fiber.child) { performUnitOfWork(fiber.child);//beginWork } if (fiber.sibling) { performUnitOfWork(fiber.sibling);//completeWork } }
render阶段整体执行流程
用demo_0看视频调试
- 捕获阶段
从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。 - 冒泡阶段
在捕获阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上‘冒泡’到父节点执行completeWork,直到rootFiber。 示例,demo_0调试
function App() { return ( <> <h1> <p>count</p> xiaochen </h1> </> ) } ReactDOM.render(<App />, document.getElementById("root"));
当执行完深度优先遍历之后形成的Fiber树:
图中的数字是遍历过程中的顺序,可以看到,遍历的过程中会从应用的根节点rootFiber开始,依次执行beginWork和completeWork,最后形成一颗Fiber树,每个节点以child和return相连。
注意:当遍历到只有一个子文本节点的Fiber时,该Fiber节点的子节点不会执行beginWork和completeWork,如图中的‘chen’文本节点。这是react的一种优化手段
beginWork
beginWork主要的工作是创建或复用子fiber节点
function beginWork( current: Fiber | null,//当前存在于dom树中对应的Fiber树 workInProgress: Fiber,//正在构建的Fiber树 renderLanes: Lanes,//第12章在讲 ): Fiber | null { // 1.update时满足条件即可复用current fiber进入bailoutOnAlreadyFinishedWork函数 if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false; switch (workInProgress.tag) { // ... } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } } else { didReceiveUpdate = false; } //2.根据tag来创建不同的fiber 最后进入reconcileChildren函数 switch (workInProgress.tag) { case IndeterminateComponent: // ... case LazyComponent: // ... case FunctionComponent: // ... case ClassComponent: // ... case HostRoot: // ... case HostComponent: // ... case HostText: // ... } }
从代码中可以看到参数中有current Fiber,也就是当前真实dom对应的Fiber树,在之前介绍Fiber双缓存机制中,我们知道在首次渲染时除了rootFiber外,current 等于 null,因为首次渲染dom还没构建出来,在update时current不等于 null,因为update时dom树已经存在了,所以beginWork函数中用current === null来判断是mount还是update进入不同的逻辑
- mount:根据fiber.tag进入不同fiber的创建函数,最后都会调用到reconcileChildren创建子Fiber
update:在构建workInProgress的时候,当满足条件时,会复用current Fiber来进行优化,也就是进入bailoutOnAlreadyFinishedWork的逻辑,能复用didReceiveUpdate变量是false,复用的条件是
- oldProps === newProps && workInProgress.type === current.type 属性和fiber的type不变
- !includesSomeLane(renderLanes, updateLanes) 更新的优先级是否足够,第15章讲解
reconcileChildren/mountChildFibers
创建子fiber的过程会进入reconcileChildren,该函数的作用是为workInProgress fiber节点生成它的child fiber即 workInProgress.child。然后继续深度优先遍历它的子节点执行相同的操作。
//ReactFiberBeginWork.old.js export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { //mount时 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes, ); } else { //update workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); } }
reconcileChildren会区分mount和update两种情况,进入reconcileChildFibers或mountChildFibers,reconcileChildFibers和mountChildFibers最终其实就是ChildReconciler传递不同的参数返回的函数,这个参数用来表示是否追踪副作用,在ChildReconciler中用shouldTrackSideEffects来判断是否为对应的节点打上effectTag,例如如果一个节点需要进行插入操作,需要满足两个条件:
- fiber.stateNode!==null 即fiber存在真实dom,真实dom保存在stateNode上
(fiber.effectTag & Placement) !== 0 fiber存在Placement的effectTag
var reconcileChildFibers = ChildReconciler(true); var mountChildFibers = ChildReconciler(false);
```js function ChildReconciler(shouldTrackSideEffects) { function placeChild(newFiber, lastPlacedIndex, newIndex) { newFiber.index = newIndex; if (!shouldTrackSideEffects) {//是否追踪副作用 // Noop. return lastPlacedIndex; } var current = newFiber.alternate; if (current !== null) { var oldIndex = current.index; if (oldIndex < lastPlacedIndex) { // This is a move. newFiber.flags = Placement; return lastPlacedIndex; } else { // This item can stay in place. return oldIndex; } } else { // This is an insertion. newFiber.flags = Placement; return lastPlacedIndex; } } } ```
在之前心智模型的介绍中,我们知道为Fiber打上effectTag之后在commit阶段会被执行对应dom的增删改,而且在reconcileChildren的时候,rootFiber是存在alternate的,即rootFiber存在对应的current Fiber,所以rootFiber会走reconcileChildFibers的逻辑,所以shouldTrackSideEffects等于true会追踪副作用,最后为rootFiber打上Placement的effectTag,然后将dom一次性插入,提高性能。
export const NoFlags = /* */ 0b0000000000000000000; // 插入dom export const Placement = /* */ 0b00000000000010;
在源码的ReactFiberFlags.js文件中,用二进制位运算来判断是否存在Placement,例如让var a = NoFlags,如果需要在a上增加Placement的effectTag,就只要 effectTag | Placement就可以了
bailoutOnAlreadyFinishedWork
//ReactFiberBeginWork.old.js function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) { //... if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { return null; } else { cloneChildFibers(current, workInProgress); return workInProgress.child; } }
如果进入了bailoutOnAlreadyFinishedWork复用的逻辑,会判断优先级第12章介绍,优先级足够则进入cloneChildFibers否则返回null
completeWork
completeWork主要工作是处理fiber的props、创建dom、创建effectList
//ReactFiberCompleteWork.old.js function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { const newProps = workInProgress.pendingProps; //根据workInProgress.tag进入不同逻辑,这里我们关注HostComponent,HostComponent,其他类型之后在讲 switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case HostRoot: //... case HostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { // update时 updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); } else { // 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; // 处理props和updateHostComponent类似 if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { markUpdate(workInProgress); } } return null; }
从简化版的completeWork中可以看到,这个函数做了一下几件事
- 根据workInProgress.tag进入不同函数,我们以HostComponent举例
- update时(除了判断current===null外还需要判断workInProgress.stateNode===null),调用updateHostComponent处理props(包括onClick、style、children ...),并将处理好的props赋值给updatePayload,最后会保存在workInProgress.updateQueue上
- mount时 调用createInstance创建dom,将后代dom节点插入刚创建的dom中,调用finalizeInitialChildren处理props(和updateHostComponent处理的逻辑类似)
之前我们有说到在beginWork的mount时,rootFiber存在对应的current,所以他会执行mountChildFibers打上Placement的effectTag,在冒泡阶段也就是执行completeWork时,我们将子孙节点通过appendAllChildren挂载到新创建的dom节点上,最后就可以一次性将内存中的节点用dom原生方法反应到真实dom中。
在beginWork 中我们知道有的节点被打上了effectTag的标记,有的没有,而在commit阶段时要遍历所有包含effectTag的Fiber来执行对应的增删改,那我们还需要从Fiber树中找到这些带effectTag的节点嘛,答案是不需要的,这里是以空间换时间,在执行completeWork的时候遇到了带effectTag的节点,会将这个节点加入一个叫effectList中,所以在commit阶段只要遍历effectList就可以了(rootFiber.firstEffect.nextEffect就可以访问带effectTag的Fiber了)
effectList的指针操作发生在completeUnitOfWork函数中,例如我们的应用是这样的
function App() { const [count, setCount] = useState(0); return ( <> <h1 onClick={() => { setCount(() => count + 1); }} > <p title={count}>{count}</p> xiaochen </h1> </> ) }
那么我们的操作effectList指针如下(这张图是操作指针过程中的图,此时遍历到了app Fiber节点,当遍历到rootFiber时,h1,p节点会和rootFiber形成环状链表)
rootFiber.firstEffect===h1 rootFiber.firstEffect.next===p
形成环状链表的时候会从触发更新的节点向上合并effectList直到rootFiber,这一过程发生在completeUnitOfWork函数中,整个函数的作用就是向上合并effectList
//ReactFiberWorkLoop.old.js function completeUnitOfWork(unitOfWork: Fiber): void { let completedWork = unitOfWork; do { //... if ( returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags ) { if (returnFiber.firstEffect === null) { returnFiber.firstEffect = completedWork.firstEffect;//父节点的effectList头指针指向completedWork的effectList头指针 } if (completedWork.lastEffect !== null) { if (returnFiber.lastEffect !== null) { //父节点的effectList头尾指针指向completedWork的effectList头指针 returnFiber.lastEffect.nextEffect = completedWork.firstEffect; } //父节点头的effectList尾指针指向completedWork的effectList尾指针 returnFiber.lastEffect = completedWork.lastEffect; } const flags = completedWork.flags; if (flags > PerformedWork) { if (returnFiber.lastEffect !== null) { //completedWork本身追加到returnFiber的effectList结尾 returnFiber.lastEffect.nextEffect = completedWork; } else { //returnFiber的effectList头节点指向completedWork returnFiber.firstEffect = completedWork; } //returnFiber的effectList尾节点指向completedWork returnFiber.lastEffect = completedWork; } } } else { //... if (returnFiber !== null) { returnFiber.firstEffect = returnFiber.lastEffect = null;//重制effectList returnFiber.flags |= Incomplete; } } } while (completedWork !== null); //... }
最后生成的fiber树如下
然后commitRoot(root);进入commit阶段
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK