Vue2 响应式原理(数据劫持 与 发布-订阅者模式)
海边的小溪鱼 2017-07-14 Vue2
Vue2 实现响应式的原理,以及存在的一些问题(手写 Vue2 响应式)
Vue 响应式指的是:data 中的数据发生变化时,视图也会随之更新
什么是数据驱动视图(MVVM)
数据驱动视图(Model-View-ViewModel):简写形式为"MVVM"
Model:表示数据层
View:表示视图层,通常就是 DOM 层
ViewModel:业务逻辑层,是 Model 与 View 的桥梁(数据变化驱动视图更新,视图变化会驱动数据更新)
Vue2 实现响应式的原理,以及存在的一些问题

- 想完成整个响应,我们需要如下过程:
- 数据劫持/代理:监听数据的变化
- 收集依赖:收集视图依赖了哪些数据
- 发布订阅模式:数据变化时,自动“通知”需要更新的视图部分,并进行更新
- 响应式原理:采用
数据劫持结合发布-订阅者模式的方式,来实现数据的响应式。- 在创建 Vue 实例时,Vue 会遍历 data 中的每一个属性,并通过
Object.defineProperty()给 data 中的每一个属性添加 getter 和 setter 方法 - 当有人
读取数据时,会触发getter方法(收集依赖),watcher(观察者)会把接触过的数据记录为依赖(会记录谁用了这个数据) - 当有人
修改这个数据数据时,会触发setter方法(派发更新),通知 watcher(观察者)触发相应的监听回调,生成新的虚拟 DOM 树,视图更新(使关联这个数据的组件重新渲染)
- 在创建 Vue 实例时,Vue 会遍历 data 中的每一个属性,并通过
- Vue2 中操作
对象和数组类型数据时,存在的一些问题- 对象类型:通过 Object.defineProperty() 数据劫持,来实现对属性的读取和修改
- 缺点:新增属性、删除属性,界面不会更新
- 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)
- 缺点:通过下标修改数组,界面不会更新
- 对象类型:通过 Object.defineProperty() 数据劫持,来实现对属性的读取和修改
注意:每个组件都对应一个 watcher,它会在组件渲染时记录这些属性,并在 setter 触发时重新渲染。
# 1. 数据劫持/拦截:Object.defineProperty()
用于劫持一个对象的属性,通常我们对属性的 getter 和 setter 方法进行劫持,在对象的属性发生变化时进行特定的操作。
Vue2 响应式核心原理:通过 Object.defineProperty() 来劫持 data 里面各个属性的 getter 和 setter方法,来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
let person = { name: "张三", age: 18 }
let p = {}
// 格式:Object.defineProperty("对象名称", "属性名称", 配置对象)
Object.defineProperty(p, "name", {
// get() 方法:当"读取"数据时会调用
get() {
console.log("有人读取了 name 属性");
return person.name
},
// set() 方法:当"修改"数据时会调用
set(value) {
console.log(`有人修改了 name 属性,新值为${value},我要去更新界面`);
person.name = value
}
// 那么问题来了,我要 添加/删除 一个属性,怎么才能被监听到呢?
// 那么问题又来了, 我要读取/修改 age 属性,又得要写一个 Object.defineProperty() 方法
// 所以 Vue2 中的响应式,还是有一些问题存在的
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意:只用 Object.defineProperty() 可以实现双向绑定,但是效率非常低,我们还需要结合发布-订阅者模式去更加精准的更新视图
# 2. Vue2 响应式(缺点)
- 新增、删除对象中的属性,页面不会更新
<template>
<div class="container">
<h2>{{ obj }}</h2>
<button @click="addObj">添加对象属性</button>
<button @click="delObj">删除对象属性</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: { name: "王五", age: 30 }
}
},
methods: {
// 1. 添加对象属性,界面不会更新
addObj() {
this.obj.sex = "男" // 添加对象属性,界面不会更新
// 解决方法:
this.$set(this.obj, "sex", "男") // 通过 $set(目标对象, 属性名, 值)
Vue.set(this.obj, "sex", "男") // 通过 Vue 身上的 set
},
// 2. 删除对象属性,界面不会更新
delObj() {
delete this.obj.name // 删除对象属性,界面不会更新
// 解决方法:
this.$delete(this.obj, "name") // 通过 $delete(目标对象, 属性名)
Vue.delete(this.obj, "name") // 通过 Vue身上的 delete
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- 通过下标修改数组,页面不会更新
<template>
<div class="container">
<h2>{{ arr }}</h2>
<button @click="changeArr">修改数组元素</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: ["张三", "李四"],
}
},
methods: {
// 1. Vue2 中通过下标修改数组,界面不会更新
changeArr() {
this.arr[0] = "小张" // 通过下标修改数组,界面不会更新
// 解决方法:
this.$set(this.arr, 0, "男") // 通过 $set(目标对象, 索引, 值)
Vue.set(this.arr, 0, "男") // 通过 Vue 身上的 set
this.arr.splice(0, 1, "男") // 通过 .splice(开始位置, 结束位置, 值)
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25