type
status
date
slug
summary
tags
category
icon
password

1. 前言

那么双缓存 Fiber 树是如何构建的?
在处理好优先级和标记后,就会进入到 render 阶段,此时仍在 Reconciler 中。取决于本次更新为同步还是异步,React 会调用 performSyncWorkOnRootperformConcurrentWorkOnRoot,前者为同步更新,后者为异步更新:
唯一的区别就是是否调用 shouldYield 方法,如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress 就是当前已创建的 workInProgress fiber,performUnitOfWork 用于创建下一个 fiber 节点并赋值给 workInProgress,然后连接起来称为 Fiber 树。
在 React 16 架构中的 Fiber Reconciler 是从 Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归,所以 performUnitOfWork 的工作可以分为两部分:“递”和“归”。

2. 递阶段(beginWork

从 rootFiber 向下深度优先遍历,调用 beginWork 方法,将注入的 fiber 节点创建子 filer 节点,然后连接在一起,直到叶子节点(即没有子组件的组件)时进入“归”阶段。
  • current:当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即workInProgress.alternate
  • workInProgress:当前组件对应的 Fiber 节点
  • renderLanes:优先级相关

2.1 mount 时

代码细节方面,mount 时,会根据 fiber.tag 的不同创建不同子 Fiber 节点:

2.2 update 时

如果是 update 时,如果 current 树不为空,那么可以复用之前的 current Fiber,比如克隆 current.childworkInProgress.child,这样就不需要重复创建:

2.3 reconcileChildren

reconcileChildren 方法是 Reconciler 的核心部分:
  • 对于 mount 的组件:创建子 Fiber 节点
  • 对于 update 的组件:对比当前和上一次更新的 Fiber 节点(Diff 算法),然后生成新的 Fiber 节点。
它在 beginWork 执行后调用,返回值为一个新的 Fiber 节点,最终都会给到 workInProgress.child,并作为下一次 performUnitOfWork 执行时的 workInProgress 传参。
reconcileChildFibers 方法执行后会为节点添加 effectTag 属性。
fiber.effectTag 保存了需要在 Renderer 中执行的 DOM 操作:

3. 归阶段(completeWork

调用 completeWork 方法处理 fiber 节点。如果该节点存在兄弟节点,则进入它的“递”阶段;如果不存在就进入父节点的“归”阶段,这样交错进行直到 rootFiber
在这个阶段,仍然会根据 fiber.tag 的不同执行不同的逻辑:
根据 current === null ? 判断 mount 还是 update,上面的 HostComponent 这种类型表示原生 DOM 元素,例如,如果一个 Fiber 节点是一个 <div> 元素的 HostComponent,那么这个节点的 stateNode 属性会指向相应的 DOM 元素。因此,不仅要考虑 current 是否为 null,还要考虑 fiber.stateNode 是否有值:

3.1 mount 时

递的时候,会根据 Fiber 节点生成 DOM 节点,将子孙 DOM 节点插入刚生成的 DOM 节点中,然后处理 props。

3.2 update 时

在 mount 时已经生成了对应的 DOM 节点,所以 update 阶段就不需要这步操作。需要做的是处理 props:
  • onClickonChange等回调函数的注册
  • 处理 style prop
  • 处理 DANGEROUSLY_SET_INNER_HTML prop
  • 处理 children prop
需要注意的是,在 updateHostComponent 内部有一句:
这意味着被处理过的(变化的) props 被挂在 fiber.updateQueue 上,后续在 commit 阶段就会被渲染在页面上。

3.3 appendAllChildren

在 mount 时有一个 appendAllChildren 方法,它的调用会不断将子孙 DOM 节点插入到当下的 DOM 节点中,因此当到达 rootFiber 时,就已经有了一个完整的离屏 DOM 树了,然后提交给 Renderer 渲染就行了。

4. effectList 与单向链表

在有了完整的 DOM 树,并且 props 也都被处理完毕,在进入 commit 阶段之前有一个问题。
在渲染时,要考虑到每个节点的 effectTag(递阶段追加上的执行的标记操作),如果还是递归的方式去查找带有该属性的标记节点就会陷入复杂的逻辑中。
React 在这块的处理办法是利用单向链表的方式:
在归阶段 Fiber 节点执行 completeWork 后,如果存在 effectTag 属性就会被保存在一条被称为effectList 的单向链表中。
这样,只要遍历整条链表就能执行所有的 effect 了。
effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。—— React 团队成员 Dan Abramov
圣诞树彩灯
圣诞树彩灯

5. commit 阶段(渲染页面)

在 render 阶段处理完所有的工作后,fiberRoot 传递给 commitRoot 方法,进入 commit 阶段。
与 render 阶段(异步可中断)不同,这个阶段是同步执行的,React 将会根据 rootFiber.firstEffect 以及每个 fiber 下的 updateQueue 属性来渲染。
  • rootFiber.firstEffect:保存了副作用的 effectList
  • fiber.updateQueue:保存了变化的 props
另外,除了上述属性的 DOM 操作,在这个阶段还会执行生命周期钩子、hook。

5.1 主要工作

这个阶段的主要工作:
  • 变量赋值、状态重制
  • before mutation 阶段(执行 DOM 操作前)
  • mutation 阶段(执行 DOM 操作)
  • layout 阶段(执行 DOM 操作后)
  • useEffect 相关处理、性能追踪、生命周期钩子和 hook

5.2 before mutation

在这个阶段,React 会执行一些生命周期方法。
遍历 effectList,调用 commitBeforeMutationEffects
  1. 处理 DOM 节点渲染/删除后的 autoFocusblur 逻辑
  1. 调用 getSnapshotBeforeUpdate 生命周期钩子
  1. 调度 useEffect
由于 React 15 和 React 16 架构上的不同,组件在渲染时可能会多次触发 componentWillXXX 生命周期钩子。React 提供了替代的生命周期钩子 getSnapshotBeforeUpdate
有源码如下:
scheduleCallback 方法属于 Scheduler 调度器模块,以某个优先级异步调度一个回调函数。
在回调函数中可以看到 flushPassiveEffects 方法会触发 useEffectflushPassiveEffects 方法会拿到 effectList 链表,之后会遍历执行链表,于是副作用就会触发。
为什么要异步调度?
  • 保证 DOM 更新和副作用顺序:在 React 的架构中,DOM 更新是在 Commit 阶段同步完成的。为了确保 DOM 完成更新之后再执行副作用,flushPassiveEffects 被设计成异步的,这样可以保证所有的 DOM 变更都已完成,然后才开始执行副作用。这避免了在中间状态执行副作用,从而确保副作用依赖的 DOM 状态是最新的。
  • 避免嵌套更新导致的无限循环:如果副作用同步执行且在执行过程中触发了新的状态更新,可能会导致嵌套的更新循环,特别是在副作用中调用了 setState。异步调度副作用可以有效避免这种情况,确保所有的更新在一个批次中完成后,再处理副作用。

5.3 mutation

遍历 effectList 执行,调用 commitMutationEffects
  1. 根据 ContentReset effectTag 重置文字节点
  1. 更新 ref
  1. 根据 effectTag 分别处理,其中 effectTag 包括(Placement | Update | Deletion | Hydrating)
简单来说,React 会将所有需要更新的 DOM 变更应用到真实的 DOM 上。

5.4 layout

遍历 effectList 执行,调用 commitLayoutEffects
  1. commitLayoutEffectOnFiber(调用 生命周期钩子hook 相关操作)
  1. commitAttachRef(赋值 ref)
  • 对于 commitLayoutEffectOnFiber
    • 根据 fiber.tag 的不同:如果是 ClassComponent,会通过 current === null? 区分是 mount 还是 update,调用 componentDidMount componentDidUpdate;如果是 FunctionComponent 相关类型,会调用 useLayoutEffect hook 的回调函数,调度 useEffect 的销毁与回调函数。
  • 对于 commitAttachRef:获取 DOM 实例,更新 ref
简单来说,该方法的主要工作就是根据 effectTag 调用不同的处理函数处理 Fiber 并更新 ref

6. 总结

在 render 阶段遍历组件树,调用每个组件的 render 方法,根据返回的 JSX 元素创建出对应的 Fiber 节点并连接起来,挂载和更新的阶段是不同的,对于更新的节点,只要状态更新、props 改变,就会触发计算生成新的树,期间如果有更高优先级的任务就会中断让位,通过调度器来确定在何时恢复 render 阶段的工作。
在提交到 Render 之前,就生成好了一个完整的 DOM 树,不过是在内存中的,通过 effectList 将副作用连接起来,commit 阶段主要通过遍历这个 effectList 来触发生命周期函数getSnapshotBeforeUpdate、渲染真实 DOM 以及触发 componentDidUpdatecomponentDidMount 生命周期钩子或处理相关副作用函数。
 
听说你还不会 Tailwind CSS(响应式篇)不要再说搞不清 React 架构了(上)
Eric 见嘉
Eric 见嘉
Less is more.
公告
type
status
date
slug
summary
tags
category
icon
password
💭
合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。

关于我
土木转行的前端开发工程师,陆续分享技术干货。
联系我
微信公众号:见嘉 Being Dev