Vue2数据响应式原理深度解析(四)
Vue2数据响应式原理逻辑深度解析(四)
前言
大家好,欢迎来到今天的学习,本篇文章是本系列的最后一个内容。在上一篇文章当中我们实现了关于数组相关的绑定响应式操作,今天我们来聊聊关于订阅发布的内容
目标
利用模块式编程,通过多个文件之间相互调用,文件内部的函数等方法嵌套循环使用来实现订阅发布
概念
那究竟什么是发布订阅呢?其实对于有关的概念我们不用记得太死板,这样反而不好理解。举个例子,比如有五个人去商店买可乐,但可乐已经卖完了,老板说把你们的联系方式留在这里,有货了给你们打电话。这就是一个对于订阅发布的一个粗浅理解,五个人把电话留给老板是订阅的过程,一有消息老板马上通知五个人是发布的过程,来看下面这张图
这就是vue2发布订阅的一个大致结构和流程,这里稍微理解一下,当我们的依赖,也就是某些依赖数据的地方,当它们在查找一个值时,例如obj.a.b.c,当我要确定这个值时 ,一定会先触发getter函数,在getter函数内部我们就会把构造函数Watcher的实例存入构造函数Dep的实例dep身上的一个数组里,先存起来,如果后面很多地方都会查询这个值,那这个数组肯定会有源源不断的元素进来,而当我改变了这个c的值,那么我就会触发setter函数,那这个函数内部就会遍历那个数组,并执行里面的相关函数,来让所有用到c的地方全部更改,可谓牵一发而动全身,这也是发布订阅的核心思想
实现步骤
实现发布订阅的两个核心函数 : Watcher 和 Dep
创建Dep.js文件
首先我们需要在数据被添加响应式之前,我们需要给构造函数Observer的实例身上的__ob__添加一个dep属性,如图
此时我们还不能打印这个实例身上是否有dep属性,因为Dep构造函数还未创建,因此我们可以创建一个Dep.js文件,并向外暴露这个Dep构造函数
接着我们要向外暴露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这个实例
打印结果
可以看到我们已经成功的添加了Dep的实例,实例身上有id和sub这个数组
简单总结一下,我们到目前为止完成了Dep函数的初步搭建和其实例的添加
创建Watcher.js文件
为什么要创建Watcher.js文件呢?因为我们需要收集依赖,也就是说我们要收集当前依赖的信息,Watcher函数的功能就是用来你收集依赖的
于是首先创建一个Watcher.js文件
暴露一个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实例
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文件
可以看到我们传递了三个值进去,那在Watcher.js文件内部,打印这个Dep.target,看看它身上的当前Watcher实例有没有挂载成功
打印结果
从这里就能指定Watcher实例挂载成功,并且把index.js内部传递进来的值都挂载成功
这里要注意一下,为什么打印区多了如下的显示呢?
这是因为之前在Watcher.js内部做循环遍历的时候就已经来检索相关的值了,因此我们来打印一下subs数组内部到底有没有添加这个Watcher的实例,回到Dep.js文件中
打印结果:
可以看到这个subs数组已经存入了一个Watcher实例
接下来我们要触发getter函数,来执行这个还未执行的callback函数,首先把函数相关完善一下
回到 defineReactive.js文件,添加如下
触发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文件中修改值,如下
这样一来经过相关函数的多方配合执行之下,来看打印结果
打印成功,至此我们就完成了订阅发布的整个流程,而在真实的源码中,整个回调内部是会完成改变dom数据节点的数值的,这里就不多阐释
总结
在理解订阅发布的时候,需要明白这几件事
- 进入到Watcher函数内部的get时就已进入到订阅的过程
- 进入到getter函数中就已进入到发布的过程
- 触发getter会遍历所有Watcher实例并执行
- 所有Watcher实例执行完毕,并触发相应回调
至此,Vue2数据响应式的知识点已经总结完毕,关于里面很多细节都已提到,大家必须牢牢掌握JS的知识,以及模块化开发的认识,才能更好地理解相关的逻辑链路
本节代码
评论区滴滴
转载自:https://juejin.cn/post/7167293601330757640