Vue 响应式原理

Vue 最重要的部分,就是响应式原理了


点我查看官方讲解

data

众所周知,给 vue 实例设置数据一般是这么写的

1
2
3
4
5
6
7
const vm = new Vue({
data(){
return {
n: 0
}
}
})

现在改写一下上述代码

1
2
3
4
5
6
7
8
9
let rawData = {
n: 0
}
const vm = new Vue({
data(){
return rawData
}
})
console.log(rawData)

有什么不同呢?在第一个例子中你不能观测到 vue 对 data 返回的对象做了些什么,因为这个对象是个匿名对象

现在你发现,打印出来的 rawData 变了!它不再是个单纯的 {n:0},而是变成了

1
2
3
4
5
6
{__ob__: _e}
n: (...)
__ob__: _e {value: {…}, dep: oe, vmCount: 1}
get n: ƒ ()
set n: ƒ (e)
__proto__: Object

类似上述模样,n 不再是一个单纯的 0 —— 尽管你访问 rawData.n 的时候,还是可以取到 0

这是怎么回事呢??

原来 vue 在通过 data 取得实例数据之后,立即对实例数据进行了包装,将实例对象内的所有值都通过 Object.defineProperty 转化为了一组 getter/setter,然后拦截其 set 事件,获取更新消息,及时刷新视图

新增字段

上面提到,当在视图中使用了 data 中的数据,且该数据已经设置了监听时,数据更新会触发视图更新

但是这个自动设置监听的事件,是发生在生命周期的 beforeCreate 到 created 阶段执行的,简称为初始化阶段完成了数据监听的设置

1
2
3
4
5
6
7
8
9
10
const vm = new Vue({
el: '#app',
template: `<div>{{a}}{{b}}</div>`,
data(){
return {
a: 'x'
}
}
})
vm.b = 'b'

如上代码,视图上只会显示字符串 x,字段 b 因为是后加入的字段,所以没有设置监听

那不是初始化阶段添加的数据,要怎么渲染呢

一般有两种办法

  1. 预先设置占位符
  2. 使用 vue 提供的设置方法

对于占位符法,非常简单,只需要先定义好 b 就行了

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
template: `<div>{{a}}{{b}}</div>`,
data(){
return {
a: 'x',
b: ''
}
}
})
vm.b = 'b'

这样就会刷新视图

但是一般不知道未来会有多少字段,所以一般用 vue 提供的设置方法 Vue.set

定义如下

1
2
3
4
5
6
7
Vue.set( target, propertyName/index, value )

// 使用例
Vue.set(vm, 'b', '233') // 令 vm.b = '233'

// 也可以设置到子对象
Vue.set(vm.obj, 'n', 0) // 令 vm.obj.n = 0

第一个参数是要设置的目标,第二个是字段名,第三个是字段值

上例中为 vm 这个实例设置字段 b,并将其值设置为字符串 233

通过这种方法添加的字段,自动带有监听器,更新时可以触发视图刷新

当 vue 实例是具名实例(如 vm)时,也可以使用 vm.$set 来设置字段,使用方法同 Vue.set,因为 vm.$set 就是 Vue.set 的一个别名

数组变异方法

需要注意的是,虽然数组定义约等于对象定义,但是 Vue.setvm.$set 不可以为数组内元素添加监听器

为了监听数组的变化,vue 提供了 7 个基于 Array 的变异方法

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

之所以称之为”变异方法”,是因为 vue 实际上是对原方法进行了代理,除了正常调用原方法外,还会额外将数组变化通知给 vue 示例,从而在通过以上 7 个方法操作数组的时候,视图也会更新

nextTick

刚才虽然说,不通过 Vue.set 添加的属性,是没有监听器,不能渲染到页面上的

但是还是有特例

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: '#app',
template: `<div>{{a}}{{b}}</div>`,
data(){
return {
a: 'x'
}
}
})
vm.a = 'a'
vm.b = 'b'

本来 b 应该说是后加入且没有监听器的,所以 b 理论上不会出现在视图上

但实际情况是,字符串 a 和 b 都出现在了视图上,也就是说两句赋值都成功了

这是怎么回事呢

结合之前学习的 javascript 事件循环,原来 vue 会缓冲在一次操作中涉及的视图变化,在当前宏任务中不会渲染视图,而是在下一个事件循环开始前才渲染视图

所以此处发生的情况如下

  1. 改变了 vm.a,且 vm.a 有设置监听器,所以 vue 收到通知,将在下一个事件循环开始前渲染视图

  2. 当前事件循环还没结束,又对 vm.b 进行了赋值,所以现在 vm 上也有了数据字段 b

    此时 b 上虽然没有监听器,但是监听器的作用就是触发视图渲染,而视图渲染已经被 a 触发了,所以 b 是躺赢2333

    当然,此处是先改变 a,如果后改变 a,效果也是一样的,只要触发渲染,就会出现 b

  3. 当前事件循环结束,渲染视图,b 搭上了便车

所以,有时候会因为某些后添加的变量搭上了便车,而造成一种这个变量也设置了监听的错觉,会引发一些潜在的 bug

出于面试需要,了解这个搭便车就可以了,实际写码的时候建议使用 Vue.set


感谢阅读

--It's the end.Thanks for your read.--