导言
其实,在Vue中 Object 和 Array 的变化监听是采用不同的处理方式的
Object 的变化监听
如何追踪变化
function defineReactive(data, key , val) {
Object.defineProperty(data, key, {
enumerable : true,
configurable : true,
get : function() {
return val;
},
set : function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
}
})
}
封装好了之后, 每当从 data 的 key 中读取数据时, get 函数被触发; 每当往 data 的 key 中设置数据时 , set函数被触发
如何收集依赖
我们之所以要观察数据,其目的是当数据的属性发生变化时, 可以通知那些曾经使用了该数据的地方.
<template>
<h1>{{name}</h1>
</template>
先收集依赖,即把用到的数据 name 地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。 在getter 中收集依赖,在setter 中触发依赖
依赖收集到哪里
我们要在 getter 中收集依赖, 那么要把依赖收集到哪里去呢?
我们把 defineReactive 函数稍微改造一下
function defineReactive(data, key , val) {
let dep = [];
Object.defineProperty(data, key, {
configurable : true,
enumerable : true,
get : function() {
dep.push(window.target);
return val;
},
set : function(newVal) {
if(val === newVal) {
return;
}
for(let i = 0 ; i < dep.length ; i ++) {
dep[i](newVal, val);
}
val = newVal;
}
})
}
我们把依赖收集封装成一个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 ; i < subs.length ; 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
function defineReactive(data, key , val) {
let dep = new Dep();
Object.defineProperty(data , key , {
enumerable : true,
configurable : true,
get : function() {
dep.depend(); // 收集依赖
return val;
},
set : function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify(); // 触发依赖
}
})
}
依赖是什么
在上面的代码中,我们收集的依赖是 window.target, 那么它到底是什么呢?
换句话说,就是当属性发生变化后,通知谁?
我们要通知用到的数据的地方, 而使用数据的地方很多
- 模板
- 用户写的一个 watch
我们需要抽象出一个集中处理这些情况的类,就叫做 Watcher
什么是 Watcher
Watcher 是一个中介的角色, 数据发生变化时通知它 , 然后它再通知其他地方。 关于 Watcher , 先看一个经典的使用方式:
vm.$watch('count', function(newVal, val) {
})
思考一下,怎么实现这个功能?
class Watcher {
constructor(vm, expOrFn , cb) {
this.vm = vm;
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.call(this.vm, this.value, oldValue)
}
}
递归检测所有key
其实,我们已经可以实现变化检测的功能了, 但是前面介绍的代码只能检测数据中的某一个属性,我们希望把数据中的所有属性(包括子属性)都检测到, 所以要封装一个 Observer 类。 这个类的作用是将一个数据内的所有属性(包括子属性)都转换成 getter/setter 的形式, 然后去追踪他们的变化。
class Observer {
constructor(value) {
this.value = value;
if(Array.isArray(value)) {
this.walk(value)
}
}
/**
* 这个方法只有数据类型为 Object时被调用
*/
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);
}
let dep = new Dep();
Object.defineProperty(data, key , {
enumerable : true,
configurable : true,
get : function() {
dep.depend();
return val;
},
set : function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify();
}
})
}
关于 Object.defineProperty 的缺陷问题
前面介绍了 Object类型数据的变化检测原理 , 了解了数据的变化是通过 Object.defineProperty 来追踪的。但也正是这种追踪方式, 有些语法中即便数据发生了变化,vue.js也追踪不到
就比如这样:
var vm = new Vue({
el : '#el',
methods : {
action () {
this.obj.name = 'zs'
}
},
data : {
obj : {}
}
})
在上面的代码中, 我们在 action 方法中删除了 obj 中的 name 属性, 而 vue.js 无法检测到这个变化, 所以不会向依赖发送通知。
为了解决这个问题,vue.js提供了两个API
- vm.$set
- vm.$delete