前言
前几天在掘金上看到一位网友的面经(大厂面试:别拯救了,拯救不了一点 - 掘金 (juejin.cn)),其中有一道题引起了我的注意:

这道题难在解释清楚原理是什么,这涉及到了Vue渲染页面的原理和nexttick执行顺序。
本文主要内容包括:
-
原题答案分析。
1.1. Vue响应式原理概览。
1.2. nexttick原理概览。
1.3. 原题目原理分析。
-
修改后的题目分析。
1. 原题答案分析
这里我在Vue2源码中提供的examples来运行这道题

vue2 源码地址:github.com/vuejs/vue
还原题目:
html
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
<div ref="num">
<h1>a是: {{a}}</h1>
<h1>b是: {{b}}</h1>
</div>
</div>
<script>
const vm = new Vue({
data() {
return {
a: 100,
b: 200
}
},
created() {
setTimeout(() => {
this.$nextTick(() => {
console.log(this.$refs.num.innerHTML)
})
this.b = 400
}, 200)
}
})
vm.$mount('#app')
</script>

可以看到在$nextTick 中拿到的a和b是 300 和 400 ,这很好理解,毕竟nexttick是异步执行嘛,拿到更新后的数据也正常。
把对this.a移动到下面来试试?
html
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
<div ref="num">
<h1>a是: {{a}}</h1>
<h1>b是: {{b}}</h1>
</div>
</div>
<script>
const vm = new Vue({
data() {
return {
a: 100,
b: 200
}
},
created() {
setTimeout(() => {
this.$nextTick(() => {
console.log(this.$refs.num.innerHTML)
})
this.a = 300 // 把对a的修改移动到下面来试试。
this.b = 400
}, 200)
}
})
vm.$mount('#app')
</script>
输出结果:

这回拿到的怎么就是100和200,但是nexttick不是异步执行吗?按理说应该拿到的还是更新后的数据。
想要知道原因,就先要了解vue响应式的原理。
1.1 Vue响应式原理概览
vue的响应式原理大概可以分为如下几个步骤:

被观察者:Dep
一个dep是一个可观察的对象,可以有多个观察者观察它,在data的中,每个对象(包括数组)都有一个Dep,

Watcher观察data中的对象,其实就是观察Dep。那么Dep做了什么事呢?在对象(或者数组)被获取的时候,Dep会去收集Watcher,就拿渲染Watcher来说,当访问data中的数据的时候,Watcher会首先把自己挂载到Dep.target上,然后Dep会去收集Watcher

这样,每个数据就"知道"自己被谁观察了,自己发生改变的时候(即触发set),就会通知观察自己的Watcher,去执行回调(比如刷新页面、重新执行计算属性)。

观察者:Watcher
Watcher是Vue实现响应式的核心部分。我们都知道,Vue的响应式原理基于观察者模式,而Watcher就是这个观察者,它的主要工作是在被观察者发生改变时,接收通知并执行相应的动作(即执行回调函数),类似于JavaScript中的事件监听。在Vue中,有三种Watcher:
1. 渲染Watcher:
渲染Watcher随着组件的创建而创建,每个组件独一份,在创建Watcher的时候,组件会将更新自己的函数updateComponent传入Watcher作为回调,当本组件内的的数据变化的时候,Dep就会通知Watcher触发这个回调,从而更新组件。

updateCompoent内容:
- 侦听器Watcher:
侦听器Watcher是Vue的watch核心内容,侦听器Watcher和渲染watcher的在于侦听器Watcher多一个用户传入的回调,并且第一个回调也不再是刷新页面了,而是获取侦听对象

vue2中的侦听器,在被执行后会立马获取一遍侦听目标(vue3 略有不同),之前提到过,获取数据会被Dep记录,当侦听目标发生改变的时候,watcher就会做两件事(即传入的两个回调):1. 重新获取目标值。2.执行用户传入的回调函数。
- 计算属性Watcher:
计算属性Watcher和侦听器Watcher很像,

主要的区别就是计算属性有缓存。

evaluate函数:

1.2 nexttick原理概览
nexttick作用其实很简单:
- 接受回调函数。
- 把回调函数放到任务队列中(其实就是个数组)。
- 主线程执行完毕,开始遍历数组,依次执行。

那nexttick可以在下次 DOM 更新循环结束之后执行延迟回调。具体是怎么实现的呢? 这又要回到刚刚提到的Watcher了,其实只需要了解一下页面更新的完整流程你就知道原因了:

再简化一下:

也就是说,nexttick之所以能获取到更新后的页面,是因为用户调用nexttick传入的函数比更新页面的函数后入队列,这就是为什么在修改数据之前调用nexttick是拿不到页面更新后的数据的原因。
原题目原理分析
在大概了解Vue的响应式原理之后,我们就可以来分析这道题目了,

为什么this.a 放到上面去拿到的是更新后的页面也就说得通了:

在理解了原理之后,这道题目就是这么简单。
修改后的题目分析
将题目小小的变动一下:
html
<meta charset="utf-8" />
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="../../dist/vue.js"></script>
<link rel="stylesheet" href="../../node_modules/todomvc-app-css/index.css" />
<div id="app">
<div ref="num">
<h1>a是: {{a}}</h1>
<h1>b是: {{b}}</h1>
</div>
</div>
<script>
const vm = new Vue({
data() {
return {
a: 100,
b: 200
}
},
created() {
// 去除 setTimeout
this.$nextTick(() => {
console.log(this.$refs.num.innerHTML)
})
this.a = 300
this.b = 400
}
})
vm.$mount('#app')
</script>
这里我将settimeout去除了,结果还和之前一样吗?
输出:

可以看到,结果和去除settimeout之间不一样了,拿到的居然是更新后的值!
2.1 原因分析
这其实和我们代码执行的位置有关系。

我们知道,Vue的数据响应式是基于观察者模式的,在created中,虽然data已经被劫持,但是由于渲染Watcher还没有被创建,所以页面连一次数据都还没更新,

这个时候我们修改data,不会触发页面的更新逻辑,但是data的值实实在在的已经改了。 当Vue创建渲染Watcher的时候(也就是执行第一次页面更新的时候,而且是同步执行),拿到的data中的a和b就是已经修改后的数据了,所以我们在nexttick中获取到的页面也就是更新后的了。

其实只要我们把逻辑写在mounted中就可以获取到更新之前的值了,因为mounted执行的时候,渲染Watcher已经创建。
输出测试:
