React Reconciler Render阶段之beginWork理解

React Reconciler Render阶段之beginWork理解

Tags
Source Code
React.js
Published
Author

render阶段

根据本次更新是同步更新还是异步更新,在render阶段中,也就是方法中调用performSyncWorkOnRootperformConcurrentWorkOnRoot方法,从根Fiber节点递归创建子Fiber节点。 > workLoopConcurrent() > workLoopSync()
// performSyncWorkOnRoot中会调用该方法function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); }}// performConcurrentWorkOnRoot中会调用该方法function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); }}
shouldYield:true浏览器帧没有剩余时间,应该停止循环 shouldYield:false浏览器帧有剩余时间,可以继续循环
上面代码的含义: 如果当前浏览器帧没有剩余时间,shouldYield = true,会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前已创建的workInProgress Fiber
接下来的performUnitOfWork方法又分为两个步骤:
  • beginWork,传入当前 Fiber 节点,创建子 Fiber 节点
  • completeUnitOfWork,通过Fiber 节点创建真实 DOM 节点
最终的目标就是:
  • 构建出新的 Fiber 树(workInProgress Fiber 树)
  • 与旧 Fiber 比较得到 effect 链表(插入、更新、删除、useEffect 等都会产生 effect)
下面通过一个例子来看代码是怎么调度的:
function App() { return ( <div> <span>this is Header</span> main <footer>this is Footer</footer> </div> ) } ReactDOM.render(<App />, document.getElementById("root"));
notion image
fibertree
整体流程如下:
rootFiber beginWorkApp Fiber beginWorkdiv Fiber beginWorkspan Fiber beginWork“this is Header” Fiber beginWork“this is Header” Fiber completeWorkspan Fiber completeWorkmain Fiber beginWorkmain Fiber completeWorkfooter Fiber beginWork“this is Footer” Fiber beginWork“this is Footer” Fiber completeWorkfooter Fiber completeWorkdiv Fiber completeWorkApp Fiber completeWorkrootFiber completeWork

performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = unitOfWork.alternate; setCurrentDebugFiberInDEV(unitOfWork); let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, subtreeRenderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { next = beginWork(current, unitOfWork, subtreeRenderLanes); } resetCurrentDebugFiberInDEV(); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; } ReactCurrentOwner.current = null;}

beginWork

beginWork调度完该节点之后,返回workInProgress.child。返回到performUnitOfWork函数,next为当前节点的child,如果next !== null则准备开始进入子节点的调度
function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes,): Fiber | null { const updateLanes = workInProgress.lanes; // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点) if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || hasLegacyContextChanged() || // Force a re-render if the implementation changed due to hot reload: (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; } else if (!includesSomeLane(renderLanes, updateLanes)) { ... return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } else { if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) { didReceiveUpdate = true; } else { didReceiveUpdate = false; } } } else { didReceiveUpdate = false; } workInProgress.lanes = NoLanes; switch (workInProgress.tag) { case IndeterminateComponent: { return mountIndeterminateComponent( current, workInProgress, workInProgress.type, renderLanes, ); } case LazyComponent: { const elementType = workInProgress.elementType; return mountLazyComponent( current, workInProgress, elementType, updateLanes, renderLanes, ); } case FunctionComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); } case ClassComponent: { const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps); return updateClassComponent( current, workInProgress, Component, resolvedProps, renderLanes, ); } case HostRoot: return updateHostRoot(current, workInProgress, renderLanes); case HostComponent: return updateHostComponent(current, workInProgress, renderLanes); .... }}
简化部分代码,我们可以发现整个beginWork函数可以分为两个部分:workInProgress.lanes = NoLanes;之前的部分是关于复用子Fiber节点的逻辑,即进入bailout流程,后面则是关于更新当前 Fiber 节点的逻辑。
  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级
除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null
组件update时,由于之前已经mount过,所以current !== null
  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child
  • mount时:除FiberRootNode以外,current === null。会根据Fiber.tag不同,创建不同类型的子Fiber节点

组件update时

Render 阶段会重新构建一颗 Fiber 树,但是当命中 bailout 逻辑子孙节点没有更新任务时,会复用以当前 Fiber 节点为根的整颗子树。
bailoutOnAlreadyFinishedWork
function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes,): Fiber | null { if (current !== null) { // Reuse previous dependencies workInProgress.dependencies = current.dependencies; } if (enableProfilerTimer) { // Don't update "base" render times for bailouts. stopProfilerTimerIfRunning(workInProgress); } markSkippedUpdateLanes(workInProgress.lanes); // Check if the children have any pending work. if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // The children don't have any work either. We can skip them. // TODO: Once we add back resuming, we should check if the children are // a work-in-progress set. If so, we need to transfer their effects. return null; } else { // This fiber doesn't have work, but its subtree does. Clone the child // fibers and continue. cloneChildFibers(current, workInProgress); return workInProgress.child; }}
bailout是否返回 null 需要看看当前 Fiber 节点子孙节点中是否有更新任务,如果有则不能直接返回 null,仍然需要对子节点进行处理。
当前Fiber 节点如何知道子孙节点需要更新呢?
是因为当某个节点触发了更新时,会沿着 Fiber 一直往上冒泡,这个过程中每个节点都能收集到自己子孙节点的相关信息:
markUpdateLaneFromFiberToRoot
function markUpdateLaneFromFiberToRoot( sourceFiber: Fiber, lane: Lane,): FiberRoot | null { // Update the source fiber's lanes sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); let alternate = sourceFiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, lane); } // Walk the parent path to the root and update the child expiration time. let node = sourceFiber; let parent = sourceFiber.return; while (parent !== null) { parent.childLanes = mergeLanes(parent.childLanes, lane); alternate = parent.alternate; if (alternate !== null) { alternate.childLanes = mergeLanes(alternate.childLanes, lane); } else { if (__DEV__) { if ((parent.flags & (Placement | Hydrating)) !== NoFlags) { warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber); } } } node = parent; parent = parent.return; } if (node.tag === HostRoot) { const root: FiberRoot = node.stateNode; return root; } else { return null; }}

bailout的前提条件

进入 bailout的判断条件有三个:
  • oldProps === newProps
  • hasLegacyContextChanged() 为 false
  • includesSomeLane(renderLanes, updateLanes) 为 false

oldProps === newProps

import React from 'react'function Son() { console.log('son render') return <div>Son</div>;}export default class App extends React.Component { state = { name: 'a' } componentDidMount() { setTimeout(() => { this.setState({ name: 'b' }) }, 1000) } render() { return <Son /> }}
上面的例子中setState触发了更新,两次return的React.createElement(Son)不是同一个对象。
很好理解,当前节点的属性是否有变化,没变化就可以进入,只要更新不是在 App 组件上触发的。
class Son extends React.Component { render() { console.log('child render') return <span>{this.context.value}</span> }}const memoizedSon = <Son />export default class App extends React.Component { componentDidMount() { setTimeout(() => { this.setState({ value: 'new context' }) }, 1000) } render() { return memoizedSon; }}
如果通过缓存,每次 render 返回的都是同一个 ReactElement 对象,通过其创建的 Fiber 上的 pendingPropsmemoizedProps 也都指向同一个对象

hasLegacyContextChanged()

class Son extends React.Component { render() { console.log('child render') return <span>{this.context.value}</span> }}Son.contextTypes = { value: PropTypes.string};const memoizedSon = <Son />export default class App extends React.Component { state = { value: 'context' } getChildContext() { return this.state } componentDidMount() { setTimeout(() => { this.setState({ value: 'new context' }) }, 1000) } render() { return memoizedSon; }}App.childContextTypes = { value: PropTypes.string}
使用了旧的已废弃的 Context,hasLegacyContextChanged() 会为 true,所以这个例子不会走 bailout。

includesSomeLane(renderLanes, updateLanes)

  • renderLanes: 当前节点更新的优先级
  • updateLanes: 此次更新的优先级
判断当前节点上的更新任务的优先级是否包含在了此次更新 的优先级之中。如果当前节点的更新优先级大于等于此次更新的优先级,则 includesSomeLane(renderLanes, updateLanes) 会返回 true
这边!includesSomeLane(renderLanes, updateLanes)指当前Fiber节点优先级不够。

reconcileChildren

对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法
  • 对于mount的组件,除fiberRootNode以外,current === null。根据fiber.tag不同,创建不同类型的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点
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, ); }}

总结

最终会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress的传参,这样就根节点遍历到所有的子节点并生成Fiber节点组成Fiber Tree
mountChildFibersreconcileChildFibers逻辑基本一致,区别是:reconcileChildFibers会为生成的Fiber节点带上effects属性,而mountChildFibers不会。
// DOM需要插入到页面中export const Placement = /* */ 0b00000000000010;// DOM需要更新export const Update = /* */ 0b00000000000100;// DOM需要插入到页面中并更新export const PlacementAndUpdate = /* */ 0b00000000000110;// DOM需要删除export const Deletion = /* */ 0b00000000001000;// 删除子节点export const ChildDeletion = /* */ 0b000000000000000010000;
通过二进制表示effectTag,可以使用位操作为fiber.effectTag赋值多个effect
beginWork中会通过flags收集自身副作用。然后在completeWork中 将flags 冒泡合并到祖先的 subtreeFlags。这样做的好处是可以 在commit 阶段,跳过无副作用子树。
二进制位运算在React中的含义:
workInProgress.flags |= PerformedWork这个操作可以看作是给workInProgressflags基础上增加一个新的标记。
(completedWork.flags & Incomplete) === NoFlags这个判断条件是,如果flags中没有Incomplete,才会进入。
如果要通知RendererFiber节点对应的DOM节点插入页面中,需要满足两个条件:
  1. fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点
  1. (fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
整体流程如下:
notion image
beginWork
参考: React17.02 React 技术揭秘 React Fiber源码解析 图解React React Note