今天给大家带来一篇源码解析的文章,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 大循环
关于废除
如开头提到的,time slicing 这部分内容已经在 master 分支被移除了,关于为什么废除,我特地发了 issue,可以戳这里:(天啊,我和尤终于可以和平地进行交谈了)
https://github.com/vuejs/rfcs/issues/89
简单说,就是 time slicing 的收益不大,除了 issue 中提到的,它本身的场景就少的可怜
也因为 vue 现在的实现,由于调度的基本单位是组件,所以它仍然会因为组件内部的逻辑而被阻断
比如我把用例中用于阻断的 block 函数改为 1s,就已经彻底卡死了
思考
从 issue 和源码本身,我们可以思考一些问题,同时用来凑字数
时间切片是否必须?
答案是否定的,尤的回复已经足够充分了:https://github.com/vuejs/rfcs/issues/89#issuecomment-546988615
大致有两点:
那,fre 呢?
fre 的异步渲染,是否也存在这个问题,不得不承认,fre 虽然粒度很小,对于组件内部的阻断可以搞定,但是元素本身也可以被阻断
而且第一个问题也是存在的,就是没有太多适用场景
但是 fre 源码层面还是意义重大的,即便这玩意搞出来,发现它作用不大,副作用不小,但 fre 作为我个人的学习和研究的项目,它的价值从来就不是业务层面的
只是我应该停下来,异步渲染搞定了,只是向大家展示它的源码实现,未来不应该跟随 react 去搞一堆业务 API,如 useTransition 等等
关于源码?
vue3 发版当天,源码解读就放出了,但是到目前为止,所有的源码解读统统都是蹭热度的
不久的将来,vue 的源码又要烂大街了……
这种现象引起反省,我们读源码到底是为了什么?为了面试吗?为了更好的写业务?
对我而言,仅仅只是感兴趣,我对这部分源码感兴趣,我就去读,并且只读感兴趣的部分
其实大家也看到了,我很少写源码解读的文章,因为我一直反对所谓的【通读源码】
将阅读源码作为一项工作,同样的小函数,读了一遍又一遍,重复劳动
这和糊 shi 有什么区别呢?
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!