一、响应式的基本思路
众所周知,vue的核心就是响应式,那么响应式是在vue当中怎么实现的呢?在vue2当中,响应式是靠Object.defineProperty()来实现响应式的,虽然vue3已经使用Proxy重写了这部分响应式的代码,但是响应式的基本思路并没有发生变化。响应式的基本思想是先在访问数据的时候收集依赖,然后当数据发生修改的时候,通知到收集的依赖当中,这样就可以实现对数据的响应式处理。
二、响应式的代码实现
2.1、利用Object.defineProperty()实现拦截
/**
*@params data 需要实现响应式的对象
*@params key 需要实现响应式的属性
*@params val 属性的初始化值
*/
function defaineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
//当获取key属性的值时,会触发get函数
get: function () {
return val;
},
//当对key属性设置新的值时,会触发set函数
set: function (newVal) {
if (val == newVal) {
return;
}
val = newVal;
},
});
}
首先,创建一个函数,通过函数来封装Object.defineProperty()属性,传入三个参数data、key、val,Object.defineProperty()接收三个参数,第一个参数是需要拦截的对象,第二个参数是需要拦截对象当中的属性,第三个参数是关于属性的配置,而我们的拦截方法也是在配置当中实现的。当一个对象的属性通过Object.defineProperty()去拦截之后,我们再访问这个属性,会触发get方法,同时执行当中的代码,而当我们对拦截对象当中的属性设置新的值的时候,会触发set()方法。这样我们就实现了对对象当中的某个属性进行拦截的操作。
2.2、实现依赖收集
首先,我们需要明白什么是依赖,这里的依赖就是对于需要获取我们拦截的属性的值的地方,可以是一个函数、一个表达式、模版等,只要是需要获取我们拦截对象的属性的值,我们都称为依赖,因为这些东西都依赖于拦截属性。那么我们该怎么样来收集依赖呢?我们可以想一想在哪里收集依赖比较好呢?比如,当一个函数需要使用我们对象当中的属性,首先需要获取属性的值,这时候就会触发我们刚刚设置好的get方法了,因此,我们在get方法当中收集依赖是比较好的。那么收集依赖的地方有了,那么我们该用什么来存储我们收集到的依赖呢?这里我们使用数组来存储收集到的依赖。好了,现在大致的思路就已经有了,下面让我们来实现一下:
/**
* 创建一个dep类来收集依赖
*/
class Dep {
constructor() {
this.subs = [];
}
//添加依赖
addSub(sub) {
this.subs.push(sub);
}
//删除依赖
removeSub(sub) {
if (this.subs.indexOf(sub) !== -1) {
const index = this.subs.indexOf(sub);
this.subs.splice(index, 1);
}
}
//收集依赖
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();
}
}
}
在上面的代码当中,我们定义了一个Dep类,在类的构造函数当中,我们初始化了一个空数组,这个数组之后就是我们用来存储依赖的地方。在类当中,我们定义了四个方法,addSub()、removeSub()、depend()、notify(),他们分别代表着添加、删除、收集、通知的功能。我们现在把这些方法和Object.defineProperty()结合起来,这样我们就可以收集到依赖了。
function defaineReactive(data, key, val) {
//创建dep实例
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();
},
});
}
2.3实现Watcher
上面我们已经实现了单个属性的依赖收集。但是,一个响应式的属性有很多依赖,我们需要抽取出一个能通知到所有依赖的类,而我们在收集依赖的时候,只需要收集那个依赖的实例就好了。下面我们来实现一下Watcher:
/**
* 简单的匹配路径
*/
const baseRE = /[^\w.$]/;
function parsePath(path) {
if (baseRE.test(path)) {
return;
}
const subMessage = path.split(".");
//返回一个函数,函数返回属性的值
return function (obj) {
for (let i = 0; i < subMessage.length; i++) {
if (!obj) {
return;
}
obj = obj[subMessage[i]];
}
return obj;
};
}
/**
* 观察者
* @params vm 需要进行监听的对象
* @params path 需要进行监听对象当中的属性
* @params cb 回调函数,当数据发生更新的时候,会调用这个回调函数
*/
class Watcher {
constructor(vm, path, cb) {
this.vm = vm;
this.getter = parsePath(path);
this.cb = cb;
this.value = this.get();
}
get() {
window.target = this;
let value = this.getter.call(this.vm, this.vm);
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
这个代码可以把对象自动添加到Dep类当中去,因为我们在get()方法当中把this放到了window.target当中,而我们在Dep当中收集依赖的时候我们收集的是window.target当中的依赖,也就是说,当收集依赖的时候,会把Wacter的实例收集进去。这样我们就实现了Watcher的注入。假如我们需要监听data.a.b.c的变化情况那么我们需要先对其进行监听,当其发生变化的时候,让Dep类当中notify()调用实例的update()的方法。然后触发一开始传入进去的回调函数,完成一个闭环。
2.4、实现多个属性监听
其实上面已经实现了单个属性的监听,但是当我们要监听对象当中所有的属性的时候就太麻烦了,甚至所有属性的子属性,因此,我们需要封装一个Observer类来实现递归侦测对象当中的所有属性
/**
* 将对象的所有子属性全部解析称为响应式
*/
function defaineReactive(data, prop, val) {
//判断当前的键值是否是对象,如果是对象,那么继续通过Observer来进行处理
if (typeof val === "object") {
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, prop, {
enumerable: true,
configurable: true,
get: function () {
// 添加依赖
dep.depend();
return val;
},
set: function (newVal) {
if (val === newVal) {
return;
}
val = newVal;
// 通知依赖更改
dep.notify();
},
});
}
class Observer {
constructor(value) {
// 将外界传入的对象存储在类当中
this.value = value;
//判断是否是数组,不是数组调用walk方法
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(value) {
//获取对象的所有键
let keys = Object.keys(value);
//遍历所有的键,把键统统使用defaineReactive来实现监听响应
for (let i = 0; i < keys.length; i++) {
defaineReactive(value, keys[i], value[keys[i]]);
}
}
}
2.5、流程图示
三、总结
在vue当中监听对象的操作大致就像上面那样,但是还有缺陷,就是无法监听对象的删除和增减,因为新增加的对象并没有进行响应式的包装处理。因此,vue2给我吗提供了vm.$set和vm.$delete这两个API来进行操作。还有这个响应式处理无法处理数组。