React的diff
React 的 diff
一个DOM节点在某一时刻最多会有 4 个节点和它相关。
current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点。DOM节点本身。JSX对象。即ClassComponent的render方法的返回结果,或FunctionComponent的调用结果。JSX对象中包含描述DOM节点的信息。
Diff算法的本质是对比 1 和 4,生成 2
diff 算法的优化
由于 diff 算法本身会带来性能损耗,前后两颗树完全比对的算法复杂度为 O(n3),其中 n 是树中元素的的数量。
为了降低算法复杂度,React的diff会预设三个限制:
- 只对同级元素进行
Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。 - 两个不同类型的元素会产生出不同的树。如果元素由
div变为p,React 会销毁div及其子孙节点,并新建p及其子孙节点。 - 开发者可以通过
key prop来暗示哪些子元素在不同的渲染下能保持稳定。
考虑如下例子:
// 更新前 |
如果没有key,React会认为div的第一个子节点由p变为h1,第二个子节点由h1变为p。这符合限制 2 的设定,会销毁并新建。
但是当我们用key指明了节点前后对应关系后,React知道key === "ran"的p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。
reconcileChildFibers 函数会根据不同的 newChild(JSX 对象)调用不同的处理函数。
单节点 diff
当 newChild 类型为 object、number、string,代表同级只有一个节点。
- 先判断 key 是否相同,然后 type,都相同时 DOM 才能复用。
- 当 child !== null 且 key 相同且 type 不同时,执行 deleteRemainingChildren 将 child 及其兄弟 fiber 都标记删除。
- 当 child !== null 且 key 不同时,仅将 child 标记删除。
关于 2,3 步,当 key 相同但 type 不同,说明已经完全无法复用了,都需要删除。但 key 不同只代表该 fiber 不能复用,后面的兄弟 fiber 还有复用的可能性。
多节点 diff
当 newChild 类型为 Array,同级有多个节点。
多节点 diff 有多种情况需要处理
1. 节点更新
节点更新又包含两种情况:
<div> |
2. 节点新增或删除
<div> |
3. 节点位置变化
<div> |
不同的情况执行不同的逻辑,React 团队发现更新比其他两种的频率更高,于是 diff 优先判断更新情况。又因为 fiber 是单链表结构的,所以无法使用双指针优化遍历。diff 会经过两轮遍历:
- 第一轮:处理更新节点。
- 第二轮:处理不为更新的节点。
React 中触发更新
除了 SSR 相关,触发更新的方法:
- ReactDOM.render
- this.setState
- this.forceUpdate
- useState
- useReducer
调度更新
render 阶段从 rootFiber 开始向下遍历,触发更新的 fiber 调用 markUpdateLaneFromFiberToRoot 一直向上遍历到 rootFiber 并返回 rootFiber。触发更新的 fiber 中保存了一个 Update 的对象。
之后通知 Scheduler 根据更新的优先级,决定以同步还是异步的方式调度本次更新。
高优更新中断正在进行中的低优更新,先完成render - commit流程。
待高优更新完成后,低优更新基于高优更新的结果重新更新。