深入学一下 React (五) 原理篇 调度

93 阅读3分钟

调度 与 时间分片

为什么采用异步调度?

  • vue 有这 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图。
  • 在 React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选择从根节点开始 diff ,查找不同,更新这些不同。
    要让浏览器有绘制任务那么执行绘制任务,在空闲时间执行更新任务,就能解决卡顿问题了

时间分片

  • 不用setTimeOut(fn, 0)的原因:连续用会间隔4ms,一帧才16ms,太浪费了
  • 不直接用requestIdleCallback的原因:万一浏览器半天都不闲着呢?

React设置的几个优先级:

  • Immediate -1 需要立刻执行。
  • UserBlocking 250ms 超时时间250ms,一般指的是用户交互。
  • Normal 5000ms 超时时间5s,不需要直观立即变化的任务,比如网络请求。
  • Low 10000ms 超时时间10s,肯定要执行的任务,但是可以放在最后处理。
  • Idle 一些没有必要的任务,可能不会执行。

模拟requestIdleCallback

为了兼容每个浏览器,React需要自己实现一个 requestIdleCallback ,那么就要具备两个条件

  1. 实现的这个 requestIdleCallback ,可以主动让出主线程,让浏览器去渲染视图。
  2. 一次事件循环只执行一次,因为执行一个以后,还会请求下一次的时间片。

既然setTimeOut(fn, 0)不符合要求,那么就使用MessageChannel

  • MessageChannel.port1 只读返回 channel 的 port1 。
  • MessageChannel.port2 只读返回 channel 的 port2 。
  • 在一次更新中,React 会调用 requestHostCallback ,把更新任务赋值给 scheduledHostCallback ,然后 port2 向 port1 发起 postMessage 消息通知。
  • port1 会通过 onmessage ,接受来自 port2 消息,然后执行更新任务 scheduledHostCallback ,然后置空 scheduledHostCallback ,借此达到异步执行目的。

scheduleCallback

具体的请看这里

无论是上述正常更新任务 workLoopSync 还是低优先级的任务 workLoopConcurrent ,都是由调度器 scheduleCallback 统一调度的

  • 低优先级异步任务的处理,比同步多了一个超时等级的概念。会计算上述那五种超时等级。

对于调度本身,有几个概念必须掌握。

  • taskQueue,里面存的都是过期的任务,依据任务的过期时间( expirationTime ) 排序,需要在调度的 workLoop 中循环执行完这些任务。
  • timerQueue 里面存的都是没有过期的任务,依据任务的开始时间( startTime )排序,在调度 workLoop 中 会用advanceTimers检查任务是否过期,如果过期了,放入 taskQueue 队列。

scheduleCallback 流程如下。

  • 创建一个新的任务 newTask。
  • 通过任务的开始时间( startTime ) 和 当前时间( currentTime ) 比较:当 startTime > currentTime, 说明未过期, 存到 timerQueue,当 startTime <= currentTime, 说明已过期, 存到 taskQueue。
  • 如果任务过期,并且没有调度中的任务,那么调度 requestHostCallback。本质上调度的是 flushWork。
  • 如果任务没有过期,用 requestHostTimeout 延时执行 handleTimeout。

requestHostTimeout 就是通过 setTimeout 来进行延时指定时间的。)

调度流程图调度流程图
调和 + 异步调度 流程总图调和 + 异步调度 流程总图