二、Object 变化侦测
变化侦测指的是,当运行时的状态发生变化时,应用程序可以知道哪个状态发生了变化,并作出相应的动作。
变化侦测的方式可以分为2种:一种是“推(push)”,一种是“拉(pull)”。
Vue 的变化侦测就属于“推,当状态发生变化时,它就会通知相应的依赖对象进行更新。
2.1 追踪变化
前面说过,Object 的属性可以分为2种:数据属性和访问属性。
Vue 就是通过利用访问属性的 setter
和 getter
函数来追踪对象的变化的。
每当对象属性被读取时,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)
objProxy.access = '111' alert(isStateChange)
|
2.2 收集依赖
知道了如何监听对象的变化,但是应该如何收集依赖呢?也就是说当状态发生变化后,该向谁通知状态变更呢?
收集依赖需要分为3步:要收集什么?什么时候收集?收集到哪里?
哪些算是依赖?其实就是用到了对象属性的地方就是依赖。比如说:
1 2 3
| <template> <div>{{ obj.name }}</div> </template>
|
对于上述模板,它调用了 name
属性,也就是对 obj.name
产生了依赖,当 obj.name
发生变化时,就需要通知模板进行重新渲染。
对于这种调用了对象属性的地方,都是需要收集起来的。
前面已经说过了,Vue 是通过利用访问属性的 setter
和 getter
函数来进行侦听变化的。
因此收集原理也很简单,在 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)
objProxy.access = '111' alert(isStateChange)
|
对于这个,可以简单地为每一个属性创建一个局部变量 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 () { 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 () { 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(); 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(); } }) }
|
至此,收集依赖的工作基本完成了。
等等,还有一件重要的事,究竟什么是依赖呢?它到底从哪来的?虽然在前面的代码里,假设依赖是一个函数,并且已经设置到了 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 () { window.target = 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
的作用就是将对象的所有属性都转换成访问属性,通过访问属性的 setter
和 getter
来实现变化追踪:
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; } })
delete objProxy.access alert(isStateChange)
objProxy.name = 'newProp' alert(isStateChange)
|
setter/getter
只能跟踪属性值是否发生修改,但是没办法侦测到新增属性和删除属性。