本文是对于Build a Reactivity System的翻译前端
使用过vue,而且对于vue实现响应式的原理感兴趣的前端童鞋。vue
本教程咱们将使用一些简单的技术(这些技术你在vue源码中也能看到)来建立一个简单的响应式系统。这能够帮助你更好地理解Vue以及Vue的设计模式,同时可让你更加熟悉watchers和Dep class.react
当你第一次看到vue的响应式系统工做起来的时候可能会以为难以想象。es6
举个例子:npm
<div id="app">
<div>Price: ${{ price }}</div>
<div>Total: ${{ price * quantity }}</div>
<div>Taxes: ${{ totalPriceWithTax }}</div>
</div>
复制代码
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 5.00,
quantity: 2
},
computed: {
totalPriceWithTax() {
return this.price * this.quantity * 1.03
}
}
})
</script>
复制代码
上述例子中,当price改变的时候,vue会作下面三件事情:编程
可是等一下,当price改变的时候vue是怎么知道要更新哪些东西的?vue是怎么跟踪全部东西的?设计模式
若是这对于你来讲不是很明显的话,咱们必须解决的一个大问题是编程一般不会以这种方式工做。举个例子,若是运行下面的代码:app
let price = 5
let quantity = 2
let total = price * quantity // 10 right?
price = 20
console.log(`total is ${total}`)
复制代码
你以为这段代码最终打印的结果是多少?由于咱们没有使用Vue,因此最终打印的值是10ide
>> total is 10
复制代码
在Vue中咱们想要total的值能够随着price或者quantity值的改变而改变,咱们想要:函数
>> total is 40
复制代码
不幸的是,JavaScript自己是非响应式的。为了让total具有响应式,咱们须要使用JavaScript来让事情表现的有所不一样。
咱们须要先保存total的计算过程,这样咱们才能在price或者quantity改变的时候从新执行total的计算过程。
首先,咱们须要告诉咱们的应用,“这段我将要执行的代码,保存起来,后面我可能须要你再次运行这段代码。”这样当price或者quantity改变的时候,咱们能够再次运行以前保存起来的代码(来更新total)。
咱们能够经过将total的计算过程保存成函数的形式来作,这样后面咱们可以再次执行它。
let price = 5
let quantity = 2
let total = 0
let target = null
target = function () {
total = price * quantity
})
record() // Remember this in case we want to run it later
target() // Also go ahead and run it
复制代码
请注意咱们须要将匿名函数赋值给target变量,而后调用record函数。使用es6的箭头函数也能够写成下面这样子:
target = () => { total = price * quantity }
复制代码
record函数的定义挺简单的:
let storage = [] // We'll store our target functions in here
function record () { // target = () => { total = price * quantity }
storage.push(target)
}
复制代码
这里咱们将target(这里指的就是:{ total = price * quantity })存起来以便后面能够运行它,或许咱们能够弄个replay函数来执行全部存起来的计算过程。
function replay (){
storage.forEach(run => run())
}
复制代码
replay函数会遍历全部咱们存储在storage中的匿名函数而后挨个执行这些匿名函数。 紧接着,咱们能够这样用replay函数:
price = 20
console.log(total) // => 10
replay()
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
复制代码
上面的代码虽然也能工做,可是可能并很差。或许能够抽取个class,这个class负责维护targets列表,而后在咱们须要从新运行targets列表的时候接收通知(并执行targets列表中的全部匿名函数)。
一种解决方案是咱们能够把这种行为封装进一个类里面,一个实现了普通观察者模式的Dependency Class
因此,若是咱们建立个JavaScript类来管理咱们的依赖的话,代码可能长成下面这样:
class Dep { // Stands for dependency
constructor () {
this.subscribers = [] // The targets that are dependent, and should be
// run when notify() is called.
}
depend() { // This replaces our record function
if (target && !this.subscribers.includes(target)) {
// Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() { // Replaces our replay function
this.subscribers.forEach(sub => sub()) // Run our targets, or observers.
}
}
复制代码
请注意,咱们这里不用storage,而是用subscribers来存储匿名函数,同时,咱们不用record而是经过调用depend来收集依赖,而且咱们使用notify替代了原来的replay。如下是Dep类的用法:
const dep = new Dep()
let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity }
dep.depend() // Add this target to our subscribers
target() // Run it to get the total
console.log(total) // => 10 .. The right number
price = 20
console.log(total) // => 10 .. No longer the right number
dep.notify() // Run the subscribers
console.log(total) // => 40 .. Now the right number
复制代码
上面的代码和以前的代码功能上是一致的,可是代码看起来更具备复用性(Dep类能够复用)。惟一看起来有点奇怪的地方就是设置和执行target的地方。
后面咱们会为每一个变量建立个Dep实例,同时若是能够将建立匿名函数的逻辑封装起来的话就更好了,或许咱们能够用个watcher函数来作这件事情。
因此咱们不用经过调用如下代码来收集依赖
target = () => { total = price * quantity }
dep.depend()
target()
复制代码
而是经过调用watcher函数来收集依赖(是否是赶脚代码清晰不少?):
watcher(() => {
total = price * quantity
})
复制代码
Watcher fucntion的定义以下:
function watcher(myFunc) {
target = myFunc // Set as the active target
dep.depend() // Add the active target as a dependency
target() // Call the target
target = null // Reset the target
}
复制代码
watcher函数接收一个myFunc,把它赋值给全局的target变量,而后经过调用dep.depend()将target加到subscribers列表中,紧接着调用target函数,而后重置target变量。
如今若是咱们运行如下代码:
price = 20
console.log(total)
dep.notify()
console.log(total)
复制代码
>> 10
>> 40
复制代码
你可能会想为何要把target做为一个全局变量,而不是在须要的时候传入函数。别捉急,这么作天然有这么作的道理,看到本教程结尾就阔以一目了然啦。
如今咱们有了个简单的Dep class,可是咱们真正想要的是每一个变量都拥有本身的dep实例,在继续后面的教程以前让咱们先把变量变成某个对象的属性:
let data = { price: 5, quantity: 2 }
复制代码
让咱们先假设下data上的每一个属性(price和quantity)都拥有本身的dep实例。
这样当咱们运行:
watcher(() => {
total = data.price * data.quantity
})
复制代码
由于data.price的值被访问了,我想要price的dep实例能够将上面的匿名函数收集到本身的subscribers列表里面。data.quantity也是如此。
若是这时候有个另外的匿名函数里面用到了data.price,我也想这个匿名函数被加到price自带的dep类里面。
问题来了,咱们何时调用price的dep.notify()呢?当price被赋值的时候。在这篇文章的结尾我但愿可以直接进入console作如下的事情:
>> total
10
>> price = 20 // When this gets run it will need to call notify() on the price
>> total
40
复制代码
要实现以上意图,咱们须要可以在data的全部属性被访问或者被赋值的时候执行某些操做。当data下的属性被访问的时候咱们就把target加入到subscribers列表里面,当data下的属性被从新赋值的时候咱们就触发notify()执行全部存储在subscribes列表里面的匿名函数。
咱们须要学习下Object.defineProperty()函数是怎么用的。defineProperty函数容许咱们为属性定义getter和setter函数,在我使用defineProperty函数以前先举个很是简单的例子:
let data = { price: 5, quantity: 2 }
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log(`I was accessed`)
},
set(newVal) { // Create a set method
console.log(`I was changed`)
}
})
data.price // This calls get()
data.price = 20 // This calls set()
复制代码
>> I was accessed
>> I was changed
复制代码
正如你所看到的,上面的代码仅仅打印两个log。然而,上面的代码并不真的get或者set任何值,由于咱们并无实现,下面咱们加上。
let data = { price: 5, quantity: 2 }
let internalValue = data.price // Our initial value.
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log(`Getting price: ${internalValue}`)
return internalValue
},
set(newVal) { // Create a set method
console.log(`Setting price to: ${newVal}` )
internalValue = newVal
}
})
total = data.price * data.quantity // This calls get()
data.price = 20 // This calls set()
复制代码
Getting price: 5
Setting price to: 20
复制代码
因此经过defineProperty函数咱们能够在get和set值的时候收到通知(就是咱们能够知道何时属性被访问了,何时属性被赋值了),咱们能够用Object.keys来遍历data上全部的属性而后为它们添加getter和setter属性。
let data = { price: 5, quantity: 2 }
Object.keys(data).forEach(key => { // We're running this for each item in data now
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
复制代码
如今data上的每一个属性都有getter和setter了。
total = data.price * data.quantity
复制代码
当上面的代码运行而且getprice的值的时候,咱们想要price记住这个匿名函数(target)。这样当price变更的时候,能够触发执行这个匿名函数。
或者:
下面让咱们把这两种想法组合起来:
let data = { price: 5, quantity: 2 }
let target = null
// This is exactly the same Dep class
class Dep {
constructor () {
this.subscribers = []
}
depend() {
if (target && !this.subscribers.includes(target)) {
// Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
// Go through each of our data properties
Object.keys(data).forEach(key => {
let internalValue = data[key]
// Each property gets a dependency instance
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend() // <-- Remember the target we're running
return internalValue
},
set(newVal) {
internalValue = newVal
dep.notify() // <-- Re-run stored functions
}
})
})
// My watcher no longer calls dep.depend,
// since that gets called from inside our get method.
function watcher(myFunc) {
target = myFunc
target()
target = null
}
watcher(() => {
data.total = data.price * data.quantity
})
复制代码
在控制台看下:
下面的图如今应该看起来有点感受了: