上面就是响应式的思路过程,但是光说思路,不上代码那是假把式,所以一下将对这个图中的每一步进行解释。
1.将数据变为响应式数据
什么是响应式数据,给你一个对象,当这个数据发生变化或者说被操作(读取和修改)时,它能做出响应。那么做到响应式的先决条件就是你得能监测到对这个数据的操作。Object.defineProperty()这个api可以做到这一点,那么我们先聊聊这个api
Object.defineProperty()
这里咱不详细介绍这个api了,只说响应式用到的.
/**
* 该api接收三个参数
* 1.要监视的对象
* 2.要监视的属性
* 3.配置对象config,其中包括get和set
* */
// 栗子
const obj = {
name:'mdd'
}
let proxy_obj=obj.name;
// proxy_obj相当于obj的代理
Object.defineProperty(obj, "name", {
get() {
return proxy_obj;
},
set(newVal) {
proxy_obj = newVal;
},
});
这里我们主要用配置对象中的get方法和set方法。
get方法:当你访问该对象中的该属性时,会被触发
set方法:当你修改该对象中的该属性时,会被触发
解释一下上面栗子中,为什么要定义proxy_obj,这里如果你不定义一个额外的空间存储变量,当你读写的时候就会发生溢栈。当你修改的时候会触发set(),但是你修改之前需要先读取该变量,因此又会触发get(),这时会返回该对象的该属性,因此相当于又读了一遍,就这样会陷入一个循环。所以需要像上面栗子一样,定义一个变量,相当于obj代理,看似操作obj,其实操作proxy_obj。
经过Object.defineproperty()处理后,该对象就变成了这样,多了get和set
思路
我们熟悉了工具之后就要开始干活了,我们先想一下我们要干什么,我们想要在一个数据变的时候通知我,但是你得先把'我'记录下来,说白了就是你得让数据知道,当这个数据变的时候数据得通知谁,这个“记录”的过程就可以在get里写,通知的过程就在set里写。
const obj = {
name:'mdd'
}
let proxy_obj=obj.name;
//proxy_obj相当于obj的代理
Object.defineProperty(obj, "name", {
get() {
// 进行记录
return proxy_obj;
},
set(newVal) {
// 通知
proxy_obj = newVal;
},
});
关于收集
现在我们可以监测这个数据的变化了,我们浅捋一下目前思路并引出一下接下来要做的。
我访问data上的name属性,然后data.name把我记录下来,有一天,data.name被改了,data.name根据记录通知了我。好了,目前大概思路就是这样,问题也就出来了,这个data.name把我记录下来,他记在哪啊,他没有脑子,所以不可能凭空记住,因此我们可以用个数组来作为存储空间,当通知我的时候只要去循环这个数组就好了。还是那句话,光说不写,假把式,我们上代码!
const obj = {
name:'mdd'
}
// proxy_obj相当于obj的代理
let proxy_obj = obj.name;
// 定义存储空间
let dep = [];
Object.defineProperty(obj, "name", {
get() {
// 进行记录
dep.push('mdd');
return proxy_obj;
},
set(newVal) {
// 通知
proxy_obj = newVal;
for (let i = 0; i < dep.length; i++){
console.log(dep[i],'读取了,去通知他');
}
},
});
好了,我们现在知道要把东西存到哪了,第二步思路也就捋通了。但是现在还有一个问题,并不是每次都是mdd触发的,也可能是别人,但是我们又不知道别人叫啥,这时候我们可以定义一个全局的存储空间,在任何都可以访问的到,我们将这个访问者赋给这个变量,我们只需要每次检查这个全局变量是否被赋值就好了。
我们到时候只需要把访问的人赋值给这个window.target就可以了。
现在我们可以把依赖相关的操作抽象成一个类。
class Dep{
constructor() {
this.dep = [];
}
add(val) {
this.dep.push(val)
}
remove(d) {
let index = this.dep.indexOf(d);
this.dep.splice(index, 1); //将对应的项从数组中删除
}
depend() {
if (window.target) {
// this.dep.push(this.target)
this.add(window.target)
}
}
notify() {
for (let i = 0; i < this.dep.length; i++){
this.dep[i]; //应该触发dep里依赖的更新函数
}
}
}
我们再把我们的get和set方法里的内容修改一下
到这里我们就可以完成依赖的收集和通知了,但是还有一个关键的问题我们还没说,那就是依赖是谁?这个window.target的值是什么?
收集的到底是谁
在上面的例子中,我们收集是"我",但是我们在代码里不可能收集的还是"我",而是真正读取这里的地方,这里vue写的有点小套路,我们就直接说vue是怎么做的了。
vue思路
这里我们需要回顾一下vue中响应式的使用场景,我们在模板引擎中通过{{obj.name}}的方式使用这个值,vue解析到这个花括号里的值时,会创建一个Watcher类的实例,通过这个实例可以做什么呢?当watcher(Watcher实例)发生改变时,会触发什么操作(一个函数),通过这个函数,完成对视图的改变,这个地方说的比较抽象,我们还是结合代码来理解。
class Watcher{
constructor(vm,prop,callback) {
this.vm = vm;
this.getter = parseVal(prop); //路径解析
this.callback = callback;
this.value = this.get();
}
get() {
window.target = this;
let val=this.getter.call(this.vm,this.vm); // 读取该值,触发依赖收集
window.target = undefined;
return val;
}
update() {
const oldVal = this.value;
this.value = this.get();
this.callback.call(this.vm,this.value,oldVal)
}
}
代码解释:当在模板中遇到{{obj.name}}时,会创建一个watcher实例通过这个Watcher类,注意接下来的操作,他先把这个实例赋值给window.target,然后在读取了一下vm身上的obj.name属性,这时候会触发依赖收集,然后就会将这个watcher实例收集起来,当这个值改变的时候,会触发依赖上的update函数,update会执行callback函数,这个callback函数就是当这个值改变后需要进行的操作。
总结一下
到这里vue实现针对对象的响应式处理就说完了,注意我这里说的是对象的,因为对于数组,触发更新的方式是不一样的啊~