vue3 源码解读之 time slicing的使用方法
今天给大家带来一篇源码解析的文章,emm是关于vue3的,vue3源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈
不过今天我要解析的部分是已经被废除的timeslicing部分,这部分源码曾经出现在vueconf2018的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街
打包
阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要
vue3使用的rollup进行打包,我们需要先对它进行改造
importcleanupfrom'rollup-plugin-cleanup' plugins:[ cleanup()//增加了一个cleanup插件 tsPlugin, aliasPlugin, createReplacePlugin(isProductionBuild,isBunlderESMBuild,isCompat), ...plugins ],
增加cleanup插件主要目的是打包出无注释的文件
以上,是我个人阅读源码的习惯,我觉得注释和类型的作用就是碍眼的,所以先去掉再说
用例
我们在读源码之前,需要先实现一个正确用例,但是我读的这个版本的源码,还是class的,怎么办?
这个时候我们可以根据测试用例来猜测并给出代码
functionblock(){ conststart=performance.now() while(performance.now()-start<2){ } } classTestextendComponent{ render(props){ block() returnh('li',props.msg) } } classAppextendComponent{ msg='' render(){ constlist=[] for(leti=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); functionflushAfterMacroTask(){ window.postMessage(key,`*`); }
这段代码非常容易理解,就是在宏任务队列里执行了flush函数,继续
然后关键就来了
functionflush(){ letjob; while(true){ job=stageQueue.shift(); if(job){ stageJob(job); } else{ break; } { constnow=getNow(); if(now-flushStartTimestamp>frameBudget&&job.expiration>now){ break;//此处为关键,意思是超过16ms,或者任务过期,跳出循环 } } } ...以下代码省略...
上面的循环很关键,它做的事情很简单的,从stageQueue里出栈一个任务,然后执行stateJob
stateJob做的事情很简单,就是往commitQueue里push这个任务
functionstageJob(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)
得出的结果是
functionmountComponentInstance(){...}
看名字就知道是组件挂载函数,当然组件更新和卸载的函数也是同理
到现在,我们也知道了参与调度的是组件挂载更新的函数,所以本质上,vue的时间切片的基本单位是组件,也就是说,如果你的组件挂载需要一个小时,那你仍然要卡一小时
凑字数
剩下的内容纯属凑字数,就是除了核心调度之外的东西
比如commitQueue是操作dom的,那它咋个操作
functioncommitJob(job){ const{ops,postEffects}=job; for(leti=0;i如上,拿到ops,然后进行操作,我们看一下ops是啥就行了
[,,functionCreactElement(){}]
凑合凑合,是个数组,包含了dom操作的方法和被操作的元素
然后这个过程是同步完成的,也就是所谓的高优先级任务,必须等到彻底收集完毕,才可以循环执行它
做完这个,postEffectQueue主要是一些额外的副作用和清理工作,我实在凑字数无能,就不打印了
总结
最后我们用最直白的话,总结一下:
在宏任务队列中,不断的从stageQueue分批次(16ms)将组件的函数转移到commitQueue里,转移完了,同步操作dom
原理其实还是利用了宏任务队列,其实现在vue的做法和fre也有一点点类似,fre是在宏任务中,尽可能更多的去访问reconcile大循环
关于废除
如开头提到的,timeslicing这部分内容已经在master分支被移除了,关于为什么废除,我特地发了issue,可以戳这里:(天啊,我和尤终于可以和平地进行交谈了)
https://github.com/vuejs/rfcs/issues/89
简单说,就是timeslicing的收益不大,除了issue中提到的,它本身的场景就少的可怜
也因为vue现在的实现,由于调度的基本单位是组件,所以它仍然会因为组件内部的逻辑而被阻断
比如我把用例中用于阻断的block函数改为1s,就已经彻底卡死了
思考
从issue和源码本身,我们可以思考一些问题,同时用来凑字数
时间切片是否必须?
答案是否定的,尤的回复已经足够充分了:https://github.com/vuejs/rfcs/issues/89#issuecomment-546988615
大致有两点:
- 除了高帧率动画,其他的场景几乎都可以使用防抖和节流去提高响应性能
- vue现在的实现,粒度太大,最终的效果十分有限,不值得
那,fre呢?
fre的异步渲染,是否也存在这个问题,不得不承认,fre虽然粒度很小,对于组件内部的阻断可以搞定,但是元素本身也可以被阻断
而且第一个问题也是存在的,就是没有太多适用场景
但是fre源码层面还是意义重大的,即便这玩意搞出来,发现它作用不大,副作用不小,但fre作为我个人的学习和研究的项目,它的价值从来就不是业务层面的
只是我应该停下来,异步渲染搞定了,只是向大家展示它的源码实现,未来不应该跟随react去搞一堆业务API,如useTransition等等
关于源码?
vue3发版当天,源码解读就放出了,但是到目前为止,所有的源码解读统统都是蹭热度的
不久的将来,vue的源码又要烂大街了……
这种现象引起反省,我们读源码到底是为了什么?为了面试吗?为了更好的写业务?
对我而言,仅仅只是感兴趣,我对这部分源码感兴趣,我就去读,并且只读感兴趣的部分
其实大家也看到了,我很少写源码解读的文章,因为我一直反对所谓的【通读源码】
将阅读源码作为一项工作,同样的小函数,读了一遍又一遍,重复劳动
这和糊shi有什么区别呢?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。