聊聊vue响应式那点事

89 阅读5分钟

上面就是响应式的思路过程,但是光说思路,不上代码那是假把式,所以一下将对这个图中的每一步进行解释。

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实现针对对象的响应式处理就说完了,注意我这里说的是对象的,因为对于数组,触发更新的方式是不一样的啊~