<body>
<div id="app" ref="app">
<div>{{ a }}</div>
<div>{{ b }}</div>
<div><button @click="test">修改</button></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data(){
return {
a: 1,
b: 2
}
},
methods: {
test(){
let _this = this
this.a = 11
this.$nextTick(function get(){
console.log(_this.$refs.app.innerHTML)
})
this.b = 22
}
},
})
</script>
</body>
data中的数据a绑定了一个render watcher
data中的数据b也绑定了一个render watcher 经过分析对比,a和b绑定的其实是同一个render watcher,对应的就是当前组件的render函数。
a和b的Watcher的id都是1。
上面的test方法执行过程:
this.a = 11
- 调用通过Object.defineProperty为a添加的set方法
- set函数会调用a绑定的Dep的notify方法
- Dep的notify方法中会遍历Dep下的所有Watcher,执行Watcher的update方法
- 调用nextTick方法将Watcher推入到微任务队列中
this.$nextTick(function get(){
console.log(_this.$refs.app.innerHTML)
})
- 调用nextTick方法将get方法推入到微任务队列中
this.b = 22
此段代码的执行过程同this.a = 11
,但由于他绑定的Watcher和a绑定的Watcher是同一个render Watcher。所以b对应的Watcher不会被推入到微任务队列中。
基于JS的事件循环机制,此轮事件循环的主代码执行完成后,会执行当前主代码中产生的所有微任务。
由上面的分析可以得到微任务队列中有两个微任务,第一个是当前组件的render Watcher,第二个是通过nextTick传入的get方法。
render Watcher对应的当前组件的render方法,render方法执行完成后生成新的vnode,新旧vnode进行diff对比。将差异vnode渲染到真实dom中。
接下来执行第二个微任务,即get方法,get方法获取当前组件的innerHTML。由于get方法是在render Watcher 之后执行,所以此时获取到的innerHTML是最新的,如下
<div>11</div> <div>22</div> <div><button>修改</button></div>
考虑到兼容性问题,nextTick的实现异步的逻辑为:
优先使用promise.then,再MutationObserver,再setImmediate,最后使用setTimeout;
源码实现如下:
var timerFunc;
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
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)
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;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}