一直对时间切片非常感兴趣,虽然最新的vue-next中剔除了时间切片,但是这里还是可以借鉴下其中的原理和思想:
首先要先知道javascript的执行机制,javascript的任务分为macro-task宏任务和micro-task微任务,宏任务主要为同步代码,settimeout,setInterval等,微任务为promise,process.nextTick等。网上有一张图能比较清楚的说明两者相互间的关系:
大致明白了宏任务和微任务,理解时间切片就简单多了。切入代码可以先从测试文件开始:
it('queueJob', async () => {
const calls: any = []
const job1 = () => {
calls.push('job1')
}
const job2 = () => {
calls.push('job2')
}
queueJob(job1)
queueJob(job2)
expect(calls).toEqual([])
await nextTick()
expect(calls).toEqual(['job1', 'job2'])
})
这里使用了2个函数,queueJob和nextTick,queueJob源码如下:
export function queueJob(rawJob: Function) {
console.log(rawJob)
const job = rawJob as Job
if (currentJob) {
currentJob.children.push(job)
}
// Let's see if this invalidates any work that
// has already been staged.
if (job.status === JobStatus.PENDING_COMMIT) {
// staged job invalidated
invalidateJob(job)
// re-insert it into the stage queue
requeueInvalidatedJob(job)
} else if (job.status !== JobStatus.PENDING_STAGE) {
// a new job
queueJobForStaging(job)
}
if (!hasPendingFlush) {
hasPendingFlush = true
flushAfterMicroTask()
}
}
其中rawJob就是我们传入queueJob的匿名函数,通过执行queueJob函数首先会将他推入stageQueue队列,执行queueJob(job1)时,hasPendingFlush为false,则执行flushAfterMicroTask函数,后续的queueJob则不会flushAfterMicroTask。flushAfterMicroTask源码如下:
function flushAfterMicroTask() {
flushStartTimestamp = getNow()
return p.then(flush).catch(handleError)
}
代码很简单,就是将在flush函数放入微任务队列中,当宏任务执行完成,就会执行flush函数,简单来说,就是先将要执行的一组任务推入stageQueue,执行完成后执行微任务即执行一次flush。很显然,flush就是时间切片的关键,源码如下:
function flush(): void {
let job
while (true) {
// console.log(stageQueue)
job = stageQueue.shift()
// console.log(job)
if (job) {
stageJob(job)
} else {
break
}
if (!__COMPAT__) {
const now = getNow()
if (now - flushStartTimestamp > frameBudget && job.expiration > now) {
break
}
}
}
if (stageQueue.length === 0) {
// all done, time to commit!
for (let i = 0; i < commitQueue.length; i++) {
commitJob(commitQueue[i])
}
commitQueue.length = 0
flushEffects()
// some post commit hook triggered more updates...
if (stageQueue.length > 0) {
if (!__COMPAT__ && getNow() - flushStartTimestamp > frameBudget) {
return flushAfterMacroTask()
} else {
// not out of budget yet, flush sync
return flush()
}
}
// now we are really done
hasPendingFlush = false
pendingRejectors.length = 0
for (let i = 0; i < nextTickQueue.length; i++) {
nextTickQueue[i]()
}
nextTickQueue.length = 0
} else {
// got more job to do
// shouldn't reach here in compat mode, because the stageQueue is
// guarunteed to have been depleted
flushAfterMacroTask()
}
}
这段代码其实就干两件事,首先stageQueue不断出栈,然后通过stageJob函数推入commitQueue队列,stageJob源码如下:
function stageJob(job: Job) {
// job with existing ops means it's already been patched in a low priority queue
if (job.ops.length === 0) {
currentJob = job
job.cleanup = job()
currentJob = null
commitQueue.push(job)
job.status = JobStatus.PENDING_COMMIT
}
}
当超过某个时长或者任务过去则跳出循环。这里frameBudget为16ms左右。之后就是判断如果stageQueue出栈完了都进入commitQueue队列了,则执行commitQueue队列里的job,在vue中,这些job就是挂载组件,副作用更新组件等。如果stageQueue没有清空,那么执行flushAfterMacroTask函数,flushAfterMacroTask函数源码如下:
function flushAfterMacroTask() {
window.postMessage(key, `*`)
}
window.addEventListener(
'message',
event => {
if (event.source !== window || event.data !== key) {
return
}
flushStartTimestamp = getNow()
try {
flush()
} catch (e) {
handleError(e)
}
},
false
)
能看出来接下来就是在宏任务中不断地执行flush函数,直到stageQueue为空,然后执行commitQueue队列中的job。
讲到这里其实关于时间切片的原理也大致说清楚了,Vue通过queueJob函数,将需要组件挂载和更新的job推入stageQueue队列,然后再之后的宏任务中调用flush函数,不停地将stageQueue出栈,推入commitQueue队列,最后执行job。
通过这样的方法,可以减少页面的阻断,每次数据更新将其推入stageQueue队列,并不会立即执行,可以看出对于高帧频的操作,有着明显的效果。
个人认为好像也就能针对高帧频的操作会有效果,也许这就是被废除的原因吧。
通过vue的时间切片,我们可以尝试写一个建议版的,执行代码如下:
test
每次输入input则在div中插入200条数据,这里使用时间切片,可以发现input连着输入时不会卡顿,但是当稍有停顿在输入时明显卡顿,通过代码可以看出卡顿的原因在于,stageQueue为空执行commitQueue中的job时,其实是同步操作,这个时候肯定会造成阻塞,这也是vue废除这个特性的原因,因为vue的渲染都是以组件单最小单位的,组件渲染时间长卡顿的问题通过时间切片无法解决。个人认为这个功能和节流防抖差不多。当然如果是高帧频操作,time slicing对于性能还是有很大帮助的,我们这里可以尝试不同时time scling 运行这段html:
test
当连续输入时,卡炸了。