【翻譯】構建響應式系統-vue

聲明

本文是對於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的值。
  • 從新計算表達式price * quatity,而且將計算後的值更新到頁面上。
  • 調用totalPriceWithTax函數並更新頁面。

可是等一下,當price改變的時候vue是怎麼知道要更新哪些東西的?vue是怎麼跟蹤全部東西的?設計模式

這不是JavaScript編程一般的工做方式

若是這對於你來講不是很明顯的話,咱們必須解決的一個大問題是編程一般不會以這種方式工做。舉個例子,若是運行下面的代碼: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)。

image

咱們能夠經過將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列表中的全部匿名函數)。

解決方案:A Dependency Class

一種解決方案是咱們能夠把這種行爲封裝進一個類裏面,一個實現了普通觀察者模式的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
})
複製代碼

解決方案:A Watcher Function

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上的每一個屬性(pricequantity)都擁有本身的dep實例。

image

這樣當咱們運行:

watcher(() => {
  total = data.price * data.quantity
})
複製代碼

由於data.price的值被訪問了,我想要price的dep實例能夠將上面的匿名函數收集到本身的subscribers列表裏面。data.quantity也是如此。

image

若是這時候有個另外的匿名函數裏面用到了data.price,我也想這個匿名函數被加到price自帶的dep類裏面。

image

問題來了,咱們何時調用pricedep.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()

咱們須要學習下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變更的時候,能夠觸發執行這個匿名函數。

  • Get => Remember this anonymous function, we’ll run it again when our value changes.
  • Set => Run the saved anonymous function, our value just changed.

或者:

  • Price accessed (get) => call dep.depend() to save the current target
  • Price set => call dep.notify() on price, re-running all the targets

下面讓咱們把這兩種想法組合起來:

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
})
複製代碼

在控制檯看下:

image
就像咱們想的同樣!這時候 pricequantity都是響應式的!當 pricequantity更新的時候咱們的total都會及時地更新。

看下Vue

下面的圖如今應該看起來有點感受了:

image
你看見紫色的帶有getter和setter的Data圓圈沒?是否是看起來很熟悉!每一個component實例都擁有一個 watcher實例用於經過getters和setters來收集依賴。當某個setter後面被調用的時候,它會通知相應地watcher從而致使組件從新渲染。下面是我添加了一些標註的圖:
image
Yeah!如今你對Vue的響應式有所瞭解了沒? 很顯然,Vue內部實現會比這個更加複雜,可是經過這篇文章你知道了一些基礎知識。在下個教程裏面咱們會深刻Vue內部,看看源碼裏面的實現是否和咱們的相似。

咱們學到了什麼?

  • 如何建立一個能夠同來收集依賴(depend)並執行全部依賴(notify)的Dep class
  • 如何建立watcher來管理咱們當前正在執行的代碼(target)
  • 如何使用Object.defineProperty()來建立getters和setters。
相關文章
相關標籤/搜索