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
for(macroTaskofmacroTaskQueue){// 1. Handle current MACRO-TASKhandleMacroTask();// 2. Handle all MICRO-TASKfor(microTaskofmicroTaskQueue){handleMicroTask(microTask);}}
import{noop}from'shared/util'import{handleError}from'./error'import{isIOS,isNative}from'./env'constcallbacks=[]letpending=falsefunctionflushCallbacks(){pending=falseconstcopies=callbacks.slice(0)callbacks.length=0for(leti=0;i<copies.length;i++){copies[i]()}}// Here we have async deferring wrappers using both microtasks and (macro) tasks.// In < 2.4 we used microtasks everywhere, but there are some scenarios where// microtasks have too high a priority and fire in between supposedly// sequential events (e.g. #4521, #6690) or even between bubbling of the same// event (#6566). However, using (macro) tasks everywhere also has subtle problems// when state is changed right before repaint (e.g. #6813, out-in transitions).// Here we use microtask by default, but expose a way to force (macro) task when// needed (e.g. in event handlers attached by v-on).letmicroTimerFuncletmacroTimerFuncletuseMacroTask=false// Determine (macro) task defer implementation.// Technically setImmediate should be the ideal choice, but it's only available// in IE. 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)){macroTimerFunc=()=>{setImmediate(flushCallbacks)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||// PhantomJSMessageChannel.toString()==='[object MessageChannelConstructor]')){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=flushCallbacksmacroTimerFunc=()=>{port.postMessage(1)}}else{/* istanbul ignore next */macroTimerFunc=()=>{setTimeout(flushCallbacks,0)}}// Determine microtask defer implementation./* istanbul ignore next, $flow-disable-line */if(typeofPromise!=='undefined'&&isNative(Promise)){constp=Promise.resolve()microTimerFunc=()=>{p.then(flushCallbacks)// in problematic UIWebViews, Promise.then doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't being flushed, until the browser// needs to do some other work, e.g. handle a timer. Therefore we can// "force" the microtask queue to be flushed by adding an empty timer.if(isIOS)setTimeout(noop)}}else{// fallback to macromicroTimerFunc=macroTimerFunc}/** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */exportfunctionwithMacroTask(fn: Function): Function{returnfn._withTask||(fn._withTask=function(){useMacroTask=trueconstres=fn.apply(null,arguments)useMacroTask=falsereturnres})}exportfunctionnextTick(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 =trueif(useMacroTask){macroTimerFunc()}else{microTimerFunc()}}// $flow-disable-lineif(!cb&&typeofPromise!=='undefined'){returnnewPromise(resolve=>{_resolve=resolve})}}
上篇派发更新的最后提到了
nextTick
,在Vue中,nextTick
也是一个核心实现,本篇来详细说一下nextTick
的实现原理。JS运行机制
JS执行是单线程的,它是基于事件循环,详细说明可以查看 一文讲解浏览器运行渲染机制、JS任务队列及事件循环,这里再简单过一下。事件循环大致分为以下几步:
关于宏任务和微任务,也可以在 一文讲解浏览器运行渲染机制、JS任务队列及事件循环 里找到相应的解释,下面来模拟一下执行顺序:
Vue的实现
在Vue里实现的异步操作就是
nextTick
。在 Vue 源码 2.5+ 后,nextTick
的实现单独有一个 JS 文件来维护它,它的源码并不多,总共也就 100 多行。接下来我们来看一下它的实现,在src/core/util/next-tick.js
中:简单提一下,除了上面的
nextTick
的实现之外,还有两个地方用到了nextTick
:下面来分析一下
nextTick
的具体实现:首先在全局定义了一个数组
callbacks
,一个状态pending
,以及一个函数flushCallbacks
。接着后面定义了microTimerFunc
和macroTimerFunc
,分别对应的是 micro task 的函数和 macro task 的函数,它们两个其实就是针对浏览器的支持程度,做不同的处理。对于 macro task 的实现,优先检测是否支持原生
setImmediate
,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的MessageChannel
,如果也不支持的话就会降级为setTimeout 0
;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。接着它对外暴露2个函数:
withMacroTask
和nextTick
,withMacroTask
其实就是做了一层封装,确保函数在执行过程中如果对数据进行了修改,触发变化执行nextTick
的时候强制走一个marcoTimeFunc
,换句话说也就是,强制在DOM事件的回调函数期间,如果修改了数据,那这些数据更改推入的队列就会被当做macroTask
在nextTick
后执行。而
nextTick
函数,上篇派发更新的最后执行nextTick(flushSchedulerQueue)
的时候用到了它,它不仅可以传入一个回调函数,还可以传入一个Promise,在这里使用 try/catch 的方式执行是因为JS是单线程的,如果不使用 try/catch 并且执行期间有一个报错了,整个逻辑就会崩掉,后面的逻辑就不会执行了。在执行nextTick
的时候传入的是匿名函数,通过 push 的方式,把匿名函数全部压到callbacks
中,接着判断pending
,目的就是确保这块的逻辑只执行一次。然后根据useMaroTask
来判断走macroTimerFunc
还是microTimerFunc
,注意,无论执行那个,都会在下一个tick的时候才执行flushCallbacks
。也就是说在当前 tick 内,无论进行多少次
nextTick
,都会把cb
收集起来,放到callbacks
数组中,然后在下一个 tick 的时候遍历并执行这些匿名函数,整个就是一个异步过程。除了传入匿名函数的方式之外,也可以不传入匿名函数,通过
nextTick.then(() => {})
的方式调用:当
_resolve
执行的时候,就会跳到then
里面了。所以:
当检测不到
cb
的时候,就判断是不是传入了一个 Promise。最后注意一点,当
flushCallbacks
执行的时候,cb
是同步执行的,promise 是异步执行。有趣的问题
下面执行结果是什么?
输出:
过程大概是这样的:首先打印的是 sync 的值,接着打印 nextTick 的,最后打印 promise nextTick 的值。第一步调用
setter
的时候会调用内部的nextTick
函数,第二步手动调用nextTick
,第三步也是手动调用,那上面分析过每一次进行nextTick
的时候,都是在callbacks
里进行push
操作,这样也就意味着先push
的先执行,遍历也是从前往后的,所以先添加的就会先执行,而对于上面的例子来说,先添加的是一个 nextTick 匿名函数,然后再添加修改msg为456
时的watcher,也就是flushSchedulerQueue
,也就是说整体在执行flushCallbacks
的时候,会先执行 nextTick 的匿名函数,然后在执行flushSchedulerQueue
的时候才会重新渲染,所以重新渲染是在匿名函数之后,所以匿名函数打印的是原来的值,而第三个 promise nextTick 就是打印的渲染之后的了,如果说打印的不是
this.$refs.msg.innerText
,而是this.msg
,那么所有的就都是 456 了,因为this.msg
修改了值,会立刻发生那个变化,而视图的更新(DOM的变化)是在下一个 tick 才会进行的,所以打印的就都一样了。The text was updated successfully, but these errors were encountered: