render阶段
根据本次更新是同步更新还是异步更新,在render阶段中,也就是方法中调用performSyncWorkOnRoot或performConcurrentWorkOnRoot方法,从根Fiber节点递归创建子Fiber节点。
// 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"));
整体流程如下:
- rootFiber beginWork
- App Fiber beginWork
- div Fiber beginWork
- span Fiber beginWork
- “this is Header” Fiber beginWork
- “this is Header” Fiber completeWork
- span Fiber completeWork
- main Fiber beginWork
- main Fiber completeWork
- footer Fiber beginWork
- “this is Footer” Fiber beginWork
- “this is Footer” Fiber completeWork
- footer Fiber completeWork
- div Fiber completeWork
- App Fiber completeWork
- rootFiber 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.alternateworkInProgress:当前组件对应的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 节点为根的整颗子树。
function bailoutOnAlreadyFinishedWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,): Fiber | null {if (current !== null) {// Reuse previous dependenciesworkInProgress.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 一直往上冒泡,这个过程中每个节点都能收集到自己子孙节点的相关信息:
function markUpdateLaneFromFiberToRoot(sourceFiber: Fiber,lane: Lane,): FiberRoot | null {// Update the source fiber's lanessourceFiber.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 === newPropshasLegacyContextChanged()为 falseincludesSomeLane(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
整体流程如下:
参考:
React17.02
React 技术揭秘
React Fiber源码解析
图解React
React Note
