vue收集依赖watcher

前文,有聊到vue中的数据侦测机制(observer),如果实现对监听对象object和数组的数据变化。但是,如果我们只知道数据的变化,也无法及时的把这些数据更新到视图。所以,我们需要收集依赖,等数据更新了,就把收集到的依赖循环触发一遍就好了,这样数据的变化就可以及时更新到视图了。

收集依赖Dep

对于对象来说,依赖是在getter中收集,在setter中触发执行。那么,依赖存储在哪呢?vue中用了一个Dep类来管理依赖,对于响应数据对象的每一个key值,都有一个数组来存储依赖。先来看看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
export default class Dep {
export default 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,len = subs.slice.length;i<1;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
21
22
23
24
25
26
function defineReactive(obj,key,value) {
// 递归对象的值,如果值为对象,也监测
observer(value);
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable: true,
get() {
// 对于对象 我们在这里 收集依赖 watcher
dep.depend()
return value
},
set(newValue) {
if(value == newValue) {
return;
}
//对象: 在这里触发收集的依赖
value = newValue;
dep.notify()
//给某个key设置值的时候 可能也是一个对象 也需要监听
observer(newValue);

console.log('视图更新');
}
})
}

此时便已完成了对象的依赖收集,那么依赖究竟是什么呢?

依赖watcher

在上面的代码中,我们收集的是Dep.target,它就是watcher,它是一个能集中处理页面用的数据或者用户自己写的watch的一个抽象类,我们执行它里面的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default class Watcher{
constructor(vm,expOrFn,cb) {
// vue的实例对象
this.vm = vm;
// 执行getter ,就可以得到用户传进的 如 data.a.b.c的值
this.getter = parsePath(expOrFn);
this.cb = cb;
// 触发 getter 收集依赖
this.value = this.get()
}
get() {
window.target = this;
// 获取新值
let value = this.getter.call(this.vm,this.vm);
window.target = null;
return value;
}

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

我们现在get方法中把window.target设为this,即watcher的当前实例,然后读取传入的属性值得到初始值(老值),就会触发getter收集依赖watcher到监听对象相应key得dep中。以后当这个key 如data.a.b.c发生变化时,就会走setter,从而执行watcher的update方法得到数据的最新状态。我们平常用的vm.$watch('a.b.c',cb)和模板中通过指令和插值语法绑定的数据都是基于watcher。还有parsePath的原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const reg = /[^\w.$]/
export function parsePath(path) {
if (reg.test(path)) {
return;
}
const args = path.split('.');
// obj 在watcher 中值 this.vm
return function (obj) {
for (let i = 0, len = args.length; i < len; i++) {
if (!obj) {
return
}
obj = obj[args[i]]
}
}
}

先将字符用‘.’分割,然后一层层从data上取值。

object问题

至此,object类型的数据变化侦测和依赖收集触发都清晰了。但是getter/setter这种追踪方式,无法监听到在对象上,新增属性和delete,也不会通知依赖。但是官方提拱了两个API — vm.$set和vm.$delete来填坑。

数组

数组有许多的原型方法,我们在vue种用来改变数组的方法都是内部的拦截器提供的。为了配合数组的依赖收集,我们在observe函数修改下。

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
export class Observer {
constructor(value) {
this.value = value;

if (!Array.isArray(value)) {
// 处理object
this.walk(value)
}
}
/*
walk 将对象的每一个属性监听
*/
walk(obj) {
const keys = Object.keys(obj);
for(let i=0; i<keys.length;i++) {
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}

function defineReactive(data,key,val) {
if(typeof val == 'object') {
new Observer(val)
}
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get() {
dep.depend();
return val
},
set(newVal){
if(val == newVal) {
return
}
val = newVal;
dep.nofify()
}
})
}

在改造了Observer函数后,我们快速过下数据拦截器的实现方法,数组常用的修改方法有push,pop,shift,unshift,splice,sort,reverse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(null);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(method) {
// 缓存 原型上的方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods,method,{
enumerable: false,
writable: true,
configurable: true,
value:function mutator(...args) {
return original.apply(this,args)
}
})
})

先自定义操作数组的原有方法,后面我们就可以在mutator函数中,发送通知,触发依赖。后面用拦截器覆盖原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class Observer {
constructor(value) {
this.value = value;

if (Array.isArray(value)) {
//覆盖原型
value.__proto__ = arrayMethods;
}else{
// 处理object
this.walk(value)
}
}

}

数组收集依赖

其实,数组的依赖也是在getter中收集的,因为数组也是通过data的key来访问,如this.list,也会触发list这个属性的getter。所以,数组实在getter中收集依赖,在拦截器中触发。然后数组的依赖时存放在Observer中,然后拦截器中要访问这些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export class Observer {
constructor(value) {
this.value = value;
// 这个dep 会把对象和数组的依赖 都收集好,后面的 set和delete api 也会用到
this.dep = new Dep();
if (Array.isArray(value)) {
//覆盖原型
value.__proto__ = arrayMethods;
}else{
// 处理object
this.walk(value)
}
}
}

把dep保存在Observer的属性上之后,我们就可以在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
41
42
function defineReactive(data,key,val) {
let childOb = observe(val);
if(typeof val == 'object') {
new Observer(val)
}
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get() {
if(childOb) {
// 再次收集依赖
childOb.dep.depend()
}
dep.depend();
return val
},
set(newVal){
if(val == newVal) {
return
}
val = newVal;
dep.nofify()
}
})
}
/*
会为value返回一个Observe 实例,我们在拦截器中 就可以访问 dep
如果创建成功,直接返回Observer实例
如果这个实例已经存在 直接返回
*/
export function observe(value,asRootData) {
if(typeOf value != 'object') {
return
}
let ob;
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer ) {
ob = value.__ob__;
}else{
ob = new Observer(value)
}
return ob
}

通过上面的方式,我们可以在getter中将依赖不管时对象还是数组的都收集到Observer实例中的dep中,这样我们就可以在拦截器中通过value.ob.dep通知依赖了。在此我还有标记当前的value是否已经被Observer转换成了响应式数据,所以还要再Observer中加上一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Observer{
constructor(value) {
...
def(value,'__ob__',this)
...
}
}
function def(obj,key,val,enumerable) {
Object.defineProperty(obj,key,{
enumerable: !!enumerable,
writable: true,
configurable: true,
val
})
}

接下来我们还需要监听数组的每一项,并再拦截器中发送通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Observer{
constructor(value) {
this.value = value
def(value,'__ob__',this)
...

if(Array.isArray(value)) {
//监听数组的每一项
this.observeArray(value)
}else{
this.walk(value)
}
}
observeArray(val){
for(let i=0;i<val.length;i++) {
observe(val[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
const arraryProto = Array.prototype;
// 数组原型上的方法
let proto = Object.create(arrayProto);
['push', 'unshift', 'splice', 'reverse', 'sort', 'shift', 'pop'].forEach(method=>{
proto[method] = function (...args) {
const ob = this.__ob__
// 和 Object一样,我们也要处理 数组的新增数据 ,push unshift 和 splice都可以新增数据
let inserted; // 默认没有插入新的数据
switch(method) {
case 'push':
case 'unshift':
inserted = args
break;
// 数组的splice 只有传递三个参数 才是往数组增加数据
case 'splice':
inserted = args.slice(2)
break;
default:
break;
}
console.log('视图更新');
// 检测新增的数据
if(inserted) {
ob.observeArray(inserted)
}
// 发送依赖
ob.dep.depend()
// 还是调用数组的原型方法,但是我们可以在这里 发送数组的变化通知
arrayProto[method].call(this, ...args)
}
})

至此,数组原型的方法都被拦截器代理了,也能正常的发送依赖,但是还是无法拦截数组特有的修改方法,比图this.list[0] = 1,this.list.length=0 ;就无法追踪了。

-------------本文结束感谢您的阅读-------------