Vue.js数据响应性原理
Vue.js最独特的特性之一,就是其非侵入性的响应性。当要修改数据时,无需调用API,直接修改数据,页面的视图就会自动更新。本文会深入了解一下Vue.js数据响应性的原理。
侵入式与非侵入式
侵入式
React数据变化
1
2
3this.setState({
a: this.state.a + 1
})微信小程序数据变化
1
2
3this.setData({
a: this.data.a + 1
})
非侵入式
Vue数据变化
1
this.a++
可以看出侵入式在修改数据时,都调用了框架提供的API来使数据发生变化,在函数中可以包含修改页面HTML内容的代码;但是非侵入式并没有调用API,而是直接修改页面的数据,就可以使页面HTML发生变化,这就是Vue.js的数据响应式的神奇之处。
Object.defineProperty()
JavaScript中有两种方法可以侦测数据变化:Object.defineProperty()
和ES6的Proxy
。在Vue2开发时由于浏览器对ES6的支持度并不理想,所以Vue2中是使用Object.defineProperty()
来实现数据侦测的。
Object.defineProperty()
是JavaScript引擎赋予的功能,可以检测对象属性的变化,从IE8开始兼容。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
1 | var obj = {} |
Object.defineProperty()
不仅可以添加或修改对象的属性,还可以定义这些属性的额外选项,如是否可被枚举、是否可修改、getter
、setter
等,Vue.js用到了这一特性,通过改写对象的setter
,实现数据的响应性。setter
/getter
是存取描述符,value
是数据描述符,在Object.defineProperty()
中这两种属性不能被同时定义。
1 | var obj = {} |
defineReactive
由于直接在Object.defineProperty
中定义getter/setter需要临时变量周转,我们可以把Object.defineProperty
闭包实现,封装成defineReactive
函数:
1 | function defineReactive(data, key, value) { |
递归侦测数据全部属性
现在有这样一个对象:
1 | var obj = { |
然后我们对obj
运行defineReactive
函数,并访问obj.a.b.c
:
1 | defineReactive(obj, "a") |
控制台只打印了尝试访问a属性,并没有打印尝试访问b/c属性,这是因为并不是obj对象的任何一部分都是响应性的,我们可以新建一个Observer
类解决该问题。
Observer
类的目的是将一个对象转化为每一个层级的属性都是响应式的对象,给每一层都实例化一个Observer
,来响应数据的变化,__ob__
就是那个观察者。
1 | export default class Observer { |
通过Observer
类实现了对象的每一个属性都是响应性的,但是如果对象内有嵌套的属性呢?我们可以使用递归来完成嵌套属性的数据劫持:
1 | import Observer from "./Observer.js"; |
1 | var obj = { |
三个函数的调用关系如下图:

数组响应式
使用上面的代码对数组进行操作:
1 | var obj = { |
我们发现执行obj.a.push(4)
时,控制台没有打印尝试设置a属性
,原因是JavaScript通过Array
的原型里的方法来对数组进行读写,因此Object
的getter/setter就不管用了。解决的办法是改写Array原型中可以改变数组本身的7个方法。思路就是以Array.prototype
为原型新建一个对象,然后重写对象上七个方法,加上数据劫持的代码,然后将Array的原型指向这个对象。
拦截器
拦截器就是一个和Array.prototype
一样的对象,只不过拦截器中可以修改数组本身的方法会被我们改写:
1 | import { def } from "./utils.js" |
在上面的代码中,我们创建了变量arrayMethods
,它就是拦截器,之后我们会使用它去覆盖Array.prototype
。
接着我们要在arrayMethods
中使用Object.defineProperty
方法对那七个可以改变数组自身的方法进行改写。
所以今后在调用Array.prototype.push
方法时,其实调用的是arrayMethods.push
,而arrayMethods
是函数mutator
,也就是说实际执行的是mutator
函数,mutator
中执行原函数,做它该做的事,然后我们可以在mutator
函数中做一些其他的事,比如发送变化通知。
使用拦截器覆盖Array原型
有了拦截器需要让它生效,就要用它覆盖Array.prototype
,但是我们不能直接覆盖,因为这样会污染全局的Array对象,我们只希望拦截那些响应性的数组。在前面我们将对象转换为响应性对象,是通过Observer的,数组也一样,我们只需要在Observer中添加拦截器覆盖那些将被转换为响应性数组的数组的原型就好了:
1 | import { def } from "./utils.js" |
侦测数组中元素的变化
前面说过侦测数组的变化,是指侦测数组元素的增加或减少,数组中保存了的数据的变化也是需要被侦测的,此外,向数组中添加新的元素,这个元素也需要被侦测。也就是说响应式数组中的全部子数据都要被侦测。
前面对象的数据侦测是使用Observer
实现的,现在Observer
不仅可以侦测对象,还可以侦测数组了。所以我们要对Observer
做一些处理,让它可以处理数组:
1 | import { def } from "./utils.js" |
侦测数据新元素的变化
数组添加的新元素也需要是响应性的,其实并不难,只要获取到数组的新元素,并将它变成响应性就好了:
1 | import { def } from "./utils.js" |
现在数组的响应性就算是完成了,我们来测试一下:
1 | import { observe } from "./observe.js"; |
结果如下
1 | node index.js |
可以发现通过打点调用数组的方法时,会打印出arrayIntercepted,
依赖收集
如果只是把Object.defineProperty()
进行封装,那其实没什么实际用处,真正有用的是收集依赖。
Vue中需要用到数据的地方称为依赖,举个例子:
1 | <template> |
这个模板中使用了数据name,所以当name发生变化时,要向使用它的地方发送通知。先收集依赖,就是说把用到name的地方都收集起来,然后等name发生变化时候,把之前收集好的依赖循环触发一边就好了。换句话收就是在getter中收集依赖,在setter中触发依赖。
依赖收集到哪里
每一个数据都要有一个数组属性,用来存储当前数据的依赖,假设依赖是一个函数,保存在window.target
上,现在我们可以把defineReactive
函数改写一下:
1 | import { observe } from "./observe.js"; |
上面的代码添加了数组dep
,用来存储被收集的依赖,然后在set
被触发时,循环触发收集到的依赖。但是这样子写有点耦合,我们可以把dep
封装成一个Dep
类,用来专门管理依赖:
1 | var uid = 0; |
然后再修改一下defineReactive
:
1 | import { observe } from "./observe.js"; |
这样代码开起来清晰多了,这样也解决了依赖收集的问题。
依赖究竟是什么
上面的代码中,我们收集的依赖是window.target
,它到底是什么?
收集的依赖是什么,就是当数据发生变化时,要通知谁。我们要通知用到数据的地方,这个地方可能有很多,可能是模板,可能是watch
,我们需要一个类来集中处理这些情况。我们在收集依赖的时候就把这个类的实例收集进来,通知的时候就通知它一个,然后再由它通知别的地方,在Vue中,这个类叫做Watcher
。
这样就回答了上面的问题,我们收集的依赖是Watcher
。
Watcher
Watcher
可以理解为一个中介,数据发生变化的时候通知它,它再通知别的地方。
Watcher
有一个经典的使用方式:
1 | $vm.watch("a.b.c", function (oldval, newval) { |
这段代码表示当data.a.b.c
发生变化时,会触发第二个参数中的回调。
如何实现这个功能?我们只要把Watcher
实例添加到data.a.b.c
的Dep中,然后当data.a.b.c
发生变化时,由Dep
通知Watcher
,Watcher
再执行参数中的回调。
根据Watcher
的功能,我们写出下面的代码:
1 | import { parsePath } from "./utils"; |
上面的代码中的Watcher
可以自动把自己添加到Dep
中去,从而实现依赖收集。上面代码中的parsePath
解析了数据的路径,返回一个函数,就是getter
,用来触发数据的get
,下面是它的实现原理:
1 | const bailRE = /[^\w.$]/; |
数组的依赖收集
当侦测到数组发生变化时,会向依赖发送通知,此时,要先能访问到依赖,前面的数据拦截器中已经可以访问到Observer
实例了,所以这里就只要在Observer
实例中拿到Dep
,然后发送通知就好了:
1 | methodsToPatch.forEach(function (method) { |
总结
- Vue的数据响应性核心就是
observe
,将数据设置为响应性对象,observe
,Observer
,defineReactive
三者互相调用,递归地将数据设置为响应性对象。 - 使用拦截器对数组的原型进行替换,实现数组的响应性。
Dep
和Watcher
用于依赖收集,Dep
是依赖列表,Watcher
是依赖,在getter
中收集依赖,在setter
中触发依赖。
贴上Vue官网的一张图,可以更好的理解

代码
array.js
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
33
34
35
36
37import { def } from "./utils.js"
// 复制数组的prototype
const arrayProto = Array.prototype;
// 创建拦截器,并将其导出
export const arrayMethods = Object.create(arrayProto);
// 7个会改变对象本身的方法
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
methodsToPatch.forEach(function (method) {
// 复制原来的方法,原来的方法本身作用不能被剥夺
const original = arrayProto[method];
def(arrayMethods, method, function (...args) {
var result = original.apply(this, args);
// 获取数组的Observer
const ob = this.__ob__;
// 获取数组的新增元素
let inserted = [];
switch (method) {
case "push":
case "unshift":
inserted = arguments;
break;
case "splice":
inserted = [...arguments][2];
break;
}
// 如果有新增的元素,那就把它变得响应性
if (inserted) {
ob.observeArray(inserted)
}
// 通知数组的Observer,数组发生了变化,通知数组的每一个Watcher,数组发生了变化,让它们重新渲染
ob.dep.notify()
console.log("arrayIntercepted");
return result;
}, false)
})defineReactive.js
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
33
34
35
36
37import { observe } from "./observe.js";
import Dep from "./Dep.js";
export default function defineReactive(data, key, value) {
console.log("defineReactive", data, key);
const dep = new Dep()
// 如果只有两个参数,则让value等于要设置的属性的值
if (arguments.length == 2) {
value = data[key];
}
var childOb = observe(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`尝试访问${key}属性`);
dep.depend(); // 收集依赖
if (childOb) {
childOb.dep.depend();
}
return value;
},
set(newValue) {
console.log(`尝试设置${key}属性`, newValue);
if (value === newValue) {
return;
}
value = newValue;
childOb = observe(newValue)
dep.notify()
}
});
}Dep.js
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
33
34
35
36
37
38
39
40
41var uid = 0;
export default class Dep {
constructor() {
console.log("Dep constructor");
this.subs = [];
this.id = uid++;
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
depend() {
if (window.target) {
this.addSub(window.target);
}
}
notify() {
console.log("dep notify");
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}observe.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import Observer from "./Observer.js";
export function observe(value) {
// 如果value不是对象就什么都不做,保证程序不会死循环
if (typeof value != 'object')
return;
var ob;
// 如果value没有__ob__,就实例化一个Observer
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}Observer.js
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import { def } from "./utils.js"
import defineReactive from "./defineReactive.js"
import { arrayMethods } from "./array.js";
import { observe } from "./observe.js";
import Dep from "./Dep.js";
// 覆盖数组原型的早期实现
// const hasProto = '__proto__' in {}
// const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export default class Observer {
constructor(value) {
this.dep=new Dep()
// 给实例添加__ob__属性,这里的this指的是实例而不是类本身
def(value, '__ob__', this, false)
console.log("Observer constructor", value);
// 如果数据类型是数组,那么就覆盖数组的原型,并且让数组变得响应性
if (Array.isArray(value)) {
// 覆盖数组原型ES6写法
Object.setPrototypeOf(value, arrayMethods)
// 覆盖数组原型早期写法
// if (hasProto) {
// value.__proto__ = arrayMethods
// } else {
// for (let i = 0, l = arrayKeys.length; i < l; i++) {
// const key = arrayKeys[i]
// def(value, key, arrayMethods[key])
// }
// }
// 让数组变得响应性
this.observeArray(value)
} else {
// 否则就遍历对象
this.walk(value);
}
}
// 遍历
walk(value) {
// 把实例里每一个属性都设置成响应性的
for (let k in value) {
defineReactive(value, k);
}
}
// 让数组的每一个数据都变得响应性
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25export function def(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
const bailRE = /[^\w.$]/;
export function parsePath(path) {
if (bailRE.test(path)) {
return;
}
const segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return;
}
obj = obj[segments[i]];
}
return obj;
}
}Watcher.js
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
33
34
35
36
37
38import { parsePath } from "./utils.js ";
var uid = 0;
export default class Watcher {
constructor(vm, expOrFn, cb) {
console.log("Watcher constructor");
this.id = uid++;
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
get() {
console.log("Watcher get");
// 把当前的Watcher实例赋值给Dep.target
window.target = this;
/*
* 触发getter,在getter中会去依赖收集,就是defineReactive中get()的dep.depend()
* 这里会把当前的watcher实例添加到dep的subs中,
* 这样dep就知道了当前的watcher实例,当dep的值发生变化时,
* 就会通知当前的watcher实例,从而触发watcher的update方法,
* 从而触发cb,从而更新视图,这就是依赖收集的过程
* 这里的getter就是parsePath返回的函数
* 这里的call函数第一个参数是函数的this指向,第二个参数是函数的参数
*/
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update() {
console.log("Watcher update");
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}