跳到主要内容

工作流

React 更新流程

schedule(调度器) 负责 维护时间切片 与浏览器任务调度 优先级调度

reconciler(协调器)负责 将 JSX 转化为 fiber fiber 树对比(双缓存)+ 增量 diff 确定本次更新的 fiber

renderer(渲染器)(renderDOM, renderSSR) 用于管理一颗 react 树 使其根据底层平台不同,用不同方式调用,实现跨端

schedule 通知 reconciler,reconcile 阶段得到新的 fiber 树后通知 render render 结果中如果 state 更新,通知 schedule,schedule 决定分不分时间切片和优先级

总结

渲染有两个阶段:Reconciliation(协调阶段) 和 Commit(提交阶段)。

首先将 State 转换为页面结构(虚拟 DOM 结构)后,再转换成真实 DOM 结构,交给浏览器渲染。当 State 发生改变时,React 会先进行调和(Reconciliation)阶段,调和阶段结束后立刻进入提交(Commit)阶段,提交阶段结束后,新 State 对应的页面才被展示出来。

reconcil 阶段

计算出目标 State 对应的虚拟 DOM 结构。主要是调用类组件的 render 方法或函数组件自身

寻找「将虚拟 DOM 结构修改为目标虚拟 DOM 结构」的最优更新方案。主要为 React 内部实现的 Diff 算法,Diff 算法会记录虚拟 DOM 的更新方式如:Update、Mount、Unmount,为提交阶段做准备。

React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch),放到更新队列里面去。

可以认为是 Diff 阶段, 这个阶段可以被中断,这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更 React 称之为'副作用(Effect)' . 以下生命周期钩子会在协调阶段被调用:

  • constructor
  • componentWillMount~~ 废弃~~
  • componentWillReceiveProps~~ 废弃~~
  • static getDerivedStateFromProps
  • shouldComponentUpdate
  • componentWillUpdate~~ 废弃~~
  • render

协调阶段发生的事情不会导致到 dom 的改变(等待 update 阶段),也就是说不会影响到用户的体验

flags

在 reconcil (调和)阶段,给 fiber 打了很多的 flags(标记),commit 阶段是会读取这些 flags 进行不同的操作的。

flags 是通过二进制掩码的方式来保存的,掩码优点是节省内存,缺点是可读性很差。

使用或位运算,可以将多个 flag 组合成一个组。

这三个阶段 用到的组合掩码 为:

export const BeforeMutationMask = Update | Snapshot;

export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;

export const LayoutMask = Update | Callback | Ref | Visibility;

commit 阶段

将调和阶段记录的更新方案应用到 DOM 中。 将上一个阶段计算出来的需要处理副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断.

调用暴露给开发者的钩子方法,如:componentDidUpdate、useLayoutEffect 等。

这些生命周期钩子在提交阶段被执行:

  • getSnapshotBeforeUpdate() 严格来说,这个是在进入 commit 阶段前调用
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount 执行 hooks
警告

需要注意的是:因为协调阶段可能被中断、恢复,甚至重做,React 协调阶段的生命周期钩子可能会被调用多次!, 例如 componentWillMount 可能会被调用两次。

17 移除 reconcile 阶段钩子的原因:为协调阶段中断处理做让步 而开发者经常在 will 钩子中执行副作用,副作用不应该执行多次 因此建议协调阶段的生命周期钩子不要包含副作用. 索性 React 就废弃了这部分可能包含副作用的生命周期方法,例如 componentWillMount、componentWillUpdate. v17 后我们就不能再用它们了, 所以现有的应用应该尽快迁移. 我们要正确地处理各种副作用,包括 DOM 变更、还有你在 componentDidMount 中发起的异步请求、useEffect 中定义的副作用... 因为有副作用,所以必须保证按照次序只调用一次,况且会有用户可以察觉到的变更, 不容差池。

commit 分成三个阶段:BeforeMuation、Muation 以及 Layout 阶段。

  • BeforeMuation,没做太多事,主要是类组件实例调用 getSnapshotBeforeUpdate 生成快照对象保存起来;

mutation 阶段是最重要的阶段,在这个阶段,React 真正地更新了文档 DOM 树

  • Muation,更新 DOM 的阶段,做了删除、插入、更新操作。

    -(1)删除逻辑:重置 ref 为 null,根据 fiber.deletions 删除 DOM 节点,调用类组件的 componentWillUnmount,调用 useInsertionEffect 和 useLayoutEffect 的 destory 方法

    -(2)插入逻辑:将标记了 Place 的节点进行真实 DOM 的插入
    -(3)对比 props 更新 DOM 节点,调用 useInsertionEffect 的 destroy 、useInsertionEffect 的 create 和 useLayoutEffect 的 destroy;

  • Layout,调用类组件的 componentDidMount、componentDidUpdate、setState 的回调函数;调用函数组件 useLayoutEffect 的 create;最后更新 ref。 useLayoutEffect 的执行时机与 componentDidMount 相同

useEffect 不在同步的 commit 阶段中执行。它是异步的,被 scheduler 异步调度执行 最后是 commit 阶段外的 useEffect,它被 Scheduler 异步调度执行,先执行完整棵树的 destroy,再执行完整棵树的 create。