Fiber大家已经很熟悉了在这里就不再介绍了,简单的总结一下新老架构
新老架构简介
- 老的架构使用递归更新会有两个缺点:
- 当层级深的时候,递归更新时间超过16ms,用户会感觉卡顿。
- 因为递归更新无法中断,假设遍历发生了中断,虽然可以保留当下进行中节点的索引,下次继续时,我们的确可以继续遍历该节点下面的所有子节点,但是没有办法找到其父节点——因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍。
- 新架构的改变:
- 增加
Scheduler(调度器),可以组件渲染的工作分片,到时会主动让出渲染主线程。Scheduler还提供了多种调度优先级供任务设置。 Reconciler内部采用Fiber(纤程)架构,用链表取代了树,记录了子节点(child)、兄弟组节点(sibling)、父节点(return),生成Fiber树。- 双缓存Fiber树
current Fiber树:屏幕上显示内容对应的Fiber树workInProgress Fiber树:正在内存中构建的Fiber树。- 多次调用
ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。 workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。即fiberRootNode.current = rootFiber
- 增加
“递”阶段mount时流程
“递”阶段
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 方法。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
“归”阶段
在“归”阶段会调用 completeWork 处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。
例子
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
对应的”递“”归”过程如下图:
beginWork
方法定义
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
- current:当前组件对应的Fiber节点在上一次更新时的
Fiber节点,即workInProgress.alternate - workInProgress:当前组件对应的Fiber节点
- renderLanes:优先级相关,在讲解
Scheduler时再讲解
从双缓存机制我们知道,除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。
组件update时,由于之前已经mount过,所以current !== null。
所以我们可以通过current === null ?来区分组件是处于mount还是update。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
mount时
我们以上面代码中div的beginWork为例子
因为是current fiber为空下一步这是进入switch(workInProgress.tag)
updateHostComponent
div workInProgress.tag为HostComponent,进入updateHostComponent
shouldSetTextContent方法判断当前Fiber节点是否只有唯一一个文本子节点,如果结果为true就不会为文本子节点单独创建一个Fiber节点,这也是react的一个优化策略
reconcileChildren
updateHostComponent方法返回一个workInProgress.child,当reconcileChildren没有执行时,workInProgress.child为null,所以reconcileChildren会为当前workInProgress创建一个child节点。
reconcileChildren方法通过判断current === null进入mountChildFibers还是reconcileChildFibers
在源码ReactChildFiber.old.js文件中我们可以看到
mountChildFibers和reconcileChildFibers方法调用的都是ChildReconciler这是调用的时候传递的boolean不同
ChildReconciler
shouldTrackSideEffects表示是否追踪副作用
什么是副作用
当shouldTrackSideEffects === false时,会直接return
当shouldTrackSideEffects === true时,会将要删除的Fiber节点的flags打上Deletion标记
扩展:为什么effectTag用的是二进制表示?
在ReactFiberFlags.js文件保存了所有的副作用(render阶段不会执行DOM操作只会给相应的Fiber节点打上标记)
通过二进制表示
effectTag,可以方便的使用位操作为fiber.flags赋值多个effect。
继续ChildReconciler
ChildReconciler会判断newChild(当前Fiber节点的子节点)是否是一个Object或者是string和number或是否为一个Array
Object表示只有一个子节点string和number表示文本子节点Array表示有多个子节点
多个子节点时,每次
beginWork只会为当前节点创建一个子节点,会进行多次beginWork
newChild为Object,并且newChild.$$typeof为REACT_ELEMENT_TYPE 进入placeSingleChild方
法
placeSingleChild
createFiberFromElement
通过调用createFiberFromTypeAndProps创建一个Fiber节点并返回。
createFiberFromTypeAndProps
为fiberTag赋值,调用createFiber方法创建Fiber节点
createFiber
实例化一个Fiber节点并返回
div mount大致流程
总结
mount时的beginWork就是为当前Fiber节点创建一个子Fiber节点- 每次
beginWork只会创建一个子Fiber节点 - 当前
Fiber节点的所有子Fiber节点beginWork后会进行CompleteWrk`
参考资料
React技术揭秘: react.iamkasong.com/process/rec…