You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
熟悉 JS 的同学肯定知道跟浏览器渲染帧相关的两个API:requestIdleCallback(浏览器空闲时调用,下文简称rIC)和requestAnimationFrame(每一帧绘制之前调用,下文简称rAF),两个 api 看似可以达到不占用主线程,优先浏览器渲染,不阻塞的效果,但是真的适用吗?很显然,都有缺陷。
varmaxSigned31BitInt=1073741823;// Times out immediatelyvarIMMEDIATE_PRIORITY_TIMEOUT=-1;// Eventually times outvarUSER_BLOCKING_PRIORITY_TIMEOUT=250;varNORMAL_PRIORITY_TIMEOUT=5000;varLOW_PRIORITY_TIMEOUT=10000;// Never times outvarIDLE_PRIORITY_TIMEOUT=maxSigned31BitInt;
letschedulePerformWorkUntilDeadline;if(typeoflocalSetImmediate==="function"){// Node.js 和 old IE.schedulePerformWorkUntilDeadline=()=>{localSetImmediate(performWorkUntilDeadline);};}elseif(typeofMessageChannel!=="undefined"){// DOM and Worker environments.// 由于setTimeout的4ms延迟,所以使用MessageChannelconstchannel=newMessageChannel();constport=channel.port2;channel.port1.onmessage=performWorkUntilDeadline;schedulePerformWorkUntilDeadline=()=>{port.postMessage(null);};}else{// 非浏览器环境使用setTimeoutschedulePerformWorkUntilDeadline=()=>{localSetTimeout(performWorkUntilDeadline,0);};}
React-Scheduler
前言
Scheduler
顾名思义就是一个调度器,负责React
中任务的调度。众所周知 JS 是单线程,通过task
和micro task
来调度任务的执行。核心概念分为三个:时间切片、任务切片和优先级调度
小顶堆
的数据结构(不过多介绍)选择
熟悉 JS 的同学肯定知道跟浏览器渲染帧相关的两个
API
:requestIdleCallback(浏览器空闲时调用,下文简称rIC)
和requestAnimationFrame(每一帧绘制之前调用,下文简称rAF)
,两个 api 看似可以达到不占用主线程,优先浏览器渲染,不阻塞的效果,但是真的适用吗?很显然,都有缺陷。rIC
兼容性太差,
Safari
直接不兼容...can i use requestIdleCallback
执行时间不一定:浏览器空闲时执行间隔为50ms,也就是 20FPS,一秒执行 20 次,这显然间隔太长了。
例如:持续滚动页面,这时执行的间隔时间就会非常不稳定
还有一点,当页面至于后台时,干脆不执行了...
rAF
rAF
是官方推荐用于做流畅动画的api
,所以它的回调执行在页面渲染更新前。执行顺序可能在宏任务(task
)前或者后(涉及到EventLoop
,篇幅问题不过多介绍)。rAF
在各个平台的浏览器表现不一。React
可能执行两次更新综上所述,React 团队打算自己实现一个策略,用于时间分片。最终使用
MessageChannel
实现执行顺序,
microTask > messageChannel > setTimeout
,messageChannel
为dom event
,所以优先级要大于setTimeout
为什么用
task
而不用microTask
?不用microTask
的原因是,microTask
将在页面更新前全部执行完,达不到将主线程还给浏览器的目的。task
就会执行所有microTask
,并且在这个过程中新增的microTask
都会一并执行,所以React
的渲染如果在microTask
中,无法中断,React
在中断渲染之后会检查是否还有任务,如果有就再次调度一个performConcurrentWorkOnRoot
,根据事件循环来看这时再有microTask
会立即执行,所以每次都会执行完全部任务,无法达到一个tick
执行一个task
的目的为什么不使用
setTimeout
?因为setTimeout(_,0)
即使设置为0
,还会有**4ms
的问题**。在MessageChannel
无法使用的时候,降级使用setTimeout
为什么不使用
postMessage
?因为postMessage
会因为持续的滚动等操作被阻塞住。浏览器会为了保证用户交互的响应,将四分之三的优先权给了鼠标键盘事件,其余的时间会交给其他的task
,所以就导致了持续的滚动阻塞了postMessage
,Vue 2.0.0-rc.7
有个issue就是描述这个问题的。预备知识点
Scheduler
被单独拆成一个包,放在React
项目中,目录为react/packages/scheduler/src/forks/Scheduler.js全局变量
根据优先级对应不同
timeout
全局函数
任务相关变量
简单来说,任务分为两个堆——
taskQueue
和timerQueue
,两个变量都是js数组形式
的小顶堆
,taskQueue
根据expirationTime
(过期时间)由小到大排序,timerQueue
根据startTime
(开始时间)由大到小排序taskQueue
和timerQueue
分别表示任务需要立刻执行和延迟执行,通过(startTime
>currentTime
)来判断任务是添加进taskQueue
中还是timerQueue
局部变量
任务对象的属性
函数
unstable_scheduleCallback
入口函数,分成四个部分
startTime
,如果有delay
就加上timeout
expirationTime
,创建任务对象(newTask
)startTime > currentTime
来判断是push
进timerQueue
中还是taskQueue
requestHostTimeout
这部分代码很简单,就是一个延时器
handleTimeout
判断当前是否有被调度的任务,如果有就取出
timeQueue
第一位,继续等待执行,如果没有就直接调度该任务advanceTimers
将
timerQueue
中已经过期了的任务插入到taskQueue
中以上是调度
timerQueue
的过程,其中执行的函数和调度taskQueue
中函数有重复,放在下面讲解requestHostCallback
入参为
flushWork
,将flushwork
赋值给全局变量,并且触发消息通知schedulePerformWorkUntilDeadline
对于设备环境做了兼容
MessageChannel
为例,执行schedulePerformWorkUntilDeadline
会触发performWorkUntilDeadline
执行,但是会放在下一轮事件循环中执行performWorkUntilDeadline
作为
postMessage
触发的回调,主要负责执行全局变量scheduledHostCallback
,通过返回值判定是否触发下一轮postMessage
flushWork
核心,负责执行 workLoop 并且返回执行结果,在执行结束后重置全局变量
workLoop
核心,任务循环,真正执行
callback
的地方。在执行callback
前后都会执行advanceTimers
,确保taskQueue
中任务的优先级总结
总结下大致流程:通过将任务划分成
立即执行
和延迟执行
两个堆,立即执行的堆中任务会通过MessageChannel
触发,延迟执行的堆会通过setTimeout
将任务延迟到对应事件后添加进taskQueue
中触发。每次 workLoop 都会从
taskQueue
中取出任务,执行任务,如果任务执行完之后还有剩余时间,则继续执行,直到没有剩余时间或者任务队列为空。如果5ms
到了,但是还有任务,则通过postMessage
开启下一轮workLoop。
达到让出主线程的能力The text was updated successfully, but these errors were encountered: