任务调度系统
操作系统中调度
操作系统中,基于时间片调度,如果到了时间该程序还没有运行完毕,那么此时会有中断,将该程序的相关运行位置,以及上下文环境等存入相关的寄存器中,之后进行恢复。
浏览器中调度
我们在浏览器中,也可以近似的模拟出调度算法,但是js程序必须自己守时,如果到了时间不自己退出,或者是长时间占用主线程,那么卡顿是必然的,所以在浏览器中必须来手动尽可能的控制程序的运行。
浏览器中目前提供的方式
在一个EventLoop,如果当前这个循环需要更新,js代码-requestAnimationFrame-UI渲染(layout, paint)-requestIdleCallback(0个或多个)-macroTask
requestIdleCallback
当浏览器运行时,主线程有空闲的时间,那么就会去运行回调,但是浏览器空闲下来,谁也不能保证接下来浏览器会发生什么,会不会有优先级更高的任务进来,如用户点击的交互事件,或者是动画事件,相关的研究发现,人在50ms内是不太容易察觉出变化来的,所以如果当前线程空闲(用户不操作,或者当前并没什么操作进行)当前requestIdleCallback会设定<50ms执行时间。
requestIdleCallback(fn, timeout?)
- fn最终回调执行
fn(deadline),deadline.timeout是否超时,deadline.timeRemaining()剩余时间,>=0, 到0表示已经执行超时了。 - timeout,表示超时时间,因为
rIC并不一定会执行,如果浏览器长时间忙碌,那么他会在timeout超时下强制执行
React的polyfill
React@16.6.x
React团队考虑到浏览器兼容问题,并没有使用rIC,而是使用rAF+setTimeout来进行polyfill
rAF,requestAnimationFrame用来在每个渲染时,发出执行的信号
并且其调用callback时,会传入类似当前performace.now,他使用计算屏幕的刷新率(多次rAF间隔)计算出下一次渲染的deadline当然这里包括了UI线程的时间,deadline与当前时间now可近似算出,这一帧还有多少的执行时间。
-
setTimeout同样是为了防止该任务在浏览器繁忙时,多次得不到执行而设定的一个超时 -
在
rAF中发出postMessage,UI渲染结束后,调用macroTask,监听message回调的事件会执行,并通过上下文计算是否超时
使用rIC(requestIdleCallback)小例子
使用chrome时,可在
Performance调低性能,效果较为明显。
执行一定数量的console.log操作,使用同步异步来执行
模拟浏览器忙碌的情况
使用一个css动画模拟浏览器的忙碌情况,从而更好的测试性能,我们创建了一个左右移动的box
<style>
/* 使用margin移动,更好验证效果,如果做动画,transform还是首选 */
@keyframes slide {
0% {
margin-left: 0;
/* transform: translateX(0); */
}
50% {
margin-left: 200px;
/* transform: translateX(200px); */
}
100% {
margin-left: 0;
/* transform: translateX(0); */
}
}
.box {
width: 400px;
height: 200px;
animation-duration: 3s;
animation-name: slide;
animation-iteration-count: infinite;
background: red;
}
</style>
<div class="box"></box>
同步
const TEST_SIZE = 1000
function performSync () {
const arr = new Array(TEST_SIZE)
let i = 0;
console.time('push')
// 放入数组中
for (; i < TEST_SIZE; i++) {
arr[i] = i
}
i = 0
console.timeEnd('push') // push: 0.41015625ms
console.time('sync-log')
// 打印
for (; i < TEST_SIZE; i++) {
console.log(arr[i])
}
i = 0
console.timeEnd('sync-log') // sync-log: 2690.320068359375ms
}
可以看到,执行performSync时,浏览器动画发生了明显的卡顿,也即js占用时间过长,导致后面出现掉帧
异步
我们这里存取数据时,需要使用链表进行操作了,可能会涉及到频繁的增添元素,
function performAsync () {
let deadline = null
let firstCallbackNode = null
let lastCallbackNode = null
const arr = new Array(TEST_SIZE)
let i = 0
console.time('push')
// 放入链中
let obj = {}
for (; i < TEST_SIZE; i++) {
obj = { next: null, payload: i }
if (firstCallbackNode === null) {
lastCallbackNode = firstCallbackNode = obj
}else {
lastCallbackNode = lastCallbackNode.next = obj
}
}
i = 0
console.timeEnd('push') // push: 1.015869140625ms
// 执行链表中的任务
function flushWork(callback) {
while (deadline.timeRemaining() > 0) {
// 结束任务
if (firstCallbackNode === null) {
lastCallbackNode = null
return
}
callback(firstCallbackNode.payload)
firstCallbackNode = firstCallbackNode.next
}
}
// 进行调度
function scheduleWork(deadlineObj) {
deadline = deadlineObj
// 结束
if (firstCallbackNode === null) {
deadline = null
console.timeEnd('async-log') // async-log: 7676.759033203125ms
return
}
// 下一次继续调度
requestIdleCallback(scheduleWork)
// 当前执行刷新任务
flushWork(console.log)
}
console.time('async-log')
// 启动调度
requestIdleCallback(scheduleWork)
}
可以看到在执行performAsync时,动画执行依旧比较流畅,当然相应所耗费时间还是增加的。
结论
浏览要干的活是一定的,你让他一下干完,时间很少,但是发生了明显的卡顿(他自己的活来不及干了)。让他空闲的时候执行,动画较为流畅,执行任务所需的时间也久了(给你干活的时间少了)
React团队同时引进了ConcurrentMode意为给任务增加不同的优先级,这样能够更好的调度,当然他采用“懒”策略,能不执行就不执行,等他要超时了,赶紧执行。保证了有大量间隙时间给用户交互,但是任务越积越多,到最后“一大笔帐”要算的时候,还是会出现卡顿。
文中如有欠妥的地方,欢迎指正。