Vue 的特色之一就是响应式,但数据更新时,DOM 并不会当即更新。当咱们有一个业务场景,须要在 DOM 更新以后再执行一段代码时,能够借助nextTick
实现。如下是来自官方文档的介绍:html
将回调延迟到下次 DOM 更新循环以后执行。在修改数据以后当即使用它,而后等待 DOM 更新。
具体的使用场景和底层代码实如今后面的段落说明和解释。api
Vue.nextTick([callback, context])
与 vm.$nextTick([callback])
数组
前者是全局方法,能够显式指定执行上下文,然后者是实例方法,执行时自动绑定this
到当前实例上。浏览器
此外,在 2.1.0 版本还新增了不传入回调的使用方式,这种调用会返回一个 Promise,在 then 的回调执行目标操做便可,如vm.$nextTick().then(cb)
。app
如下是一个nextTick
使用例子:函数
<div id="app"> <button @click="add">add</button> {{count}} <ul ref="ul"> <li v-for="item in list"> {{item}} </li> </ul> </div>
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); } } });
以上的代码,指望在每次新增一个列表项时都使得列表项的字体是红色的,但实际上新增的列表项字体还是黑色的。尽管data
已经更新,但新增的 li 元素并不当即插入到 DOM 中。若是但愿在 DOM 更新后再更新样式,能够在nextTick
的回调中执行更新样式的操做。oop
new Vue({ el: "#app", data: { count: 0, list: [] }, methods: { add() { this.count += 1; this.list.push(1); this.$nextTick(() => { let li = this.$refs.ul.querySelectorAll("li"); li.forEach(item => { item.style.color = "red"; }); }); } } });
数据更新时,并不会当即更新 DOM。若是在更新数据以后的代码执行另外一段代码,有可能达不到预想效果。将视图更新后的操做放在nextTick
的回调中执行,其底层经过微任务的方式执行回调,能够保证 DOM 更新后才执行代码。post
在/src/core/instance/index.js
,执行方法renderMixin(Vue)
为Vue.prototype
添加了$nextTick
方法。实际在Vue.prototype.$nextTick
中,执行了nextTick(fn, this)
,这也是vm.$nextTick( [callback] )
自动绑定this
到执行上下文的缘由。字体
nextTick
函数在/scr/core/util/next-tick.js
声明。在next-tick.js
内,使用数组callbacks
保存回调函数,pending
表示当前状态,使用函数flushCallbacks
来执行回调队列。在该方法内,先经过slice(0)
保存了回调队列的一个副本,经过设置callbacks.length = 0
清空回调队列,最后使用循环执行在副本里的全部函数。this
const callbacks = []; let pending = false; function flushCallbacks() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } }
接着定义函数marcoTimerFunc
、microTimerFunc
。
先判断是否支持setImmediate
,若是支持,使用setImmediate
执行回调队列;若是不支持,判断是否支持MessageChannel
,支持时,在port1
监听message
,将flushCallbacks
做为回调;若是仍不支持MessageChannel
,使用setTimeout(flushCallbacks, 0)
执行回调队列。无论使用哪一种方式,macroTimerFunc
最终目的都是在一个宏任务里执行回调队列。
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks); }; } else if ( typeof MessageChannel !== "undefined" && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === "[object MessageChannelConstructor]") ) { const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = flushCallbacks; macroTimerFunc = () => { port.postMessage(1); }; } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0); }; }
而后判断是否支持Promise
,支持时,新建一个状态为resolved
的Promise
对象,并在then
回调里执行回调队列,如此,便在一个微任务中执行回调,在 IOS 的 UIWebViews 组件中,尽管能建立一个微任务,但这个队列并不会执行,除非浏览器须要执行其余任务;因此使用setTimeout
添加一个不执行任何操做的回调,使得微任务队列被执行。若是不支持Promise
,使用降级方案,将microTimerFunc
指向macroTimerFunc
。
if (typeof Promise !== "undefined" && isNative(Promise)) { const p = 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 macro microTimerFunc = macroTimerFunc; }
在函数nextTick
内,先将函数cb
使用箭头函数包装起来并添加到回调队列callbacks
,转入的回调cb
会在callbacks
被遍历执行的时候执行。若是没有传入cb
,则是形如this.$nextTick().then(cb)
的使用方式,因此要返回一个fulfilled
的 Promise,在箭头函数内则须要执行resolved
,令返回的 Promise 状态变为fulfilled
。接着判断当前是否正在执行回调,若是不是,将pengding
设置为真。判断回调执行是宏任务仍是微任务,分别经过marcoTimerFunc
、microTimerFunc
来触发回调队列。最后,若是,没有传入cb
,则须要建立一个Promise
实例并返回以支持链式调用,而且将_resolve
指向返回 Promise 的resolve
函数。
export function nextTick(cb?: Function, ctx?: Object) { let _resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, "nextTick"); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; if (useMacroTask) { macroTimerFunc(); } else { microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== "undefined") { return new Promise(resolve => { _resolve = resolve; }); } }
而全局方法Vue.nextTick
在/src/core/global-api/index.js
中声明,是对函数nextTick
的引用,因此使用时能够显式指定执行上下文。
Vue.nextTick = nextTick;
本文关于nextTick
的使用场景和源码作了简单的介绍,若是想深刻了解这部分的知识,能够去了解一下微任务mircotask
和宏任务marcotask
。