摘要
本文介绍了$nexTick的工作原理,以及this.数据项=新值
背后做了什么,nextTick与promise的关系。阅读本文需要你对vue的基本运作过程,微任务以及promise有一定的了解。
内容
- 7道测试题
- $nextTick的工作原理,它与Promise的关系
this.数据项=新值
背后做了什么- 7道测试题的答案及讲解
测试题
下面是一份基础代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div id="div">{{a}}</div>
<button @click="btn">btn</button>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.js"></script>
<script>
var vm = new Vue({
el:"#app",
data:{ a :1 },
methods: {
btn() {
this.a = 2
const div = document.getElementById('div')
this.$nextTick(() => {
console.log('nextTick', div.innerHTML)
})
console.log(div.innerHTML)
}
}
})
</script>
</body>
</html>
问题是: 点击了按钮之后,输出的顺序和结果是什么?答案是:1;nextTick 2; 这应该难不到你吧。
好了,下面类似的题有7道,为了节省代码,我只给出了btn()中的部分。
题目1
题目2
题目3
题目4
题目5
题目6
题目7
如果你能很清楚的答出来,就请直接拉到底部,留言给我,让我膜拜一下。要是你有遗憾,或者不太会,就请你一定坚持读完哈~
this.$nextTick到底做了什么
阅读源码是最好的方式,nextTick的源码如下(有删减):
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
// 把回调添加到callbacks数组中
cb.call(ctx);
});
// 确保当前微任务执行完成之后,
// 再把callbacks的内容推入下一微任务
if (!pending) {
pending = true;
timerFunc(); //调用timerFunc
}
}
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
};
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)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
请你关注两个内容:callbacks和timerFunc。
callbacks是一个数组,它将用来收集回调。
timerFunc是一个异步函数,它是动态确定的。优先级如下:Promise.then > MutationObserver > setImmediate > setTimeout。 那在标准浏览器中(支持promise的环境),我们可以认为它就是Promise.then。所以可以用下图标识nextTick的功能。
它做两件事:
- 将回调函数push到其内部维护的callbacks数组中
- 用Promise.resolve().then 注册微任务,执行callbacks中的所有回调。
这是图1。下边的讲解中还会有一个图2。你掌握两张图之后,前面的7道题就没有问题了。
this.a=2 做了什么
假设原来的数据是a=1,现在this.a=2做了什么呢?
我也准备了图给你
上图中有很多中间过程,简化这个过程,如下图示:
这是图2。总结一下就是:
- 数据的改变会导致nextTick执行,进而将watcher.run添加到callbacks这个内部维护的数组中,然后进入微任务序列。
- nextTick才是异步更新的关键点。
上边的讲解中有一个图1,现在有张图2。目前,你已经掌握两张图,可以直接进入练习题的答案及讲解部分了。
如果你想进一步加深印象就可以看下面具体过程:
数据修改后,激活属性的setter,
调用dep的notify来通知订阅者 vue2源代码目录/src/core/observer/dep.js
每个subs[i] 就是一个watcher(一个组件一个watcher),它的update方法如下
vue2源代码目录/src/core/observer/watcher.js
注意,这里的update并不会立即执行,而是会进入 queueWatcher函数,这个函数是在scheduler.js中定义的,它用来对watcher进行统一排队管理。
vue2源代码目录/src/core/observer/scheduler.js ,
它定义了一个flushSchedulerQueue函数,在这个函数中去调用watch,但是,它并不是直接执行的,而是交给了nextTick。这里才是异步的开始。注意下面的代码中第165,166行的:根据watcher的id,去掉重复的watcher
。
最重要的,还是到了187行中的nextTick中来。
上面的flushSchedulerQueue如下(有删减):
function flushSchedulerQueue () {
queue.sort((a, b) => a.id - b.id)
// watcher的id是有序的,父组件的created在子组件之前,所以它的watcher的id肯定要小些
// 就会在前面执行
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()// 包括新的虚拟dom生成,diff算法,更新视图等等
}
}
而nextTick在前面已经分析过了。
相当于在微任务中去执行: flushCallbacks=>callbacks=>flushSchedulerQueue =>watcher.run。
你可以再次回去看看图2。
测试题答案及讲解
题目1
答案:1; nextTick 2;
分析过程:
第3行:this.a=3
修改了数据,将watcher.run添加到callbacks,记callbacks=[watcher.run],并推到微任务队列:[ ...callbacks ]
第4行:this.a=2
修改了数据,本来也要将render()函数进入微任务队列,但前面介绍的queueWatcher()有对watcher的去重功能,所以,本句代码不会让render再次进入微任务队列。
第5行:this.$nextTick,将回调添加到了微任务队列的末尾。微任务队列中有: [watcher.run, 第5行的回调]
第8行:打印输出1,此时,同步任务结束。
开始依次从微任务队列中取出任务执行。
watcher.run:会更新视图,它执行完成后,视图中div的内容是2
第5行的回调:输出nextTick 2
题目2
答案:1; nextTick 1;
分析过程:
第3行:this.$nextTick,将回调添加到callbacks, 记callbacks=[第5行的回调] 。微任务队列中有:[ ... callbacks ]
第6行:this.a=2
修改了数据,将watcher.run添加到callbacks,记callbacks=[第5行的回调, watcher.run],微任务队列中有:[第5行的回调,watcher.run ]
第7行:打印输出1,此时,同步任务结束。
开始依次从微任务队列中取出任务执行。
第5行的回调:输出nextTick 1。注意,它在watcher.run的前边执行。所以视图并没有更新。
watcher.run:会更新视图,它执行完成后,视图中div的内容是2。
题目3
答案:1 ; nextTick 2;
题目4
答案:1 ; nextTick 2; resolve 2;
分析过程:
第1行:this.a=2
修改了数据,将watcher.run函数 添加到callbacks,记callbacks=[watcher.run],并将callback 进入微任务队列。微任务队列中有:[ ...callbacks ]
第4行:注册一个微任务。微任务队列中有:[ ...callbacks, console.log('resovle') ]
第7行:this.$nextTick将回调添加到了callbacks的末尾。callbacks=[watcher.run,nextTick的回调] ,微任务队列中有:[ ...callbacks, console.log('resovle') ]
第10行:打印输出1,此时,同步任务结束。
开始依次从微任务队列中取出任务执行。所以答案是:
watcher.run -> nextTick的回调 -->console.log('resovle')
题目5
答案:1;resolve 1; nextTick 1;
题目6
答案:1; nextTick 1; resolve 2;
题目7
答案:1 ; nextTick 2; resolve 2
小结
$nextTick是异步函数,在标准浏览器中(支持原生Promise),它采用Promise.resolve().then()来实现异步功能。
$nextTick做两件事:1是将回调收集到内部闭包维护的callbacks中,2. 在本轮事件循环中的微任务中,依次执行callbacks中的回调。
数据改变时,仍旧会调用$nextTick,传入的回调是经过去重排序之后的water.run。