Vue底层学习3——手撸发布订阅模式
全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/14986941.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)
作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上一篇完成MVue框架搭建,实现数据劫持并重写
setter
及getter
,本篇就来手撸发布订阅模式~
依赖收集与追踪
上一篇的结尾我们通过console.log(`${key}属性更新了:${val}`);
预留了视图更新的代码位置,也就是当数据发生变更时,我们需要做数据对应的视图更新,那么到底更新哪些视图,就是依赖收集的意义。首先看一个日常的例子:
new Vue({
template:
`<div>
<span>{{name1}}</span>
<span>{{name2}}</span>
<span>{{name1}}</span>
</div>`,
data: {
name1: 'name1',
name2: 'name2',
name3: 'name3'
},
created() {
this.name1 = 'change name1';
this.name3 = 'change name3';
}
});
根据页面绑定的data我们可以整理出以下逻辑:
name1
被修改,视图更新2处;name2
被修改,视图更新1处;name3
被修改,视图无需更新;
所以我们需要做的事是扫描视图收集依赖,得知视图中哪里对数据有依赖后,对应数据变更时就可以得到通知,接下来可以对照第一篇《Vue底层学习1——原理解析》中的简化版原理图实现,Dep
和Watcher
遵从发布订阅模式,也是本篇的重点,建议后续代码结合着原理图看,思路会更清晰哦~
依赖对象
首先需要实现的是依赖对象Dep
,主要用于依赖收集,管理Watcher
,它与数据属性一一对应。其中需要提供2个方法:添加观察者、通知观察者。
/*** MVue.js ***/
// 依赖收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依赖(Watcher)
this.deps = [];
}
// 在deps中添加一个观察者对象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的观察者去更新视图
notify() {
this.deps.forEach((dep => dep.update()));
}
}
观察者对象
接下来实现观察者对象Watcher
,主要用于视图更新。其中需要提供更新视图的方法,构造函数中有一个看似奇怪的操作,后续会详细说明。
/*** MVue.js ***/
class Watcher {
constructor() {
// 将当前Watcher的实例指定到Dep静态属性target
Dep.target = this;
}
update() {
// 预留视图更新
console.log('数据更新了,需要我们更新视图');
}
}
自建框架整合
根据上面的实现,我们把代码跟之前的做一下初步整合:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 数据缓存
this.$options = options;
this.$data = options.data;
// 数据遍历
this.observe(this.$data);
}
observe(data) {
// 确定data存在并且为对象
if (!data || typeof data !== 'object') {
return;
}
// 遍历data对象
Object.keys(data).forEach(key => {
// 重写对象属性的getter和setter,实现数据的响应化
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, val) {
// 解决数据嵌套,递归实现深度遍历
this.observe(val);
Object.defineProperty(obj, key, {
get: function() {
return val;
},
set: function(newVal) {
// 判断属性值是否发生变化
if (newVal === val) {
return;
}
val = newVal;
// 预留视图更新
console.log(`${key}属性更新了:${val}`);
}
})
}
}
// 依赖收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依赖
this.deps = [];
}
// 在deps中添加一个观察者对象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的观察者去更新视图
notify() {
this.deps.forEach((dep => dep.update()));
}
}
class Watcher {
constructor() {
// 将当前Watcher的实例指定到Dep静态属性target
Dep.target = this;
}
update() {
// 预留视图更新
console.log('数据更新了,需要我们更新视图');
}
}
接下来需要将原本预留在defineReactive
中的视图更新替换成发布订阅模式:
- 第1步:我们需要在
defineReactive
中定义Dep
,这样在将来的某个时刻就能把收集的依赖(Watcher
)放进去; - 第2步:我们需要替换掉
Line42
中预留的部分,修改为dep.notify()
,实现通知Watcher
的功能,最终由Watcher
完成视图更新。那么Watcher
怎么来?交给第三步; - 第3步:在
MVue
构造函数中先模拟一下Watcher
的创建过程,即new Watcher()
,接下来就发生了神奇的现象,也就是前面提到的Watcher
构造函数中那行奇怪的操作,Watcher
当前的实例会被指定到Dep
的target
静态属性,这样做的目的就是为了将Watcher
添加到之前创建的Dep
中; - 第4步:在属性的
getter
中添加依赖收集,即dep.addDep(Dep.target)
,当然需要先判断target
是否存在; - 第5步:要想依赖可以成功收集,那么我们需要触发
getter
,也就是读取一下属性,同样在MVue
构造函数中模拟;
修改后代码如下:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 数据缓存
this.$options = options;
this.$data = options.data;
// 数据遍历
this.observe(this.$data);
// 模拟Watcher的创建过程——第3步
new Watcher();
// 模拟属性读取,激活getter,实现依赖收集——第5步
this.$data.name;
// 模拟Watcher的创建过程——第2步
new Watcher();
// 模拟属性读取,激活getter,实现依赖收集——第5步
this.$data.infoObj.location;
}
observe(data) {
// 确定data存在并且为对象
if (!data || typeof data !== 'object') {
return;
}
// 遍历data对象
Object.keys(data).forEach(key => {
// 重写对象属性的getter和setter,实现数据的响应化
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, val) {
// 解决数据嵌套,递归实现深度遍历
this.observe(val);
// 初始化Dep——第1步
const dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
// 依赖收集,将当前属性对应的Watcher添加至Dep中——第4步
Dep.target && dep.addDep(Dep.target);
return val;
},
set: function(newVal) {
// 判断属性值是否发生变化
if (newVal === val) {
return;
}
val = newVal;
// 通知观察者更新视图——第2步
dep.notify();
}
})
}
}
// 依赖收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依赖
this.deps = [];
}
// 在deps中添加一个观察者对象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的观察者去更新视图
notify() {
this.deps.forEach((dep => dep.update()));
}
}
class Watcher {
constructor() {
// 将当前Watcher的实例指定到Dep静态属性target
Dep.target = this;
}
update() {
// 预留视图更新
console.log('数据更新了,需要我们更新视图');
}
}
自建框架测试demo1
运行上一篇《Vue底层学习2——手撸数据响应化》中的demo1案例,执行结果如下,说明我们的发布订阅模式成功替换:
总结
本篇实现发布订阅模式的整体过程可以归纳如下:新增一个Dep
类的实例来做依赖收集。读取数据时,会触发getter
把当前的Watcher
(存放在Dep.target
中)收集到Dep
实例中。写入数据时,会触发setter
通知Dep
类调用notify
方法,以此触发所有Watcher
的update
方法来更新对应的视图。
最后提个小tips——每个Dep
针对单个属性,有多少个数据属性就有多少Dep
,但是一个Dep
中可能有多个Watcher
,因为一个属性可能在视图中出现多次。
参考资料
1、Vue源码:https://github.com/vuejs/vue;