vue3 源码解读之 time slicing的使用方法

今天给大家带来一篇源码解析的文章,emm 是关于 vue3 的,vue3 源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈

不过今天我要解析的部分是已经被废除的 time slicing 部分,这部分源码曾经出现在 vue conf 2018 的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街

打包

阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要

vue3 使用的 rollup 进行打包,我们需要先对它进行改造

import cleanup from 'rollup-plugin-cleanup' plugins: [ cleanup() //增加了一个 cleanup 插件 tsPlugin, aliasPlugin, createReplacePlugin(isProductionBuild, isBunlderESMBuild, isCompat), ...plugins ],

增加 cleanup 插件主要目的是打包出无注释的文件

以上,是我个人阅读源码的习惯,我觉得注释和类型的作用就是碍眼的,所以先去掉再说

用例

我们在读源码之前,需要先实现一个正确用例,但是我读的这个版本的源码,还是 class 的,怎么办?

这个时候我们可以根据测试用例来猜测并给出代码

function block () { const start = performance.now() while (performance.now() - start < 2) { } } class Test extend Component { render (props) { block() return h('li', props.msg) } } class App extend Component { msg = '' render () { const list = [] for (let i = 0; i < 200; i++) { list.push(h(Test, { key: i, msg: this.msg })) } return [ h('input', { onInput: e => { this.msg = e.target.value } }), h('div',list) ] } }

很好,现在我们有了一个争取,简单的用例了,接下来就是一股脑调试

调试

由于我在 fre 中也实现了时间切片,所以我对它非常了解,我知道它的作用原理,所以我们直接搜索宏任务,哈,果然有

window.addEventListener('message', event => { if (event.source !== window || event.data !== key) { return; } flushStartTimestamp = getNow(); try { flush(); } catch (e) { handleError(e); } }, false); function flushAfterMacroTask() { window.postMessage(key, `*`); }

这段代码非常容易理解,就是在宏任务队列里执行了 flush 函数,继续

然后关键就来了

function flush() { let job; while (true) { job = stageQueue.shift(); if (job) { stageJob(job); } else { break; } { const now = getNow(); if (now - flushStartTimestamp > frameBudget && job.expiration > now) { break; // 此处为关键,意思是超过16ms,或者任务过期,跳出循环 } } } ... 以下代码省略...

上面的循环很关键,它做的事情很简单的,从 stageQueue 里出栈一个任务,然后执行 stateJob

stateJob 做的事情很简单,就是往 commitQueue 里 push 这个任务

function stageJob(job) { if (job.ops.length === 0) { currentJob = job; job.cleanup = job(); currentJob = null; commitQueue.push(job); //重点在这里 job.status = 2; } }

到目前为止,我们源码读了一丢丢,但是已经几乎读完了可以说

它的本质就是,在宏任务中,stageQueue 作为低优先级任务队列,不断的出栈,然后分批次(16ms 的阈值)入栈到 commitQueue 里

呼,其实如果不是写文章,就可以到此为止了,但是写文章为了凑字数嘛,我们继续

上面我们已经知道了两个队列,stageQueue 和 commitQueue,但是并不知道他们里面都是什么东西

是什么东西被调度的呢?打印一下,你就知道:

console.log(stageQueue,commitQueue)

得出的结果是

function mountComponentInstance(){...}

看名字就知道是组件挂载函数,当然组件更新和卸载的函数也是同理

到现在,我们也知道了参与调度的是组件挂载更新的函数,所以本质上,vue 的时间切片的基本单位是组件,也就是说,如果你的组件挂载需要一个小时,那你仍然要卡一小时

凑字数

剩下的内容纯属凑字数,就是除了核心调度之外的东西

比如 commitQueue 是操作 dom 的,那它咋个操作

function commitJob(job) { const { ops, postEffects } = job; for (let i = 0; i < ops.length; i++) { applyOp(ops[i]); // 重点在这里 } if (postEffects) { postEffectsQueue.push(...postEffects); } resetJob(job); job.status = 0; }

如上,拿到 ops,然后进行操作,我们看一下 ops 是啥就行了

[<div></div>, <li></li>, function CreactElement(){}]

凑合凑合,是个数组,包含了 dom 操作的方法和被操作的元素

然后这个过程是同步完成的,也就是所谓的高优先级任务,必须等到彻底收集完毕,才可以循环执行它

做完这个,postEffectQueue 主要是一些额外的副作用和清理工作,我实在凑字数无能,就不打印了

总结

最后我们用最直白的话,总结一下:

在宏任务队列中,不断的从 stageQueue 分批次(16ms)将组件的函数转移到 commitQueue 里,转移完了,同步操作 dom

原理其实还是利用了宏任务队列,其实现在 vue 的做法和 fre 也有一点点类似,fre 是在宏任务中,尽可能更多的去访问 reconcile 大循环

关于废除

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/eab92555eb02ea3dd5c8c598ee45da5a.html