如果你有时间,请看完 Vue 官方教程中的视频;如果你看不下去,可以看看我下面👇的总结。
一个没有响应式的世界
- 看下面一段 JavaScript 代码:
let price = 5
let quantity = 2
let total = price * quantity
console.log(`total is ${total}`) // total is 10
price = 20 // 修改了变量price
console.log(`total is ${total}`) // total is 10
// total的值并没有对应着做出改变
-
当我们修改了
price的值时,虽然在计算total时会使用到price,但是total的值并不会响应式地更新。 -
【问题来了】我们怎样才能将「计算
total值的过程」保存下来,然后每次price或者quantity的值发生变化后,都运行这个「计算total值的过程」去自动更新total的值呢?
保存计算total的逻辑以复用
- 我们可以提取出计算
total的逻辑,之后每次price或者quantity发生变化的时候,都会再执行一遍这个逻辑。这样的话,当price或者quantity发生变化时,就可以自动将这种变化更新到total上面了。 - 我们使用target来提取出这个「计算
total值的逻辑」,然后使用record()将它保存在一个小盒子里,等需要用的时候,再使用replay()重新执行「这个过程」。伪代码如下:
let price = 5
let quantity = 2
let total = 0
let target = null
target = function() {
total = price * quantity
}
/* record()和replay()目前未定义 */
record() // 将target所指的函数(计算逻辑)记录一下,存放起来
target() // 执行一次target所指的函数(计算逻辑)
/*
* ......
* 修改了 price 或者 quantity 的值
* ......
*/
replay() // 执行 record 存放起来的函数(计算逻辑),即重新运行target所指的函数
- 可以看到,我们使用
record()把计算total值的逻辑保存了起来,并在price或者quantity发生变动时使用replay()重新计算total的值。做到了total的值可以随着price和quantity的值更新而去更新。我们在total身上实现了“响应式”。 - 下面我们实现一下
record()和replay(),使得代码可以运行。
let price = 5
let quantity = 2
let total = 0
let target = null
let storage = [] // 用于存放target
target = () => { total = price * quantity }
function record() { storage.push(target) }
function replay() { storage.forEach(run => run()) }
record(); // 保存「计算total值的逻辑」
target(); // 初始化total的值
console.log(total) // 10
price = 20 // 修改price
console.log(total) // 10
replay() // 使用之前保存的「计算total值的逻辑」,重新计算total的值
console.log(total) // 40
使用观察者模式重构
- 下面我们使用观察者模式(Observer Pattern)重构一下我们的代码。
观察者模式:当订阅者所观察的对象发生变动时,会通知特定的订阅者。
- 在下面的代码中,当订阅者(subscriber)所订阅的变量值变化时,会收到通知(notify)并执行一些操作。例如,对于刚才的「计算total的值的逻辑」,当计算
total需要使用的price和quantity的值发生变化时,total会收到通知,从而去执行「重新计算」这一操作。
// Dep是Dependency(依赖)的缩写
class Dep {
constructor() {
// 订阅者,用来存放所有的target
this.subscribers = [];
}
depend() {
// 如果有一个target还不在subscribers中,就将它添加进去
if(target && !this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
// 执行subscribers中的所有target
this.subscribers.forEach(sub => sub());
}
}
const dep = new Dep();
let price = 5
let quantity = 2
let total = 0
let target = null
// 对于传进来的函数,将其添加到dep的subscribers数组中
function watcher(myFunc) {
target = myFunc;
dep.depend();
target();
target = null
}
watcher(() => { total = price * quantity });
// Test Code
console.log(total); // 10
price = 20;
console.log(total); // 10
dep.notify();
console.log(total); // 40
使用 Object.defineProperty()
- 还是刚刚的需求,这次我们借助
Object.defineProperty()为data.price和data.quantity分别添加getter/setter,并通过在getter和setter中添加一些逻辑来实现响应式。
Talk is cheap, Show Me the Code!
// 将数据存放在一个对象中
let data = {
price: 5,
quantity: 2
}
let target, total, salePrice
// Dep没变
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if(target && !this.subscribers.includes(target)) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 为data中的每个属性添加一个getter和setter
Object.keys(data).forEach(key => {
let internalValue = data[key];
const dep = new Dep();
Object.defineProperty(data, key, {
// 获取某个属性的时候,添加依赖
get() {
console.log(`获取了${key}的值`)
dep.depend();
return internalValue;
},
// 修改某个属性的时候,提醒订阅者
set(newVal) {
internalValue = newVal;
console.log(`修改了${key}的值为${newVal}`)
dep.notify();
}
})
})
// 添加订阅
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
total = data.price * data.quantity
});
// Test Code
console.log(total); // 10
data.price = 20;
console.log(total); // 40
解释一下这段代码:
- 当程序运行到第50行,执行
watcher()时,会执行传入的函数(第46行),也就是要「计算total的值」 - 因为计算
total的值需要用到data.price的值和data.quantity的值,因此会去调用二者的getter - 当执行
data.price的getter时,会执行其中的dep.depend(),将当前的target添加到dep的subscriber中 - 也就是将「计算total的值」这一个逻辑添加到了
data.price中dep对象的subscriber数组上 - 下一次修改
data.price值的时候,会调用price的setter,便会执行dep.notify(),也就是执行subscriber数组中的所有函数 - 这时,因为「计算total的值」这一个逻辑在
subscriber中,所以会被执行. - 这一串操作组合起来就是:当修改
price时,会自动「计算total的值」,total的值就实现了响应式地更新。 - 对于
data.quantity也是同理,不再赘述。
小结
- 总结一下我们刚刚实现响应式的逻辑:
- 例如对于
c = a + b; - 计算
c的值时会用到a,会调用a的getter,a的getter中会执行一些操作来记住「c需要我」; - 下次
a的值变化时,会调用它的setter,a会记起「c需要我」,就会通知c也修改一下它的值;
- 例如对于