当我第一次接触Vue时,看起来就像魔术一样。
拿下面这个简单的应用为例:
<div id="app">
<div>单价:${{ price }}</div>
<div>数量:${{ quantity }}</div>
<div>总计:${{ total }}</div>
</div>
复制代码
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 5,
quantity: 2
},
computed: {
total() {
return this.price * this.quantity
}
}
})
</script>
复制代码
在控制台通过 vm.$data.price
改变它的值,页面上的值就发生了变化。
但是,Vue 如何知道 price
的值改变时,就要更改页面的内容,以及改变依赖它的值呢?
在没有使用Vue的时候,例如运行下面这段代码:
let price = 5
let quantity = 2
let total = price * quantity // 肯定输出 10
price = 20
console.log(`total is ${total}`)
复制代码
由于没有使用Vue ,结果肯定是 10
>>> total is 10
复制代码
而在Vue中,total 可以随着 price 或者 quantity 的更新而改变。
>>> total is 40
复制代码
问题1
JavaScript是程序性的,如何才能在 price 或者 quantity 被修改时,重新计算 total?
解决方案1
首先,我们需要一个方法来告诉我们的应用程序,“这是一段计算 total 的代码,我可能在某个时间要运行它,现在先储存起来”。 然后,在 price 或者 quantity 变量被修改时,再次运行刚刚储存的代码。
let price = 5
let quantity = 2
let total = 0
let target = null
target = () => { total = price * quantity }
复制代码
现在 target 中储存了计算 total 的函数,我们要把它储存起来,在需要的时候执行它。
写一个叫做 record 的函数,来做这个事情。
let storage = [] // 把代码储存到这里
function record() {
storage.push(target)
}
复制代码
储存完毕了,稍后需要运行的时候,我们就可以直接调用了,所以我们需要一个运行我们记录的所有代码的函数。
function replay() {
storage.forEach(run => run())
}
复制代码
这个 replay 函数的作用,就是遍历我们储存在仓库的每一个函数,并执行它。
然后我们就可以在代码中使用了:
price = 20
console.log(total) // => 10
repaly()
console.log(total) // => 40
复制代码
这样看起来还是挺简单的,解决了我们的问题。下面是完整的代码片段。
let price = 5
let quantity = 2
let total = 0
let target = null
let storage = []
function record() {
storage.push(target)
}
function replay() {
storage.forEach(run => run())
}
target = () => { total = price * quantity }
record() // 储蓄
target() // 执行计算
price = 20 // 更改价格
console.log(total) // => 10
replay() // 重新计算
console.log(total) // => 40
复制代码
问题2
我们想根据需要持续的记录目标,并且要有一定扩展性。
解决方案2
我们将 储存 + 执行 的行为封装到一个类中,实现一个观察者模式。
class Observer {
constructor() {
this.subscribers = []
}
record() {
if(target && !this.subscribers.includes(target)) { // 当 target 存在,并且还没有被储存过的时候,再储存
this.subscribers.push(target)
}
}
notify() { // 替换了 replay
this.subscribers.forEach(sub => sub())
}
}
复制代码
那么,现在要实现储存 + 执行 的动作,就需要这样:
const obs = new Observer()
let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity }
obs.record()
target()
console.log(total) // => 10jj
price = 20
console.log(total) // => 10
obs.notify() // => 40
console.log(total)
复制代码
效果还是一样的,但是处理 target
的地方还是有点怪怪的。我们可以封装一个 watcher
函数来处理。
target = () => { total = price * quantity }
obs.record()
target()
复制代码
可以更改为:
watcher(() => {
total = price * quantity
})
复制代码
在 watcher
中,我们可以做一些事情:
function watcher(myFunc) {
target = myFunc
obs.record() // 储存
target() // 计算
target = null // 重置为空
}
复制代码
watcher
函数接受一个参数 myFunc
参数,将其赋值给全局属性 target
,调用 obs.record()
添加为订阅者,然后调用 target
函数执行计算, 然后重置 target
函数为空。 现在,我们可以这样运行了:
price = 20
console.log(total) // => 10
obs.notify()
console.log(total) // => 40
复制代码
但是,此时和 Vue 的效果还是相差甚远。我们希望每个变量都有自己的 Observer 类。 当这个变量被访问时,可以将它的 target
保存到订阅者数组中,当被更改时,运行储存在订阅者数组中的函数。
这时,就需要 Object.defineProperty()
函数闪亮登场了,它允许我们为属性定义 getter
和 setter
方法。
我们先看一下最简单的用法:
let data = { price: 5, quantity: 2 }
Object.defineProperty(data, 'price', {
get() {
console.log(`getting 执行了`)
},
set() {
console.log(`setting 执行了`)
}
})
data.price // get 方法会执行
data.price = 20 // set 方法会执行
复制代码
现在,我们希望 get
方法执行的时候,返回一个值。 set
方法执行的时候,更新一个值,所以我们添加一个internalValue
变量来储存我们当前的 price
let data = { price: 5, quantity: 2 }
let internalValue = data.price
Object.defineProperty(data, 'price', {
get() {
console.log(`getting price: ${internalValue}`)
return internalValue
},
set(newVal) {
console.log(`setting price 为:${ newVal}`)
internalValue = newVal
}
})
total = data.price * data.quantity
data.price = 20
>>> getting price: 5
>>> setting price 为:20
复制代码
至此,我们获取并设置值的时候,都可以获得通知。
通过 Object.keys(data)
遍历数据,或者对象的键数组
let data = { price: 5, quantity: 2 }
Object.keys(data).forEach(key => {
let internalValue = data[key]
Object.defineProperty(data, key, {
get() {
console.log(`getting ${key} : ${internalValue}`)
return internalValue
},
set(newVal) {
console.log(`setting ${key} to: ${newVal}`)
internalValue = newVal
}
})
})
total = data.price * data.quantity
data.price = 20
>>> getting price : 5
>>> getting quantity : 2
>>> setting price to: 20
复制代码
这时候,我们和 Observer 类合并起来
get => 调用 obs.record() 储存当前的 target
set => 调用 obs.notify() 重新运行 target
完整代码如下:
let data = { price: 5, quantity: 2 }
let target = null
class Observer {
constructor() {
this.subscribers = []
}
record() {
if(target && !this.subscribers.includes(target)) {
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
Object.keys(data).forEach(key => {
let internalValue = data[key]
const obs = new Observer()
Object.defineProperty(data, 'price', {
get() {
obs.record()
return internalValue
},
set(newVal) {
internalValue = newVal
obs.notify()
}
})
})
function watcher(myFunc) {
target = myFunc
target()
target = null
}
watcher(() => {
data.total = data.price * data.quantity
})
复制代码
现在我们再来试验一下吧,数据每次更改,我们的代码都重新运行了。
此时我们再看一下 Vue 的运行示意图
显然,Vue 更复杂,但是我们知道了基础知识,在以后深入了解 Vue 时,看看是否可以在源码中找到这种模式。
学习 Vue 官方文档的视频教学,Vue Mastery