likes
comments
collection

Vue2数据响应式原理深度解析(四)

作者站长头像
站长
· 阅读数 19

Vue2数据响应式原理逻辑深度解析(四)

前言

大家好,欢迎来到今天的学习,本篇文章是本系列的最后一个内容。在上一篇文章当中我们实现了关于数组相关的绑定响应式操作,今天我们来聊聊关于订阅发布的内容

目标

利用模块式编程,通过多个文件之间相互调用,文件内部的函数等方法嵌套循环使用来实现订阅发布

概念

那究竟什么是发布订阅呢?其实对于有关的概念我们不用记得太死板,这样反而不好理解。举个例子,比如有五个人去商店买可乐,但可乐已经卖完了,老板说把你们的联系方式留在这里,有货了给你们打电话。这就是一个对于订阅发布的一个粗浅理解,五个人把电话留给老板是订阅的过程,一有消息老板马上通知五个人是发布的过程,来看下面这张图

Vue2数据响应式原理深度解析(四)

这就是vue2发布订阅的一个大致结构和流程,这里稍微理解一下,当我们的依赖,也就是某些依赖数据的地方,当它们在查找一个值时,例如obj.a.b.c,当我要确定这个值时 ,一定会先触发getter函数,在getter函数内部我们就会把构造函数Watcher的实例存入构造函数Dep的实例dep身上的一个数组里,先存起来,如果后面很多地方都会查询这个值,那这个数组肯定会有源源不断的元素进来,而当我改变了这个c的值,那么我就会触发setter函数,那这个函数内部就会遍历那个数组,并执行里面的相关函数,来让所有用到c的地方全部更改,可谓牵一发而动全身,这也是发布订阅的核心思想

实现步骤

实现发布订阅的两个核心函数 : Watcher 和 Dep

创建Dep.js文件

首先我们需要在数据被添加响应式之前,我们需要给构造函数Observer的实例身上的__ob__添加一个dep属性,如图

Vue2数据响应式原理深度解析(四)

此时我们还不能打印这个实例身上是否有dep属性,因为Dep构造函数还未创建,因此我们可以创建一个Dep.js文件,并向外暴露这个Dep构造函数

Vue2数据响应式原理深度解析(四)

接着我们要向外暴露Dep类,在暴露之时我们需要初步做一些事情,如下

var uid = 0;
export default class Dep {
    constructor() {
        console.log('我是DEP类的构造器');
        this.id = uid++;

        // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
        // 这个数组里面放的是Watcher的实例
        this.subs = [];
    }
};

可以看到我们在最上面声明一个变量uid,这个变量是后面每次每次new这个函数时候的标识,在函数内部,我们申明了 this.subs = [ ],因为后期我们要往这个数组内存入元素,要注意后面这里是往Dep的实例dep身上存入Watcher实例的。

下面我们先来打印一下obj对象,首先在index.js文件中定义一下结构

index.js内部结构

import observe from './observe.js';
var obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10,
    c: {
        d: {
            e: {
                f: 6666
            }
        }
    },
    g: [22, 33, 44, 55]
};

observe(obj);

我们在Observer.js文件内部打印传进去的obj的,看看它身上的__ob__里有没有dep这个实例

Vue2数据响应式原理深度解析(四)

打印结果

Vue2数据响应式原理深度解析(四)

可以看到我们已经成功的添加了Dep的实例,实例身上有id和sub这个数组

简单总结一下,我们到目前为止完成了Dep函数的初步搭建和其实例的添加

创建Watcher.js文件

为什么要创建Watcher.js文件呢?因为我们需要收集依赖,也就是说我们要收集当前依赖的信息,Watcher函数的功能就是用来你收集依赖的

于是首先创建一个Watcher.js文件

Vue2数据响应式原理深度解析(四)

暴露一个Watcher类

因为我们在最后在调用这个Watcher类的时候,会传递三个参数,所以这里的构造函数constructor会接收这三个参数;除此之外我们需要把这个三个参数挂载到Watcher身上,于是

var uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('我是Watcher类的构造器');
        this.id = uid++;
        this.target = target;
        this.getter = parsePath(expression);
        this.callback = callback;
        this.value = this.get();
    }
};

可以看到这里向外暴露了一个Watcher函数,这里的三个参数就是我们稍后传递的obj对象,以及要检索的值,和一个回调函数,并把这三个值给到Watcher实例身上了。而这里的parsePath函数,它其实是一个让解析式变成数组的方法

function parsePath(str) {
    var segments = str.split('.');

    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]]
        }
        return obj;
    };
}

这里简单解释一下,我们传递进来的expression表达式,会被split成为一个数组,于是稍后的诸如obj.a.m.n的表达式,都会被解析为[ obj, a ,m , n ] ,并且会返回一个新的函数,注意此时只是返回了这个函数,并未执行这个返回的函数 。着,我们看到了这里调用了get函数,执行get函数作用就代表我们进入了收集依赖阶段。并让全局的Dep添加一个target属性,并把当前的Watcher实例赋值给它,此时Dep函数全局唯一。

在get函数内部,我们会调用this.getter函数来对这个obj对象进行剥丝抽茧,来锁定我们要查找的值。,当我们找到这个值的时候,把给函数身上的target赋值为null,表示当前的已经检索完毕,把Watcher实例流转给下一次调用,最后返回这个value

注意 这里用到了Dep函数,因此要引入先前创建好的Dep.js文件

完善defineReactive.js等文件

在上面的概念解释部分我们说过,要在我们查询对象某个属性并触发getter的时候,要把当前的Watcher添加至dep的数组里面,于是

注意在这里一进入到 Object.defineProperty要new一个dep实例

Vue2数据响应式原理深度解析(四)

       get() {
            console.log('你试图访问' + key + '属性');
            // 如果现在处于依赖收集阶段
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return val;
        },

注意 这里有两个dep,一个是上面被new出来的,另一个是__ob__内部的,其实这里第二个if不写也不影响等会最终的结果

depend函数就是用来把当前Watcher实例推入dep身上的数组的,但现在depend函数还没有被创建出来,于是回到Dep.js,在constructor函数下面写上

    // 添加订阅
    addSub(sub) {
        this.subs.push(sub);
    }
    // 添加依赖
    depend() {
        // Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }

可以看到由于此时Dep.target一定为真,而Dep.target为Watcher的实例,并依次经过addSub,把当前的Watcher函数推入实例dep身上的subs数组里

因为要触发Watcher函数才能给Dep挂载上target属性,所以我们现在在index.js文件中触发这个Watcher,注意这里需要引入Watcher.js文件

Vue2数据响应式原理深度解析(四)

可以看到我们传递了三个值进去,那在Watcher.js文件内部,打印这个Dep.target,看看它身上的当前Watcher实例有没有挂载成功

Vue2数据响应式原理深度解析(四)

打印结果

Vue2数据响应式原理深度解析(四)

从这里就能指定Watcher实例挂载成功,并且把index.js内部传递进来的值都挂载成功

这里要注意一下,为什么打印区多了如下的显示呢?

Vue2数据响应式原理深度解析(四)

这是因为之前在Watcher.js内部做循环遍历的时候就已经来检索相关的值了,因此我们来打印一下subs数组内部到底有没有添加这个Watcher的实例,回到Dep.js文件中

Vue2数据响应式原理深度解析(四)

打印结果:

Vue2数据响应式原理深度解析(四)

可以看到这个subs数组已经存入了一个Watcher实例

接下来我们要触发getter函数,来执行这个还未执行的callback函数,首先把函数相关完善一下

回到 defineReactive.js文件,添加如下

Vue2数据响应式原理深度解析(四)

触发notify函数就是我们即将要发布的状态,因此我们要回到Dep文件内部添加相关函数

要知道我们现在要实现什么目标 是要循环遍历subs数组,触发内部的元素

于是再回到Dep.js函数内部

 // 通知更新
    notify() {
        console.log('我是notify');
        // 浅克隆一份
        const subs = this.subs.slice();
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }

notify函数添加完毕,这一步会遍历subs数组,把里面之前添加的元素全部给你遍历执行了,这就是一个发布的过程中。而由于subs[ i ] 是一个Watcher实例,因此我们现在来回到Watcher.js文件内部update函数

 update() {
        this.run();
    }
    
 get(){
      //....
    }

 run() {
        this.getAndInvoke(this.callback);
    }
    
    getAndInvoke(cb) {
        const value = this.get();

        if (value !== this.value || typeof value == 'object') {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.target, value, oldValue);
        }
    }

由于在index.js里调用Watcher时,就已经传入了一个回调函数,所以这里的Watcher早就会接收了这个回调并挂载在其实例身上,并Watcher实例在subs内部,所以这里的getAndInvoke函数的参数不为空 !而getAndInvoke函数执行的结果就是这个回调执行的结果

显然我们现在没触发getter函数,因此我们回到index.js文件中修改值,如下

Vue2数据响应式原理深度解析(四)

这样一来经过相关函数的多方配合执行之下,来看打印结果

Vue2数据响应式原理深度解析(四)

打印成功,至此我们就完成了订阅发布的整个流程,而在真实的源码中,整个回调内部是会完成改变dom数据节点的数值的,这里就不多阐释

总结

在理解订阅发布的时候,需要明白这几件事

  • 进入到Watcher函数内部的get时就已进入到订阅的过程
  • 进入到getter函数中就已进入到发布的过程
  • 触发getter会遍历所有Watcher实例并执行
  • 所有Watcher实例执行完毕,并触发相应回调

至此,Vue2数据响应式的知识点已经总结完毕,关于里面很多细节都已提到,大家必须牢牢掌握JS的知识,以及模块化开发的认识,才能更好地理解相关的逻辑链路

本节代码

评论区滴滴