完美解釋 Javascript 響應式編程原理

深刻響應式原理 — Vue.js
https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d
https://www.vuemastery.com/courses/advanced-components/evan-you-on-proxies/javascript

不少前端 JavaScript 框架,包含但不限於(Angular,React,Vue)都擁有本身的響應式引擎。經過了解響應式變成原理以及具體的實現方式,能夠提成對既有響應式框架的高效應用。html

響應式系統

咱們看一下 Vue 的響應式系統:前端

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

Vue 知道每當 price 發生變換時,它作了以下三件事情:vue

  • 在頁面中更新 price 的值。
  • 在頁面中從新計算 price * quantity 表達式,並更新。
  • 調用 totalPriceWithTax 方法,並更新。

可是等一下,這裏有些疑惑,Vue 是怎麼知道 price 更新了呢,如何去追蹤更新的具體過程呢?java

響應式在原生 JavaScript 中如何實現的呢,咱們從聲明變量開始吧~

let price = 5
let quantity = 2
let total = price *  quantity // 10 ?
price = 20
console.log(`total is ${total}`) // 10

// 咱們想要的 total 值但願是更新後的 40
複製代碼

⚠️ 問題

咱們須要將 total 的計算過程存起來,這樣咱們就可以在 price 或者 quantity 變化時運行計算過程。react

✅ 方案

首先,咱們須要告訴應用程序,「這裏有一個關於計算的方法,存起來,我會在數據更新的時候去運行它。「npm

咱們建立一個記錄函數並運行它:數組

let price = 5
let quantity = 2
let total = 0
let target = null

target = () => { total = price * quantity }

record() // 記錄咱們想要運行的實例 
target() // 運行 total 計算過程
複製代碼

簡單定義一個 recordbash

let storage = [] // 將 target 函數存在這裏
function record () { // target = () => { total = price * quantity }
	storage.push(target)
}
複製代碼

咱們存儲了 target 函數,咱們須要運行它,須要頂一個 replay 函數來運行咱們記錄的函數:app

function replay () {
	storage.forEach( run => run() )
}
複製代碼

在代碼中咱們運行:

price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼

是否是足夠的簡單,代碼可讀性好,而且能夠運行屢次。FYI,這裏用了最基礎的方式進行編碼,先掃盲。

let price = 5
let quantity = 2
let total = 0
let target = null
let storage = []

target = () => { total = price * quantity }

function record () {
	storage.push(target)
}

function replay () {
	storage.forEach( run => run() )
}

record()
target()

price = 20
console.log(total) // => 10
replay()
console.log(total) // => 40
複製代碼

對象化

⚠️ 問題

咱們能夠不斷記錄咱們須要的 target, 並進行 record,可是咱們須要更健壯的模式去擴展咱們的應用。也許面向對象的方式能夠維護一個 targe 列表,咱們用通知的方式進行回調。

✅ 方案 Dependency Class

咱們經過一個屬於本身的類進行行爲的封裝,一個標準的依賴類 Dependency Class,實現觀察者模式。

若是咱們想要建立一個管理依賴的類,標準模式以下:

class Dep { // Stands for dependency 
	constructor () {
	  this.subscribers = [] // 依賴數組,當 notify() 調用時運行
	}

	depend () {
	  if (target && !this.subscribers.includes(target)) {
	    // target 存在而且不存在於依賴數組中,進行依賴注入
	    this.subscribers.push(target)
	  }
	}

	notify () { // 替代以前的 replay 函數
	  this.subscribers.forEach(sub => sub()) // 運行咱們的 targets,或者觀察者函數
  }
}
複製代碼

注意以前替換的方法,storage 替換成了構造函數中的 subscribersrecod 函數替換爲 dependreplay 函數替換爲 notify

如今在運行:

const dep = new Dep()

let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity }

dep.depend() // 依賴注入
target() // 計算 total

console.log(total) // => 10
price = 20
console.log(total) // => 10
dep.notify()
console.log(total) // => 40
複製代碼

工做正常,到這一步感受奇怪的地方,配置 和 運行 target 的地方。

觀察者

⚠️ 問題

以後咱們但願可以將 Dep 類應用在每個變量中,而後優雅地經過匿名函數去觀察更新。可能須要一個觀察者 watcher 函數去知足這樣的行爲。

咱們須要替換的代碼:

target = () => { total = price * quantity }
dep.depend()
target()
複製代碼

替換爲:

watcher( () => {
  total = price * quantit
})
複製代碼

✅ 方案 A Watcher Function

咱們先定義一個簡單的 watcher 函數:

function watcher (myFunc) {
	target = myFunc // 動態配置 target
	dep.depend() // 依賴注入
	target() // 回調 target 方法
  target = null // 重置 target
}
複製代碼

正如你所見, watcher 函數傳入一個 myFunc 的形參,配置全局變量 target,調用 dep.depend() 進行依賴注入,回調 target 方法,最後,重置 target

運行一下:

price = 20
console.log(total) // => 10
dep.notify()
console.log(total) // => 40
複製代碼

你可能會質疑,爲何咱們要對一個全局變量的 target 進行操做,這顯得很傻,爲何不用參數傳遞進行操做呢?文章的最後將揭曉答案,答案也是顯而易見的。

數據抽象

⚠️ 問題

咱們如今有一個單一的 Dep class,可是咱們真正想要的是咱們每個變量都擁有本身的 Dep。讓咱們先將數據抽象到 properties

let data = { price: 5, quantity: 2 }
複製代碼

將設每一個屬性都有本身的內置 Dep 類

然我咱們運行:

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

data.price 的 value 開始存取時,我想讓關於 price 屬性的 Dep 類 push 咱們的匿名函數(存儲在 target 中)進入 subscriber 數組(經過調用 dep.depend())。而當 quantity 的 value 開始存取時,咱們也作一樣的事情。

若是咱們有其餘的匿名函數,假設存取了 data.price,一樣的在 price 的 Dep 類中 push 此匿名函數。

當咱們想經過 dep.notify() 進行 price 的依賴回調時候。咱們想 在 price set 時候讓回調執行。在最後咱們要達到的效果是:

$ total
10
$ price = 20 // 回調 notify() 函數
$ total
40
複製代碼

✅ 方案 Object.defineProperty()

咱們須要學習一下關於 Object.defineProperty() - JavaScript | MDN。它容許咱們在 property 上定義 getter 和 setter 函數。咱們展現一下最基本的用法:

let data = { price: 5, quantity: 2 }

Object.defineProperty(data, 'price', { // 僅定義 price 屬性
  get () { // 建立一個 get 方法
    console.log(`I was accessed`)
	},
	set (newVal) { // 建立一個 set 方法
    console.log(`I was changed`)
	}
})

data.price // 回調 get()
// => I was accessed
data.price = 20 // 回調 set()
// => I was changed
複製代碼

正如你所見,打印兩行 log。然而,這並不能推翻既有功能, get 或者 set 任意的 valueget() 指望返回一個 value,set() 須要持續更新一個值,因此咱們加入 internalValue 變量用於存儲 price 的值。

let data = { price: 5, quantity: 2 }

let internalValue = data.price // 初始值

Object.defineProperty(data, 'price', { // 僅定義 price 屬性
  get () { // 建立一個 get 方法
    console.log(`Getting price: ${internalValue}`)
    return internalValue
	},
	set (newVal) { // 建立一個 set 方法
    console.log(`Setting price: ${newVal}`)
	  internalValue = newVal
	}
})

total = data.price * data.quantity // 回調 get()
// => Getting price: 5
data.price = 20 // 回調 set()
// => Setting price: 20
複製代碼

至此,咱們有了一個當 get 或者 set 值的時候的通知方法。咱們也能夠用某種遞歸能夠將此運行在咱們的數據隊列中?

FYI,Object.keys(data) 返回對象的 key 值列表。

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}: ${newVal}`)
	    internalValue = newVal
	  }
  })
})

total = data.price * data.quantity
// => Getting price: 5
// => Getting quantity: 2
data.price = 20
// => Setting price: 20
複製代碼

CI 集成

將全部的理念集成起來

total = data.price * data.quantity
複製代碼

當代碼碎片好比 get 函數的運行而且 get 到 price 的值,咱們須要 price 記錄在匿名函數 function(target) 中,若是 price 變化了,或者 set 了一個新的 value,會觸發這個匿名函數而且 get return,它可以知道這裏更新了同樣。因此咱們能夠作以下抽象:

  • Get => 記錄這個匿名函數,若是值更新了,會運行此匿名函數

  • Set => 運行保存的匿名函數,僅僅改變保存的值。

在咱們的 Dep 類的實例中,抽象以下:

  • Price accessed (get) => 回調 dep.depend() 去注入當前的 target

  • Price set => 回調 price 綁定的 dep.notify() ,從新計算全部的 targets

讓咱們合併這兩個理念,生成最終代碼:

let data = { price: 5, quantity: 2 }
let target = null

class Dep { // Stands for dependency 
	constructor () {
	  this.subscribers = [] // 依賴數組,當 notify() 調用時運行
	}

	depend () {
	  if (target && !this.subscribers.includes(target)) {
	    // target 存在而且不存在於依賴數組中,進行依賴注入
	    this.subscribers.push(target)
	  }
	}

	notify () { // 替代以前的 replay 函數
	  this.subscribers.forEach(sub => sub()) // 運行咱們的 targets,或者觀察者函數
  }
}

// 遍歷數據的屬性
Object.keys(data).forEach(key => {
  let internalValue = data[key]

	// 每一個屬性都有一個依賴類的實例
  const dep = new Dep()

  Object.defineProperty(data, key, {
	  get () {
      dep.depend()
      return internalValue
	  },
	  set (newVal) {
	    internalValue = newVal
		dep.notify()
	  }
  })
})

// watcher 再也不調用 dep.depend
// 在數據 get 方法中運行
function watcher (myFunc) {
	target = myFunce
	target()
	target = null
}

watcher(()=> {
	data.total = data.price * data.quantity
})

data.total
// => 10
data.price = 20
// => 20
data.total
// => 40
data.quantity = 3
// => 3
data.total
// => 60
複製代碼

這已經達到了咱們的指望,pricequantity 都成爲響應式的數據了。

Vue 的數據響應圖以下:

看到紫色的 Data 數據,裏面的 getter 和 setter?是否是很熟悉了。每一個組件的實例會有一個 watcher的實例(藍色圓圈), 從 getter 中收集依賴。而後 setter 會被回調,這裏 notifies 通知 watcher 讓組件從新渲染。下圖是本例抽象的狀況:

顯然,Vue 的內部轉換相對於本例更復雜,但咱們已經知道最基本的了。

知識點

  • 如何建立一個 Dep class 而且注入全部的依賴執行(notify)
  • 如何建立一個 watcher 進行代碼管理,須要添加一個依賴項目
  • 如何使用 Object.defineProperty() 建立一個 getters 和 setters
相關文章
相關標籤/搜索