携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
为什么要收集依赖?
依赖的收集能让我们更精准的响应数据变化,举个栗子
new Vue({
template:
`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
<div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
假设我们之前写的cb就是更新template模板函数的话,那么text3其实是没必要去触发cb的。
另外还有一种场景,先看如下栗子:
let globalObj = {
text1: 'text1'
};
let o1 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
let o2 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
此时如果执行去执行globalObj.text1 = 'hello,text1';
,需要把2个地方更更新,意味着需要执行2个不同的cb。
也就意味着有2个地方收集了依赖,这样才能在set时,触发2个不同的cb。
发布订阅者模式
在继续讲解前,我们先看下什么是发布订阅者模式。比如小明去楼盘看房,暂时没看到适合的房子,此时售楼部的工作人员告诉小明,你可以在我这里订阅下楼盘信息,等有消息我就通知你。此时小明就是订阅者,而售楼部的工作人员是发布者,即有新消息时发布通知给订阅者小明。结合以上内容我们用代码实现下:
// 这是事件触发中心,发布和订阅都在此操作
class EventEmitter {
// 需要定义一个deps来存所有的订阅者列表
deps = []
// 这是订阅的方法,需要传入订阅的事件名(小明名字),发布时需要执行的handler(小明留下的联系方法)
$on(eventName, handler) {
// 订阅的信息需要存入到 deps里,通过eventName来区分
// 使用 || [] 保证了this.deps[eventName]里一定是一个[]
this.deps[eventName] = this.deps[eventName] || []
this.deps[eventName].push(handler)
}
// 发布者只要知道订阅者是谁(eventName),以及要传给订阅者的消息 (...args)就行了
$emit(eventName, ...args) {
// 因为 this.deps[eventName] 里的事件是一个数组,所以需要遍历
if(!Array.isArray(this.deps[eventName])) return false
this.deps[eventName].forEach(hanlder => {
typeof hanlder === 'function' && hanlder(...args)
})
}
}
// 下面我们可以试下
const saler = new EventEmitter()
// 小明在售楼员处订阅消息
saler.$on('小明', args => { console.log('手机通知', args) })
saler.$on('小明', args => { console.log('短信通知', args) })
saler.$on('小明', args => { console.log('微信通知', args) })
// 售楼员给小明发布消息,并遍历了几种不同端的方法
saler.$emit('小明', '有新楼盘啦66-6')
但此处也有一些缺点,当然也不能说缺点,但是用于之前我们提到的Vue响应式数据变化的依赖追踪有一些不足: 1,发布订阅者只有一个消息中心(EventEmitter),而Vue是每一个组件一个Watcher 2,每个cb都有不同,这是根据Watcher自己来定的,比如A组件需要更新A组件的template,B组件需要更新B组件的template,但是他们都引用了同一个响应式数据。 3,最重要的一点,发布者和订阅者之间毫无关联,只是通过消息中心去订阅和发布,无法找到依赖之间的关联性。
观察者模式
综上,可以看下另外一种模式,观察者模式:
为了解决上面的问题,我们把 发布者和订阅者2个角色分开。
// 发布者
class Dep {
// 发布者还是需要来记录订阅者信息
subs = []
// 添加订阅者
addSub(sub) {
// sub 是一个订阅者的实例化对象,这个实例化对象都有一个update方法
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => sub.update())
}
}
// 订阅者很简单,只有一个update方法,用于响应发布者的通知notify
class Wathcher {
constructor() {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
update() {
//此时update就可以不同的cb变化啦
console.log('update')
}
}
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
上面的发布者和订阅者就有了对应的关联,我们可以知道是谁订阅了谁,另外一个发布者添加多个订阅者也毫不影响。
依赖收集
有了上面观察者模式的写法,我们来看下如何去收集依赖。 改造一下之前的 defineReactive方法
function defineReactive (obj, key, val) {
/* 一个Dep类对象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dep.notify();
}
});
}
// 同时修改下Vue的代码
class Vue {
constructor(options) {
this.$data = options.data || {}
observer(this.$data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
/* 在这里模拟render的过程,为了触发test属性的get函数, 实际过程中是在compiler时触发收集 */
console.log('render~', this.$data.test);
}
}
整个响应式数据和依赖追踪就写完了。我们可以结合这张图总结下:
图片包含了整体流程,我们只看右边 data 和 watcher 这2块 1 一个组件对应一个watcher 2 在getter时收集依赖到watcher 3 在setter时通知watcher 4 结合 2 3 可知,watcher既是一个发布者也是一个订阅者,合称观察者
一些其他的tips
1 为什么要在Watcher实例化时给Dep.target赋值? 这个是利用了JS是单线程,所以代码都是同步执行的。在初始化组件时,也初始化了这个组件对应的watcher。同时这个组件上的data,在定义响应式数据时,对每个属性都实例化一个dep,但构造函数上的静态属性target始终指向前面实例化对象watcher,这样能保证一个组件下所有的deps都是指向对应组件的watcher,当然这个指向也可以通过一个全局变量去指定。
2 为什么一个组件会有多个deps而只有一个watcher? 我们看先看下面的代码
new Vue({
data: {
a: 'a',
b: 'b'
},
template: `
<div>
<div>{{a}}</div>
<div>{{b}}</div>
</div>
`,
compiler() {
// 这是一个模拟的伪代码,表示watcher里的update
// 用于把上面的template里的{{a}}转成a
}
})
按之前写的理解,每当 data.a 或者 data.b触发setter时,compiler都会重新执行。 那整个流程应该是
data.a
component -> -> watcher
data.b
流程如下: 1 初始化组件,新建一个watcher 2 遍历data,的a 和 b属性,为其初始化对应的2个dep,并且把watcher添加到subs中 3 data.a 和 data.b触发setter时,遍历dep实例化对象中的subs列表,触发里面sub(就是watcher)的update(就是compiler),去进行组件的rerender。其中defineReactive里实例化的dep在defineProperty的descriptor里是一个闭关,所以能保存对应的dep对象。