render阶段
根据本次更新是同步更新还是异步更新,在
render阶段
中,也就是方法中
调用performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法,从根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"));
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
上的 pendingProps
和 memoizedProps
也都指向同一个对象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
mountChildFibers
和reconcileChildFibers
逻辑基本一致,区别是: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
这个操作可以看作是给workInProgress
的flags
基础上增加一个新的标记。(completedWork.flags & Incomplete) === NoFlags
这个判断条件是,如果flags中没有Incomplete,才会进入。如果要通知
Renderer
将Fiber节点
对应的DOM节点
插入页面中,需要满足两个条件:fiber.stateNode
存在,即Fiber节点
中保存了对应的DOM节点
(fiber.effectTag & Placement) !== 0
,即Fiber节点
存在Placement effectTag
整体流程如下:
beginWork
参考: React17.02 React 技术揭秘 React Fiber源码解析 图解React React Note