如果你有时间,请看完 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
也修改一下它的值;
- 例如对于