Skip to content

React 流程

观看文档: https://7km.top/main/bootstrap

时间切片原理

消费任务队列的过程中, 可以消费1~n个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用.

如何判断超时:当前时间-任务开始时间 > 时间切片(5ms)

可中断渲染原理

在时间切片的基础之上, 如果单个任务的回调函数(task.callback)执行时间就很长(假设 200ms). 就需要task.callback自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时, 如遇超时就退出fiber树构造循环, 返回一个新的回调函数注册一个任务,等待下一次回调继续未完成的fiber树构造.

beginWork

  1. 根据  ReactElement对象创建fiber节点,调用reconcileChildren生成 Fiber 子节点,在reconcileChildren函数中实现了 diff 算法,决定哪些节点可复用
  2. 设置fiber.flags(二进制形式变量, 用来标记  fiber节点 的增,删,改状态, 等待completeWork阶段处理)

completeWork

  1. fiber节点创建 DOM 实例,为 DOM 节点设置属性
  2. 识别beginWork阶段设置的fiber.flags, 判断当前  fiber  是否有副作用(增,删,改), 如果有, 需要将当前  fiber  加入到父节点的effects队列, 等待commit阶段处理.

commitRoot

整个渲染逻辑都在 commitRoot 分三个阶段

  1. Before Mutation Phase(突变前阶段) dom 变更之前, 处理副作用队列中带有Snapshot,Passive标记的fiber节点。
    • Snapshot:表示需要调用 getSnapshotBeforeUpdate,以获取 DOM 变更前的快照数据。
    • Passive:表示需要调用 useEffect 的 cleanup 函数,清理上一次渲染的副作用。
  2. Mutation Phase(突变阶段) dom 变更, 界面得到更新. 处理副作用队列中带有PlacementUpdateDeletion标记的fiber节点,创建、更新、或删除 DOM 元素
  3. Layout Phase(布局阶段) 确保在 DOM 更新完成后执行所有需要依赖于最新布局的副作用和回调

其它知识

为什么不用 generator 做调度器

官方解释

  1. 使用 generator 必须将每个函数都包裹在生成器中。这不仅增加了大量的语法开销,而且增加了运行时开销
  2. generators are stateful. You can't resume in the middle of it.

三个昂贵工作 ABC 被分配在不同时间切片中执行且有上下文依赖

假设有这么一种情况,执行完了 A、B,但还没执行 C,但 B 此时有一个更新,那么我没法重用 A 执行的结果,也就是 A 执行完毕后的结果并没有被缓存到

官方解释:React 没法在这里去注入缓存,如果用全局缓存的话,最终也会遇到缓存失效的问题

为什么要用 MessageChannel 做时间切片

首先必须得是一个宏任务

在实际执行中,首次执行同步代码会作为一次宏任务,因此后续的顺序可以看作:

执行微任务队列 => 执行 RAF 回调(若要执行渲染) => 渲染(若要执行渲染) => 下一个任务

Scheduler 需要满足以下功能点:

  1. 将主线程还给浏览器,让浏览器有机会更新页面
  2. 在未来某个时刻继续调度任务,执行上次还没有完成的任务

也就是说我们需要一个宏任务,因为宏任务在渲染后的下一帧,不会阻塞本次循环.

使用 MessageChannel 的目的就是为了产生宏任务

  • 为什么不使用 setTimeout(fn, 0) 呢?

递归的 setTimeout() 调用会使调用间隔变为 4ms,导致浪费了 4ms。

  • 为什么不使用 rAF() 呢?

如果上次任务调度不是 rAF() 触发的,将导致在当前帧更新前进行两次任务调度。 页面更新的时间不确定,如果浏览器间隔了 10ms 才更新页面,那么这 10ms 就浪费了。