vlambda博客
学习文章列表

「Vue 源码」nextTick 的执行逻辑

Vue.nextTick 官方的定义是:
Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
https://vuejs.org/v2/api/#Vue-nextTick

在下一次 DOM 更新结束后,执行的回调函数,更改了数据,但想等着 DOM 更新结束,那就可以使用这个方法。

「在源码中」执行 renderMixin 方法的时候,会把 $nextTick 挂载到 Vue 原型,然后会返回一个 nextTick 的方法函数。
Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this)};
nextTick 源码路径在  / src/core/util/next-tick.js。
首先定义了一个数组 callbacks 来收集我们 this.$nextTick() 里面传入的函数,然后 flushCallbacks 按数组顺序依次执行,依次执行是如果你写了多个 this.$netTick ,Vue 会在执行完 DOM 更新后,依次执行页面上的 this.$nextTick 里的函数。
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]() }}
pending 初始为 false,是为了只执行一次 nextTick() 函数。
nextTick 函数的定义:
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 timerFunc() }}
nextTick 方法需要传一个函数,然后把传入的函数依次添加到 callbacks 里面去,而在 callbacks 里面的执行时机,要根据当前浏览器「任务队列」来处理。

单线程的 JS 为了防止出错影响到后面的执行,用 try catch 捕获了,就算传入的方法出错,也不会影响后面的执行。

当 this.$nextTick() 不传入任何参数的时候,并且在页面手写了 .then 方法,而当前设备支持 Promise ,就会返回一个 Promise 方法。
Promise 属于「微任务」,会在当前「宏任务」执行完之前去清空「微任务队列」,这样也能达到 DOM 更新完,延时执行方法函数的效果。
if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve })}
下面是两种 nextTick 的写法:
this.$nextTick(()=>{ console.log('updata data');})// Promisethis.$nextTick().then(()=>{ console.log('updata data');})
而我们在执行 nextTick 方法时,需要根据当前设备环境来判断支持哪种模式,isNative 来判断是否有原生方法:
function isNative (Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString())}
判断类型不为 undefined 并且支持此方法,会去执行对应的「延迟执行」方法。不同的设备会采取不同时机去执行 flushCallbacks 方法。
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { 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) } isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]')) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Techinically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) }} else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) }}
1、当前设备环境支持 Promise,或者自定义了 Promise 方法,去执行 Promise.resolve() 方法;
2、当前设备不是 IE 并且支持 MutationObserver 方法,去执行 new MutationObserver 方法;
3、当前设备支持 setImmediate 方法「只方法只针对 IE 端」,执行 setImmediate 方法;
4、如果当前设备都不支持,就执行 setTimeout 方法。

nextTick  执行的是一套微任务队列,如果当前设备不支持 MutationObserver 或者 Promise ,那就会转为宏任务 setTimeout ,关于宏任务和微任务在队列中执行顺序的区别,可以参考之前文章:


Github nextTick 地址:

https://github.com/vuejs/vue/blob/2.6/src/core/util/next-tick.js


图片授权基于 www.pixabay.com 相关协议

推荐阅读