很多人感觉vue2的响应式其实用到了观察者+发布订阅。我们先来看一下简单的发布订阅的代码:
// 调度中心
class Dep {
static subscribes = {}
// 订阅所有需求
static subscribe (key, demand) {
// 对需求分类收集
if (!Dep.subscribes[key]) Dep.subscribes[key] = []
Dep.subscribes[key].push(demand)
}
// 对所有订阅者发布通知
static publish (key, age) {
if (!Dep.subscribes[key]) return
for (const demand of Dep.subscribes[key]) {
demand(age)
}
}
}
// 找对象的猎手类
class Watcher {
constructor (name, age) {
this.name = name // 名字
this.age = age // 年龄
}
// 订阅,由调度中心将猎手需求分类并存放到全局
subscribe (key, demand) {
Dep.subscribe(key, demand)
}
// 发布,由调度中心将同分类下的需求全部触发
publish (key, age) {
Dep.publish(key, age)
}
}
// 猎手注册
const aa = new Watcher('aa', 18)
const bb = new Watcher('bb', 20)
// 猎手订阅自己感兴趣的人
aa.subscribe('key', function (age) {
if (age === aa.age) console.log(`我是aa,我们都是${age}`)
else console.log(`我是aa,我们年龄不同`)
})
bb.subscribe('key', function (age) {
if (age === bb.age) console.log(`我是bb,我们都是${age}`)
else console.log(`我是bb,我们年龄不同`)
})
// 红娘注册
const red = new Watcher('red', 35)
// 红娘发布信息
red.publish('key', 20)
// 我是aa,我们年龄不同
// 我是bb,我们都是20
从上面中发现一个重要的点,发布者和订阅者是根据key值来区分的,然后通过消息中心来中转的,他们家是是实现不知道对方是谁。
而观察者模式中观察者是一开始就知道自己观察的是谁。
上面其实就是简易版的vue原理中发布订阅那段,我们接下来看完整过程。
Vue2 的响应式
- 创建一个 Observer 对象,它的主要作用是给对象的每个属性添加 getter 和 setter 方法。
- 在 getter 和 setter 方法中分别进行依赖的收集和派发更新。
- 创建 Watcher 对象,用于监听数据的变化,当数据发生任何变化时,Watcher 对象会触发自身的回调函数。
- 在模板解析阶段,对模板中使用到的数据进行依赖的收集,即收集 Watcher 对象。
- 当数据发生变化时,Observer 对象会通知 Dep 对象调用 Watcher 对象的回调函数进行更新操作,即派发更新。
- 更新完毕后,Vue2 会进行视图的重新渲染,从而实现响应式。
下面是一个基于 Object.defineProperty 实现响应式的示例,仅供参考:
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
// 尝试递归处理
observe(obj[key]);
let val = obj[key];
const dep = new Dep(); // 新建一个依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend(); // 收集依赖
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify(); // 派发更新
}
});
});
}
// 依赖类
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
Dep.target = null;
// 观察者类
class Watcher {
constructor(vm, expOrFn, callback) {
this.vm = vm;
this.getter = parsePath(expOrFn);
this.callback = callback;
this.value = this.get(); // 初始化,触发依赖
}
get() {
Dep.target = this; // 设置当前依赖
const value = this.getter.call(this.vm, this.vm); // 触发 getter
Dep.target = null; // 清除当前依赖
return value;
}
addDep(dep) {
dep.addSub(this);
}
update() {
const oldValue = this.value;
this.value = this.get(); // 重新获取
this.callback.call(this.vm, this.value, oldValue); // 触发回调
}
}
// 解析路径
function parsePath(expOrFn) {
if (typeof expOrFn === 'function') {
return expOrFn;
}
const segments = expOrFn.split('.');
return function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return;
}
obj = obj[segments[i]];
}
return obj;
};
}
// 测试
const obj = { foo: 'foo', bar: { a: 1 } };
observe(obj);
new Watcher(obj, 'foo', (val, oldVal) => {
console.log(`foo changed from ${oldVal} to ${val}`);
});
new Watcher(obj, 'bar.a', (val, oldVal) => {
console.log(`bar.a changed from ${oldVal} to ${val}`);
});
obj.foo = 'FOO'; // 输出 `foo changed from foo to FOO`
obj.bar.a = 2; // 输出 `bar.a changed from 1 to 2`
以上代码中,函数 observe 用于递归遍历对象属性,把其进行劫持,包括收集依赖和派发更新;类 Dep 代表一个依赖,其中 addSub 用于添加订阅者实例,removeSub 用于移除订阅者实例,depend 用于收集依赖,即把当前依赖加到对应的订阅者中,notify 用于派发更新,即遍历所有订阅者,并触发其回调函数。类 Watcher 则代表一个订阅者,其中 getter 用于获取数据,callback 用于回调函数,addDep 用于添加依赖,即把当前订阅者添加到对应的依赖中,update 用于更新值,并触发相应的回调函数,如有必要。函数 parsePath 则用于解析路径字符串,返回对应属性的值。
例子中我们对对象 obj 进行了劫持,同时创建了两个观察者,分别对应 foo 和 bar.a 两个属性。当其中任意一个属性的值发生变化时,其对应的依赖都会被更新,从而触发其绑定的观订阅者的回调函数。
简单来说,在 Vue2 响应式系统中,当数据发生改变时,会触发 get 和 set 方法,get 方法会收集所有依赖该数据的 Watcher 对象,set 方法会通知 Dep 对象触发所有 Watcher 对象的回调函数进行更新。如此循环,实现了数据的响应式。