Vue 中更新dom的逻辑是异步执行的,我们都知道js是单线程的,通常异步是通过回调函数的方式来实现的,所以在vue中我们如果想获取到数据改变后异步更新dom的最新内容,必须也通过回调的方式来获取。nextTick就是vue为我们提供的一个回调语法。看下面的例子:
<a>{{str}}</a>
data: {
str: 'str1'
},
mounted () {
this.str = 'str2'
console.log(this.$el) // <a>str1</a>
this.$nextTick(() => {
console.log(this.$el) // <a>str2</a>
})
}
从例子我们可以看出,在改变str后立马获取到的当前dom不是最新的,只有在nextTick里获取到的才是最新的。
源码解析
一:dom更新为何是异步的?
如果你了解vue响应式原理vue响应式原理 :我们知道vue的变量更新是通过 setter和setter方法来监控的,当数据更新的时候,执行如下步骤:
1.数据变动会触发set方法通知所有的观察者,看下面的代码中,notify这个方法就是通知底下的所有subs(注册在该值上的观察者集合)执行update操作。
2.update 调用queueWatcher函数
3.queueWatcher 执行的时候,同一个单线程循环周期内的观察者只加入队列一次(避免重复更新),加入完成后,在回调中开始执行队列(参考代码中的中文注释)
所以在单线程的一个执行周期中,反复的修改一个变量,在下一个周期里,只会按照最后一次改动更新一次dom,大大的提高了性能。
// 变量变化后通知观察者执行更新
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
// 观察者的原型链上的更新方法
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) { // 如果配置成同步的,直接更新
this.run();
} else {
queueWatcher(this); // 默认是走这个方法异步执行
}
};
function queueWatcher (watcher) {
var id = watcher.id;
// 这里保证同一个反复的改变的变量,观察者只加入队列一次
if (has[id] == null) {
has[id] = true;
if (!flushing) {// 如果这时候更新dom的操作还没开始 继续push
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.
var i = queue.length - 1;
// 如果在异步渲染dom已经开始的时候再加入进来,顺序插入到上一次刚执行的位置,准备直接下一步执行
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
// 变量锁,默认锁着保证在一个circle循环里的所有变量改变watcher都推到一个队列里
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);// 异步执行
}
}
}
执行队列里的方法
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null; // 这里设为null让这个时候发生改变触发的观察者也能加入进来立即执行
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
// 限制一个观察者最大的更新次数在一个circle里
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
二:nextTick如何实现?
var timerFunc;
// 方法一:promise 实现的异步
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
// 方法二:MutationObserver 监听dom事件
} 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)
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
// 方法三:setImmediate
} 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 = function () {
setImmediate(flushCallbacks);
};
} else {
// 方法四:setTimeout
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) { // 返回一个promise
_resolve = resolve;
})
}
}
vue对于异步采用了如下几种机制来实现。
1.promise.then
2.MutationObserver dom事件监听
3.setImmediate 定时执行方法,会比setTimeout略早执行
4.setTimeout 传统的定时执行方法
vue通过特性探测,来选择用哪种方式来实现回调,同时还处理了一些设备的兼容特性,这其中几种方式的使用和区别,会在其他的文章中再做展开。
nextTick其他知识点:
nextTick 在浏览器原生支持promise的情况下,会返回一个promise对象
所以我们可以如下写法而避免嵌套过深。
this.$nextTick(()=>{
}).then(() => {
// nextTick后的逻辑
})
我们也可以通过绑定this来改变nextTick里的作用域上下文
this.$nextTick(function(){
}).bind(obj)