You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constqueue: Array<Watcher> = []
let has: {[key: number]: ?true} = {}
let waiting = false
let flushing = false
...
export function queueWatcher (watcher: Watcher) {constid=watcher.idif(has[id]==null){has[id]=trueif(!flushing){queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.leti=queue.length-1while(i>index&&queue[i].id>watcher.id){i--}
queue.splice(i + 1, 0, watcher)
}// queue the flushif(!waiting){waiting=truenextTick(flushSchedulerQueue)}}}
exportconstnextTick=(function(){constcallbacks=[]letpending=falselettimerFuncfunctionnextTickHandler(){pending=falseconstcopies=callbacks.slice(0)callbacks.length=0for(leti=0;i<copies.length;i++){copies[i]()}}// An asynchronous deferring mechanism.// In pre 2.4, we used to use microtasks (Promise/MutationObserver)// but microtasks actually has too high a priority and fires in between// supposedly sequential events (e.g. #4521, #6690) or even between// bubbling of the same event (#6566). Technically setImmediate should be// the ideal choice, but it's not available everywhere; and the only polyfill// that consistently queues the callback after all DOM events triggered in the// same loop is by using MessageChannel./* istanbul ignore if */if(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){timerFunc=()=>{setImmediate(nextTickHandler)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||// PhantomJSMessageChannel.toString()==='[object MessageChannelConstructor]')){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=nextTickHandlertimerFunc=()=>{port.postMessage(1)}}else/* istanbul ignore next */if(typeofPromise!=='undefined'&&isNative(Promise)){// use microtask in non-DOM environments, e.g. Weexconstp=Promise.resolve()timerFunc=()=>{p.then(nextTickHandler)}}else{// fallback to setTimeouttimerFunc=()=>{setTimeout(nextTickHandler,0)}}returnfunctionqueueNextTick(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})}}})()
if(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){timerFunc=()=>{setImmediate(nextTickHandler)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||// PhantomJSMessageChannel.toString()==='[object MessageChannelConstructor]')){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=nextTickHandlertimerFunc=()=>{port.postMessage(1)}}else/* istanbul ignore next */if(typeofPromise!=='undefined'&&isNative(Promise)){// use microtask in non-DOM environments, e.g. Weexconstp=Promise.resolve()timerFunc=()=>{p.then(nextTickHandler)}}else{// fallback to setTimeouttimerFunc=()=>{setTimeout(nextTickHandler,0)}}
背景
我们先来看一段Vue的执行代码:
这段脚本执行我们猜测1000m后会依次打印:1、2、3。但是实际效果中,只会输出一次:3。为什么会出现这样的情况?我们来一探究竟。
queueWatcher
我们定义
watch
监听msg
,实际上会被Vue这样调用vm.$watch(keyOrFn, handler, options)
。$watch
是我们初始化的时候,为vm
绑定的一个函数,用于创建Watcher
对象。那么我们看看Watcher
中是如何处理handler
的:初始设定
this.deep = this.user = this.lazy = this.sync = false
,也就是当触发update
更新的时候,会去执行queueWatcher
方法:这里面的
nextTick(flushSchedulerQueue)
中的flushSchedulerQueue
函数其实就是watcher
的视图更新:另外,关于
waiting
变量,这是很重要的一个标志位,它保证flushSchedulerQueue
回调只允许被置入callbacks
一次。接下来我们来看看
nextTick
函数,在说nexTick
之前,需要你对Event Loop
、microTask
、macroTask
有一定的了解,Vue nextTick 也是主要用到了这些基础原理。如果你还不了解,可以参考我的这篇文章Event Loop 简介好了,下面我们来看一下他的实现:
首先Vue通过
callback
数组来模拟事件队列,事件队里的事件,通过nextTickHandler
方法来执行调用,而何事进行执行,是由timerFunc
来决定的。我们来看一下timeFunc
的定义:可以看出
timerFunc
的定义优先顺序macroTask
-->microTask
,在没有Dom
的环境中,使用microTask
,比如weexsetImmediate、MessageChannel VS setTimeout
我们是优先定义
setImmediate
、MessageChannel
为什么要优先用他们创建macroTask而不是setTimeout?HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel 和 setImmediate 的延迟明显是小于setTimeout的。
解决问题
有了这些基础,我们再看一遍上面提到的问题。因为
Vue
的事件机制是通过事件队列来调度执行,会等主进程执行空闲后进行调度,所以先回去等待所有的进程执行完成之后再去一次更新。这样的性能优势很明显,比如:现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发
setter->Dep->Watcher->update->run
。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue实现了一个queue
队列,在下一个Tick(或者是当前Tick的微任务阶段)的时候会统一执行queue
中Watcher
的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个Tick(或者是当前Tick的微任务阶段)的时候调用,大大优化了性能。有趣的问题
这个的执行顺序想必大家都知道先后打印:1、promise、2、3。
因为首先触发了
this.msg = 'end'
,导致触发了watcher
的update
,从而将更新操作callback push进入vue的事件队列。this.$nextTick
也为事件队列push进入了新的一个callback函数,他们都是通过setImmediate
-->MessageChannel
-->Promise
-->setTimeout
来定义timeFunc
。而Promise.resolve().then
则是microTask,所以会先去打印promise。在支持
MessageChannel
和setImmediate
的情况下,他们的执行顺序是优先于setTimeout
的(在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。其次因为事件队列里,优先收入callback数组)所以会打印2,接着打印3但是在不支持
MessageChannel
和setImmediate
的情况下,又会通过Promise
定义timeFunc
,也是老版本Vue 2.4 之前的版本会优先执行promise
。这种情况会导致顺序成为了:1、2、promise、3。因为this.msg必定先会触发dom更新函数,dom更新函数会先被callback收纳进入异步时间队列,其次才定义Promise.resolve().then(function () { console.log('promise!')})
这样的microTask,接着定义$nextTick
又会被callback收纳。我们知道队列满足先进先出的原则,所以优先去执行callback收纳的对象。参考文章
Vue.js 升级踩坑小记
【Vue源码】Vue中DOM的异步更新策略以及nextTick机制
The text was updated successfully, but these errors were encountered: