前言
GUI 渲染线程和 JS 引擎线程是相互排斥的,比如开发者用 js 写了一个遍历大量数据的循环,在执行 js 时候,会阻塞浏览器的渲染绘制,给用户直观的感受就是卡顿
为什么采用异步调度?
v15 版本面临卡顿的问题
由于对于大型的 React 应用,会存在一次更新,递归遍历大量的虚拟 DOM ,造成占用 js 线程,使得浏览器没有时间去做一些动画效果,伴随项目越来越大,项目会越来越卡
如何解决?
vue 有着 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图。
在 React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选择从根节点开始 diff ,查找不同,更新这些不同。
React 将更新交给浏览器自己控制:如果浏览器有绘制任务那么执行绘制任务,在空闲时间执行更新任务,就能解决卡顿问题了。
与 vue 更快的响应,更精确的更新范围,React 选择更好的用户体验。
说一说React 的时间分片?
如何让浏览器控制 React 更新呢
浏览器每次执行一次事件循环(一帧)都会做如下事情:**处理事件、执行 js ,调用 requestAnimation ,布局 Layout ,绘制 Paint
**。
在一帧执行后,如果没有其他事件,那么浏览器会进入休息时间。那么有的一些不是特别紧急的 React 更新,就可以执行了。
如何知道浏览器有空闲时间?
requestIdleCallback
谷歌浏览器提供的一个 API, 在浏览器有空余的时间,浏览器就会调用 requestIdleCallback 的回调
requestIdleCallback(callback,{ timeout })
callback 回调,浏览器空余时间执行回调函数。
timeout 超时时间。如果浏览器长时间没有空闲,那么回调就不会执行,为了解决这个问题,可以通过 requestIdleCallback 的第二个参数指定一个超时时间。
5 个优先级
防止 requestIdleCallback 中的任务由于浏览器没有空闲时间而卡死
Immediate -1 需要立刻执行
UserBlocking 250ms 超时时间250ms,一般指的是用户交互
Normal 5000ms 超时时间5s,不需要直观立即变化的任务,比如网络请求
Low 10000ms 超时时间10s,肯定要执行的任务,但是可以放在最后处理
Idle 一些没有必要的任务,可能不会执行
异步更新任务描述
通过类似 requestIdleCallback 去向浏览器做一帧一帧请求,等到浏览器有空余时间,去执行 React 的异步更新任务,这样保证页面的流畅
React 如何模拟 requestIdleCallback?
requestIdleCallback 目前只有谷歌浏览器支持 ,react 为了兼容每个浏览器 自己模拟了这个 api
实现 requestIdleCallback 具备的条件
- 可以主动让出主线程,让浏览器去渲染视图
- 一次事件循环只执行一次,因为执行一个以后,还会请求下一次的时间片
能够满足条件的,就只有 宏任务 宏任务是在下次事件循环中执行,不会阻塞浏览器更新;而且浏览器一次只会执行一个宏任务
MessageChannel 接口
介绍
允许开发者创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据
MessageChannel.port1 只读返回 channel 的 port1
MessageChannel.port2 只读返回 channel 的 port2
模拟如何触发异步宏任务?
1 | let scheduledHostCallback = null |
在一次更新中,React 会调用 requestHostCallback ,把更新任务赋值给 scheduledHostCallback ,然后 port2 向 port1 发起 postMessage 消息通知
port1 会通过 onmessage ,接受来自 port2 消息,然后执行更新任务 scheduledHostCallback ,然后置空 scheduledHostCallback ,借此达到异步执行目的
React 为什么不用 settimeout ?
为了让视图流畅地运行,可以按照人类能感知到最低限度每秒 60 帧的频率划分时间片,这样每个时间片就是 16ms 。16 毫秒要完成如上 js 执行,浏览器绘制等操作。递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右
异步调度原理?
ensureRootIsScheduled
React 发生一次更新,会统一走 ensureRootIsScheduled(调度应用)
对于正常更新会走 performSyncWorkOnRoot 逻辑,最后会走 workLoopSync
对于低优先级的异步更新会走 performConcurrentWorkOnRoot 逻辑,最后会走 workLoopConcurrent
在一次更新调度过程中,workLoop 会执行每一个待更新的 fiber 。这样就解决了一次性遍历大量的 fiber ,导致浏览器没有时间执行一些渲染任务,导致了页面卡顿。
他们的区别就是异步模式会调用一个 shouldYield() :如果当前浏览器没有空余时间, shouldYield 会中止循环,直到浏览器有空闲时间后再继续遍历,从而达到终止渲染的目的
scheduleCallback
同步、异步任务进入调度器的区别
低优先级异步任务的处理,比同步多了一个超时等级的概念,会计算五种超时等级
对于正常更新任务,最后会变成类似如下结构:
1 | scheduleCallback(Immediate,workLoopSync) |
对于异步任务:
1 | /* 计算超时等级,就是如上那五个等级 */ |
相关概念
taskQueue
里面存的都是过期的任务
依据任务的过期时间( expirationTime ) 排序
需要在调度的 workLoop 中循环执行完这些任务
timerQueue
里面存的都是没有过期的任务
依据任务的开始时间( startTime )排序
在调度 workLoop 中 会用 advanceTimers 检查任务是否过期:如果过期了,放入 taskQueue 队列
做了什么?
创建一个新的任务 newTask
通过任务的开始时间( startTime ) 和 当前时间( currentTime ) 比较
当 startTime > currentTime,说明未过期,存到 timerQueue
当 startTime <= currentTime,说明已过期,存到 taskQueue
如果任务过期,并且没有调度中的任务,那么调度 requestHostCallback。本质上调度的是 flushWork
如果任务没有过期,用 requestHostTimeout 延时执行 handleTimeout
1 | function scheduleCallback(){ |
requestHostTimeout
通过 setTimeout 来进行延时指定时间
handleTimeout
会把任务重新放在 requestHostCallback 调度
1 | function handleTimeout(){ |
通过 advanceTimers 将 timeQueue 中过期的任务转移到 taskQueue 中,然后调用 requestHostCallback 调度过期的任务
advanceTimers
如果任务已经过期,那么将 timerQueue 中的过期任务,放入 taskQueue
1 | function advanceTimers(){ |
综上:React 的更新任务最后都是放在 taskQueue 中的。requestHostCallback ,放入 MessageChannel 中的回调函数是flushWork
flushWork
如果有延时任务执行的话,那么会先暂停延时任务,然后调用 workLoop ,去真正执行超时的更新任务
1 | function flushWork(){ |
workLoop
会依次更新过期任务队列中的任务。到此为止,完成整个调度过程。
1 | function workLoop(){ |
shouldYield 中止 workloop
在 fiber 的异步更新任务 workLoopConcurrent 中,每一个 fiber 的 workloop 都会调用 shouldYield 判断是否有超时更新的任务,如果有,那么停止 workLoop
1 | function unstable_shouldYield() { |
如果存在第一个任务,并且已经超时了,那么 shouldYield 会返回 true,那么会中止 fiber 的 workloop