对象变化侦测原理(二)

二、Object 变化侦测

变化侦测指的是,当运行时的状态发生变化时,应用程序可以知道哪个状态发生了变化,并作出相应的动作。

变化侦测的方式可以分为2种:一种是“推(push)”,一种是“拉(pull)”。

Vue 的变化侦测就属于“推,当状态发生变化时,它就会通知相应的依赖对象进行更新。

2.1 追踪变化

前面说过,Object 的属性可以分为2种:数据属性和访问属性。

Vue 就是通过利用访问属性的 settergetter 函数来追踪对象的变化的。

每当对象属性被读取时,getter 函数就会被触发;每当对象属性更新时,setter 函数就会被触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var isStateChange = false;

Object.defineProperty(objProxy, 'access', {
get: function () {
return obj.data;
},
set: function (val) {
obj.data = val;
isStateChange = true;
}
})

objProxy.access
alert(isStateChange) // false

objProxy.access = '111'
alert(isStateChange) // true

2.2 收集依赖

知道了如何监听对象的变化,但是应该如何收集依赖呢?也就是说当状态发生变化后,该向谁通知状态变更呢?

收集依赖需要分为3步:要收集什么?什么时候收集?收集到哪里?

  • 1、要收集什么?

哪些算是依赖?其实就是用到了对象属性的地方就是依赖。比如说:

1
2
3
<template>
<div>{{ obj.name }}</div>
</template>

对于上述模板,它调用了 name 属性,也就是对 obj.name 产生了依赖,当 obj.name 发生变化时,就需要通知模板进行重新渲染。

对于这种调用了对象属性的地方,都是需要收集起来的。

  • 2、什么时候收集?

前面已经说过了,Vue 是通过利用访问属性的 settergetter 函数来进行侦听变化的。

因此收集原理也很简单,在 getter 中收集依赖,在 setter 中触发依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var isStateChange = false;

Object.defineProperty(objProxy, 'access', {
get: function () {
return obj.data;
},
set: function (val) {
obj.data = val;
isStateChange = true;
}
})

objProxy.access
alert(isStateChange) // false

objProxy.access = '111'
alert(isStateChange) // true
  • 3、收集到哪里?

对于这个,可以简单地为每一个属性创建一个局部变量 dep 来保存。

假设依赖对象是一个函数,并且保存在 window.target 中,那么依赖的收集和通知实现可以如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function defineReactive (obj, key, val) {
let dep = []; // 保存依赖的地方
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 收集依赖
// 假设依赖对象保存在 window.target 中
dep.push(window.target);
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
// 触发依赖
for (let i = 0; i < dep.length; i++) {
dep[i](newVal, val)
}
val = newVal;
}
})
}

按照这种实现方式,每个对象属性都拥有自己的依赖收集器 dep

当然,实际的 dep 并不是那么简单的一个数组,为了减低代码的耦合性,Vue 中把它实现为一个类 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
class Dep {
constructor () {
// 依赖,订阅者
this.subs = []
}

addSub (sub) {
this.subs.push(sub)
}

removeSub (sub) {
remove(this.subs, sub)
}

depend () {
// 假设依赖对象保存在 window.target 中
if (window.target) {
this.addSub(window.target)
}
}

notify () {
const subs = this.subs.slice()
for (let i = 0, l = subd.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)
}
}
}

根据新的 dep,修改 defineReactive 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function defineReactive (obj, key, val) {
let dep = new Dep(); // 修改1
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 收集依赖
dep.depend(); // 修改2
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal;
// 触发依赖
dep.notify(); // 修改3
}
})
}

至此,收集依赖的工作基本完成了。

等等,还有一件重要的事,究竟什么是依赖呢?它到底从哪来的?虽然在前面的代码里,假设依赖是一个函数,并且已经设置到了 window.target 中, 但是并没有具体说明它是从哪里来的,是什么时候设置到 window.target 的。下面详细说明一下它。

  • 依赖对象

前面说过,依赖实际上是用到对象属性的地方,但是用到对象属性的地方太多了,而且类型可能不一样,环境也不一样。这个时候为了能够统一处理依赖对象,需要将其抽象成一个依赖对象,这个依赖对象就称为观察者 Watcher,它就像是一直在观察对象属性的变化一样,当对象属性发生变化后,它就可以通过“观察”发现,从而进行相应的动作。

在 Vue 中,Watcher 是一个中介对象,当数据发生变化时,会通知到观察者 Watcher,然后 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
class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm;
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.cb = cb;
this.value = this.get();
}

get () {
// 先把this设置到window.target中,然后再读取对象属性
window.target = this;
// 这时候被读到的对象属性就会把当前this收集到它的依赖集合里面
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}

update () {
const oldValue = this.value;
this.value = this.get();
this.cb(this.vm, this.value, oldValue);
}
}

收集依赖的过程实际上就在 getter 函数被调用的时候,那么 getter 函数具体是什么呢?也就是 parsePath 的实现是怎么样的呢?

其实 parsePath 的参数 path 是类似 a.b.c 这样的形式,如果用过 Vue 的话,就应该知道 Vue 中可以利用 vm.$watch('a,b.c', function(){}) 这种形式来监听对象属性的变化。

1
2
3
4
5
6
7
8
9
10
function parsePath (path) {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

实际原理很简单,parsePath 只是按照分隔符 . 来逐层地访问对象属性。

而前面说过,当读取对象的访问属性时,会触发依赖收集。所以当 getter 函数执行时,它其实就是在读取对象的访问属性,这个时候就会触发依赖收集的逻辑。

2.3 监测所有属性

对于每个属性,如果需要监测它的变化,必须经过 defineReactive 来处理,这样它才能像访问属性那样收集依赖,实现响应式侦听。

为了提高代码的封装,Vue 中封装了一个监听类 Observer 类,用于监听对象的所有属性。

Observer 的作用就是将对象的所有属性都转换成访问属性,通过访问属性的 settergetter 来实现变化追踪:

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
class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}

walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}

function defineReactive (obj, key, val) {
// 递归子属性
if (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 收集依赖
dep.depend();
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal;
// 触发依赖
dep.notify();
}
})
}

Observer 的实现原理很简单,就是把所有属性都遍历一遍,利用 defineReactive 将属性都转成可追踪的访问属性。

2.4 存在问题

Object 类型数据的变化侦听,其原理是通过访问属性 setter/getter 方法来实现的。

但是这种方式还存在一些问题,比如添加新属性、删除新属性时,setter/getter 是不会触发的,这些情况就没办法追踪属性的变化了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var isStateChange = false;

Object.defineProperty(objProxy, 'access', {
get: function () {
isStateChange = true;
return obj.data;
},
set: function (val) {
obj.data = val;
isStateChange = true;
}
})

// 删除属性不会触发set/get
delete objProxy.access
alert(isStateChange) // false

// 新增属性也不会触发set/get
objProxy.name = 'newProp'
alert(isStateChange) // false

setter/getter 只能跟踪属性值是否发生修改,但是没办法侦测到新增属性和删除属性。

对象变化侦测原理(二)

http://example.com/framework/vuejs/dection02/

作者

jiaduo

发布于

2021-08-28

更新于

2023-04-02

许可协议