当前位置: 首页 > Web前端 > HTML

VuenextTick机制

时间:2023-04-02 22:01:41 HTML

背景先来看一段Vue的执行代码:exportdefault{data(){return{msg:0}},mounted(){this.msg=1this.msg=2this.msg=3},watch:{msg(){console.log(this.msg)}}}执行这个脚本后,我们猜测在1000m之后,会依次打印:1,2,3。但是在实际效果中,只有一个输出:3.为什么会出现这样的情况?让我们找出来。queueWatcher我们定义了watch来监听msg,它实际上会被Vue这样调用vm.$watch(keyOrFn,handler,options)。$watch是我们初始化时绑定到vm的函数,用于创建Watcher对象。然后我们看看在Watcher中handler是如何处理的:(this.sync){this.run()}else{queueWatcher(this)}}...初始设置this.deep=this.user=this.lazy=this.sync=false,即当触发update时更新,将执行queueWatcher方法:constqueue:Array=[]lethas:{[key:number]:?true}={}letwaiting=falseletflushing=false...exportfunctionqueueWatcher(watcher:Watcher){constid=watcher.idif(has[id]==null){has[id]=trueif(!flushing){queue.push(watcher)}else{//如果已经刷新,拼接watcherbasedonitsid//如果已经超过了它的id,它将立即运行。leti=queue.length-1while(i>index&&queue[i].id>watcher.id){i--}queue.splice(i+1,0,watcher)}//刷新队列if(!waiting){waiting=trueenextTick(flushSchedulerQueue)}}}nextTick(flushSchedulerQueue)中的flushSchedulerQueue函数其实就是watcher的视图更新:functionflushSchedulerQueue(){flushing=trueletwatcher,id...for(index=0;index{setImmediate(nextTickHandler)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||//PhantomJSMessageChannel.toString()==='[objectMessageChannelConstructor]')){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=nextTickHandlertimerFunc=()=>{port.postMessage(1)}}else/*istanbulignorenext*/if(typeofPromise!=='undefined'&&isNative(Promise)){//在非DOM环境中使用微任务,例如Weexconstp=Promise.resolve()timerFunc=()=>{p.then(nextTickHandler)}}else{//回退到setTimeouttimerFunc=()=>{setTimeout(nextTickHandler,0)}}返回函数queueNextTick(cb?:Function,ctx?:Object){let_resolvecallbacks.push(()=>{if(cb){try{cb.call(ctx)}catch(e){handleError(e,ctx,'nextTick')}}elseif(_resolve){_resolve(ctx)}})if(!pending){pending=truetimerFunc()}//$flow-disable-lineif(!cb&&typeofPromise!=='undefined'){returnnewPromise((resolve,reject)=>{_resolve=resolve})}}})()首先,Vue通过回调数组模拟事件队列,事件在事件队列,通过nextTickHandler方法执行调用,执行什么由timerFunc决定我们看一下timeFunc的定义:if(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){timerFunc=()=>{setImmediate(nextTickHandler)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||//PhantomJSMessageChannel.toString()==='[objectMessageChannelConstructor]')){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=nextTickHandlertimerFunc=()=>{port.postMessage(1)}}else/*istanbulignorenext*/if(typeofPromise!=='undefined'&&isNative(Promise)){//在非DOM环境中使用微任务,例如Weexconstp=Promise.resolve()timerFunc=()=>{p.then(nextTickHandler)}}else{//fallbacktosetTimeouttimerFunc=()=>{setTimeout(nextTickHandler,0)}}你可以看到定义timerFunc优先级顺序macroTask-->microTask,在没有Dom的环境下,使用microTask,比如weexsetImmediate,MessageChannelVSsetTimeout我们优先定义setImmediate,MessageChannel为什么要用它们来创建macroTask而不是setTimeout?HTML5规定setTimeout的最小延时是4ms,也就是说在理想环境下,最快的异步回调是4ms触发Vue使用这么多函数来模拟异步任务。目的只有一个,就是让回调异步,尽快调用。MessageChannel和setImmediate的延迟明显小于setTimeout。解决问题有了这些基础,我们再来看一下上面提到的问题。因为Vue的事件机制是通过事件队列来调度执行的,空闲后会等待主进程执行完,所以回去等所有进程执行完再更新。这种性能优势很明显,例如:现在有这样一种情况,mounted的时候,test的值会被++循环执行1000次。每次++都会根据响应快慢触发setter->Dep->Watcher->update->run。如果此时不异步更新视图,那么每次++都会直接操作DOM更新视图,非常耗性能。所以Vue实现了一个队列,在下一个Tick(或者当前Tick的microtask阶段)统一执行队列中Watcher的运行。同时相同id的Watcher不会重复加入队列,所以Watcher运行不会执行1000次。最后更新view只会直接把test对应的DOM的0变成1000。保证更新view操作DOM的动作在下一个Tick(或者当前Tick的microtask阶段)被调用当前栈执行完毕后,极大的优化了性能。有趣的问题varvm=newVue({el:'#example',data:{msg:'begin',},mounted(){this.msg='end'console.log('1')setTimeout(()=>{//macroTaskconsole.log('3')},0)Promise.resolve().then(function(){//microTaskconsole.log('promise!')})this.$nextTick(function(){console.log('2')})}})执行顺序大家肯定都知道:1、promise、2、3。因为this.msg='end'先被触发,所以watcher的更新是触发,更新操作的回调推送进入vue的事件队列。this.$nextTick也为事件队列推送进入了一个新的回调函数。它们都是通过setImmediate-->MessageChannel-->Promise-->setTimeout来定义timeFunc的。而Promise.resolve().then是microTask,所以会先打印promise。在支持MessageChannel和setImmediate的情况下,它们的执行顺序先于setTimeout(在IE11/Edge中,setImmediate延迟可以在1ms以内,setTimeout最小延迟4ms,所以setImmediate早于setTimeout(0)回调函数执行其次,因为在事件队列中,回调数组优先),所以会打印2,然后打印3,但是不支持MessageChannel和setImmediate时,timeFunc会通过Promise定义,这也是Vue2.4之前的旧版本。履行承诺。这种情况会导致顺序变成:1,2,promise,3。因为this.msg必须先触发dom更新函数,dom更新函数会先被回调存入异步时间队列,然后再定义Promise.resolve().then(function(){console.log('promise!')})这样一个microTask,然后定义$nextTick,它会被存储在回调中。我们知道队列满足先进先出的原则,所以回调中存放的对象先执行。后记如果你对Vue源码感兴趣,可以来这里:更有趣的Vue约定源码讲解参考文章:Vue。

最新推荐
猜你喜欢