Vue.js数据响应性原理

Vue.js最独特的特性之一,就是其非侵入性的响应性。当要修改数据时,无需调用API,直接修改数据,页面的视图就会自动更新。本文会深入了解一下Vue.js数据响应性的原理。

侵入式与非侵入式

侵入式

  • React数据变化

    1
    2
    3
    this.setState({
    a: this.state.a + 1
    })
  • 微信小程序数据变化

    1
    2
    3
    this.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
2
3
4
var obj = {}
Object.defineProperty(obj, "a", { value: 1 })
Object.defineProperty(obj, "b", { value: 2 })
console.log(obj.a,obj.b) // 1 2

Object.defineProperty()不仅可以添加或修改对象的属性,还可以定义这些属性的额外选项,如是否可被枚举、是否可修改、gettersetter等,Vue.js用到了这一特性,通过改写对象的setter,实现数据的响应性。setter/getter是存取描述符,value是数据描述符,在Object.defineProperty()中这两种属性不能被同时定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = {}
var aValue // getter/setter需要临时变量周转
Object.defineProperty(obj, "a", {
get() {
console.log("尝试访问a属性")
return aValue
},
set(v){
console.log("尝试设置a属性")
aValue=v
}
})

console.log(obj.a)
obj.a=1
console.log(obj.a)
// 尝试访问a属性
// undefined
// 尝试设置a属性
// 尝试访问a属性
// 1

defineReactive

由于直接在Object.defineProperty中定义getter/setter需要临时变量周转,我们可以把Object.defineProperty闭包实现,封装成defineReactive函数:

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
function defineReactive(data, key, value) {
if (arguments.length == 2) {
value=data[key]
}
Object.defineProperty(data, key, {
get() {
console.log(`尝试访问${key}属性`)
return value
},
set(newValue) {
console.log(`尝试设置${key}属性`, newValue)
if (value === newValue) return
value = newValue
}
})
}

var obj = {}

defineReactive(obj, "a", 10)
console.log(obj.a)
obj.a = 11
console.log(obj.a)
// 尝试访问a属性
// 10
// 尝试设置a属性 11
// 尝试访问a属性
// 11

递归侦测数据全部属性

现在有这样一个对象:

1
2
3
4
5
6
7
var obj = {
a: {
b: {
c: 1
}
}
}

然后我们对obj运行defineReactive函数,并访问obj.a.b.c

1
2
3
4
defineReactive(obj, "a")
console.log(obj.a.b.c)
// 尝试访问a属性
// 1

控制台只打印了尝试访问a属性,并没有打印尝试访问b/c属性,这是因为并不是obj对象的任何一部分都是响应性的,我们可以新建一个Observer类解决该问题。

Observer类的目的是将一个对象转化为每一个层级的属性都是响应式的对象,给每一层都实例化一个Observer,来响应数据的变化,__ob__就是那个观察者。

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
export default class Observer {
constructor(value) {
// 给实例添加__ob__属性,这里的this指的是实例而不是类本身
def(value, '__ob__', this, false)
console.log("constructor", value);
// 遍历对象
this.walk(value)
}
walk(value) {
// 把实例里每一个属性都设置成响应性的
for (let k in value) {
defineReactive(value, k)
}
}
}

// 我们想要__ob__是一个隐藏的属性,所以要设置enumerable为false
def(obj,key,value,enumerable) {
Object.defineProperty(obj,key,{
value,
enumerable,
writable:true,
configurable:true
})
}
说白了defineReactive只能处理普通类型值,深层次对象需要observe一层一层拿出来给defineReactive

通过Observer类实现了对象的每一个属性都是响应性的,但是如果对象内有嵌套的属性呢?我们可以使用递归来完成嵌套属性的数据劫持:

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
import Observer from "./Observer.js";

// 函数入口observe
export function observe(value) {
// 如果value不是对象就什么都不做,保证程序不会死循环
if (typeof value != 'object')
return;
var ob;
// 如果对象没有__ob__,就实例化一个Observer
// 实例化Observer时,在walk函数中会对对象的每一个属性调用defineReactive函数
if (typeof value.__ob__ !== 'undefined') {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob;
}

function defineReactive(data, key, value) {
console.log("defineReactive", data, key);
// 如果只有两个参数,则让value等于要设置的属性的值
if (arguments.length == 2) {
value = data[key];
}
// 在defineReactive函数中,会对每一个属性的值(就是子元素)调用observe函数,
// 如果属性的值还是一个对象,就会重复上面的操作,如果不是对象,就会使它具有具有响应性
// 由此完成了递归的操作
observe(value)

Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`尝试访问${key}属性`);
return value;
},
set(newValue) {
console.log(`尝试设置${key}属性`, newValue);
if (value === newValue)
return;
value = newValue;
// 新设置的值也要observe
observe(newValue)
}
});
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
a: {
c: 1
},
b: 10
}

observe(obj)
// 函数入口 observe(obj)
// |- new Observer(),并遍历对象,对每一个属性执行defineReactive函数,使每一个属性都具有数据响应性
// |- defineReactive(obj,a)
// |- observe(obj.a),发现是一个对象
// |- new Observer(),遍历obj.a,对每一个属性执行defineReactive函数,使每一个属性都具有数据响应性
// |- defineReactive(obj.a,c)
// |- observe(obj.a.c),发现不是一个对象,直接返回,执行defineReactive剩下的代码,为其设置getter/setter
// |- defineReactive(obj,b)
// |- observe(obj.b),发现不是对象,直接返回,执行defineReactive剩下的代码,为其设置getter/setter
// 执行完毕

三个函数的调用关系如下图:

数组响应式

使用上面的代码对数组进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
a: [1, 2, 3]
}

observe(obj)
console.log(obj.a)
obj.a.push(4)
console.log(obj.a)

// 尝试访问a属性
// 尝试访问a属性
// 尝试访问a属性

我们发现执行obj.a.push(4)时,控制台没有打印尝试设置a属性,原因是JavaScript通过Array的原型里的方法来对数组进行读写,因此Object的getter/setter就不管用了。解决的办法是改写Array原型中可以改变数组本身的7个方法。思路就是以Array.prototype为原型新建一个对象,然后重写对象上七个方法,加上数据劫持的代码,然后将Array的原型指向这个对象。

拦截器

拦截器就是一个和Array.prototype一样的对象,只不过拦截器中可以修改数组本身的方法会被我们改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { 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) {
console.log("arrayIntercepted");
return original.apply(this, args);
}, false)
})

在上面的代码中,我们创建了变量arrayMethods,它就是拦截器,之后我们会使用它去覆盖Array.prototype

接着我们要在arrayMethods中使用Object.defineProperty方法对那七个可以改变数组自身的方法进行改写。

所以今后在调用Array.prototype.push方法时,其实调用的是arrayMethods.push,而arrayMethods是函数mutator,也就是说实际执行的是mutator函数,mutator中执行原函数,做它该做的事,然后我们可以在mutator函数中做一些其他的事,比如发送变化通知。

使用拦截器覆盖Array原型

有了拦截器需要让它生效,就要用它覆盖Array.prototype,但是我们不能直接覆盖,因为这样会污染全局的Array对象,我们只希望拦截那些响应性的数组。在前面我们将对象转换为响应性对象,是通过Observer的,数组也一样,我们只需要在Observer中添加拦截器覆盖那些将被转换为响应性数组的数组的原型就好了:

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
import { def } from "./utils.js"
import defineReactive from "./defineReactive.js"
import { arrayMethods } from "./array.js";
import { observe } from "./observe.js";

// 覆盖数组原型的早期实现
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export default class Observer {
constructor(value) {
// 给实例添加__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])
}
}
} else {
// 否则就遍历对象
this.walk(value);
}
}

// 遍历
walk(value) {
// 把实例里每一个属性都设置成响应性的
for (let k in value) {
defineReactive(value, k);
}
}
}

侦测数组中元素的变化

前面说过侦测数组的变化,是指侦测数组元素的增加或减少,数组中保存了的数据的变化也是需要被侦测的,此外,向数组中添加新的元素,这个元素也需要被侦测。也就是说响应式数组中的全部子数据都要被侦测。

前面对象的数据侦测是使用Observer实现的,现在Observer不仅可以侦测对象,还可以侦测数组了。所以我们要对Observer做一些处理,让它可以处理数组:

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
import { def } from "./utils.js"
import defineReactive from "./defineReactive.js"
import { arrayMethods } from "./array.js";
import { observe } from "./observe.js";

// 覆盖数组原型的早期实现
// const hasProto = '__proto__' in {}
// const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export default class Observer {
constructor(value) {
// 给实例添加__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])
}
}
}

侦测数据新元素的变化

数组添加的新元素也需要是响应性的,其实并不难,只要获取到数组的新元素,并将它变成响应性就好了:

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
import { 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)
}
console.log("arrayIntercepted");
return result;
}, false)
})

现在数组的响应性就算是完成了,我们来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { observe } from "./observe.js";

var obj = {
a: [1, 2, 3]
}

observe(obj)
console.log(obj.a)
obj.a.push(2, 1)
console.log(obj.a)
obj.a.pop()
console.log(obj.a);
obj.a[1] = 6
console.log(obj.a);

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ node index.js
Observer constructor { a: [ 1, 2, 3 ] }
defineReactive { a: [ 1, 2, 3 ] } a
Observer constructor [ 1, 2, 3 ]
尝试访问a属性
[ 1, 2, 3 ]
尝试访问a属性
arrayIntercepted
尝试访问a属性
[ 1, 2, 3, 2, 1 ]
尝试访问a属性
arrayIntercepted
尝试访问a属性
[ 1, 2, 3, 2 ]
尝试访问a属性
尝试访问a属性
[ 1, 6, 3, 2 ]

可以发现通过打点调用数组的方法时,会打印出arrayIntercepted,

依赖收集

如果只是把Object.defineProperty()进行封装,那其实没什么实际用处,真正有用的是收集依赖。

Vue中需要用到数据的地方称为依赖,举个例子:

1
2
3
4
5
<template>
<div>
{{name}}
</div>
</template>

这个模板中使用了数据name,所以当name发生变化时,要向使用它的地方发送通知。先收集依赖,就是说把用到name的地方都收集起来,然后等name发生变化时候,把之前收集好的依赖循环触发一边就好了。换句话收就是在getter中收集依赖,在setter中触发依赖。

依赖收集到哪里

每一个数据都要有一个数组属性,用来存储当前数据的依赖,假设依赖是一个函数,保存在window.target上,现在我们可以把defineReactive函数改写一下:

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
import { observe } from "./observe.js";

export default function defineReactive(data, key, value) {
console.log("defineReactive", data, key);

// 新增,dep是dependency的缩写
let 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.push(window.target);
return value;
},
set(newValue) {
console.log(`尝试设置${key}属性`, newValue);
if (value === newValue) {
return;
}

// 新增,触发依赖
for (let i = 0; i < dep.length; i++) {
dep[i](newValue, value);
}
value = newValue;
childOb = observe(newValue)
}
});
}

上面的代码添加了数组dep,用来存储被收集的依赖,然后在set被触发时,循环触发收集到的依赖。但是这样子写有点耦合,我们可以把dep封装成一个Dep类,用来专门管理依赖:

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
var 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)
}
}
}

然后再修改一下defineReactive

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
import { 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();
return value;
},
set(newValue) {
console.log(`尝试设置${key}属性`, newValue);
if (value === newValue) {
return;
}
value = newValue;
childOb = observe(newValue)
dep.notify()
}
});
}

这样代码开起来清晰多了,这样也解决了依赖收集的问题。

依赖究竟是什么

上面的代码中,我们收集的依赖是window.target,它到底是什么?

收集的依赖是什么,就是当数据发生变化时,要通知谁。我们要通知用到数据的地方,这个地方可能有很多,可能是模板,可能是watch,我们需要一个类来集中处理这些情况。我们在收集依赖的时候就把这个类的实例收集进来,通知的时候就通知它一个,然后再由它通知别的地方,在Vue中,这个类叫做Watcher

这样就回答了上面的问题,我们收集的依赖是Watcher

Watcher

Watcher可以理解为一个中介,数据发生变化的时候通知它,它再通知别的地方。

Watcher有一个经典的使用方式:

1
2
3
$vm.watch("a.b.c", function (oldval, newval) {
console.log("watcher callback: " + oldval + " " + newval);
}

这段代码表示当data.a.b.c发生变化时,会触发第二个参数中的回调。

如何实现这个功能?我们只要把Watcher实例添加到data.a.b.c的Dep中,然后当data.a.b.c发生变化时,由Dep通知WatcherWatcher再执行参数中的回调。

根据Watcher的功能,我们写出下面的代码:

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
import { parsePath } from "./utils";

export default class Watcher {
constructor(vm, expOrFn, cb) {
console.log("Watcher constructor");
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);
}
}

上面的代码中的Watcher可以自动把自己添加到Dep中去,从而实现依赖收集。上面代码中的parsePath解析了数据的路径,返回一个函数,就是getter,用来触发数据的get,下面是它的实现原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

数组的依赖收集

当侦测到数组发生变化时,会向依赖发送通知,此时,要先能访问到依赖,前面的数据拦截器中已经可以访问到Observer实例了,所以这里就只要在Observer实例中拿到Dep,然后发送通知就好了:

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
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)
})

总结

  1. Vue的数据响应性核心就是observe,将数据设置为响应性对象,observeObserverdefineReactive三者互相调用,递归地将数据设置为响应性对象。
  2. 使用拦截器对数组的原型进行替换,实现数组的响应性。
  3. DepWatcher用于依赖收集,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
    37
    import { 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
    37
    import { 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
    41
    var 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
    15
    import 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
    52
    import { 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
    25
    export 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
    38
    import { 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);
    }
    }