使用 Proxy 構建響應式系統

💡本篇博文可能學到的知識點html

  • 更好的理解 Vue 響應式工做原理
  • 學習 Vue 的設計模式
  • 學習 Proxy API
  • 使用 Proxy 實現觀察者模式

現代前端開發必不可少會用到的 Vue、React 等框架,這些框架的共同之處在於都提供了響應式(Reactive)和組件化(Composable)的視圖組件,組件化開發從新定義了前端開發技術棧。結合前端構建工具以及基於框架出現的各類通過精心設計的UI組件庫,讓前端也進入到了一個工程化的時代。構建頁面變得從未有過的簡潔高效。前端

若是你是一名經驗豐富(nian ling da)的程序員,或多或少都會接觸到沒有這些框架以前的狀態,那時候咱們還手持 jQuery 利器,操縱着一手好 dom,當你初次接觸到響應式框架的時候或許會被它的好用所驚豔到。咱們只須要改變數據,dom就更新了。本篇博文主要是來討論被驚豔到的響應式框架是如何實現的。咱們首先來看看 Vue 是如何實現響應式系統的?vue

Vue 是如何實現響應式系統的?

看一個簡單的小例子

假設咱們購物車中有一個商品,單價 price,數量 quantity,總價爲 totalreact

有這樣一個簡單的功能,點擊按鈕讓 price 發生變化,那麼 total 爲計算屬性,也隨之發生變化。程序員

<!-- App.vue -->
<div id="app">
  <div>Price: ¥{{ price }}</div>
  <div>Quantity: {{ quantity }}</div>
  <div>Total: ¥{{ total }}</div>
  <div>
    <button @click="price = 10">
      change price
    </button>
  </div>
</div>
// main.js
new Vue({
  el: '#app',
  data: {
    price: 6.00,
    quantity: 2
  },
  computed: {
    total () {
      return this.price * this.quantity
    }
  }
})

連接描述設計模式

考慮一下 Vue是如何實現這樣的功能的,其實咱們是更改了數據,而依賴於它的計算屬性也發生了變化。數組

Vue 是如何實現追蹤數據的變化的

在官方文檔,深刻響應式原理裏有講到app

當你把一個普通的 JavaScript 對象傳入 Vue 實例作爲 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。在內部它們讓 Vue 可以追蹤依賴,在屬性被訪問和修改時通知變動。框架

每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。dom

reactivity

一些弊端

受現代 JavaScript 的限制

對於已建立的實例,Vue 沒法檢測到對象屬性的添加和刪除。Vue 容許使用 Vue.set(object, propertyName, value) 的方法進行動態添加響應式屬性,或者使用別名寫成 this.$set(object, propertyName, value)

對於數組,Vue 沒法檢測如下數組的變更

  1. 當你利用索引直接設置一個數組項時,例如:vm.items[indexOfItem] = newValue
  2. 當你修改數組的長度時,例如:vm.items.length = newLength

第一條的解決方式一樣是使用 this.$set(vm.items, indexOfItem, newValue)

第二條的解決方式是用 vm.items.splice(indexOfItem, 1, newValue)

對於以上的弊端,其實官方已經給出瞭解決方法,就是使用 Proxy 代替 Object.defineProperty API,這個改動會伴隨着萬衆期待的 Vue 3.0 的發佈而應用。你們都知道 Vue 2.x 也就是如今的版本,響應式是經過 Object.defineProperty API來實現的,那麼既然知道了解決方法,咱們不妨提早學習一下 Proxy 是如何作到響應式的。

使用 Proxy 實現響應式系統

Proxy 簡介

Proxy 對象用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。

基本用法

let p = new Proxy(target, hanlder)

參數:

target 用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。

handler 一個對象,其屬性是當執行一個操做時定義代理的行爲的函數。

MDN 上的基礎示例

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 37;
    }
};
    
let p = new Proxy({}, handler);
    
p.a = 1;
p.b = undefined;
    
console.log(p.a, p.b);    // 1, undefined
    
console.log('c' in p, p.c);    // false, 37

上述例子使用了 get ,當對象中不存在屬性名時,缺省返回數爲37。咱們知道了對於操做對象,能夠在使用 handler 處理一些事務。關於 handler 則有十三種。

實現一個響應式系統

從新回到咱們的小例子,咱們有一些變量,單價 price,數量 quantity,總價爲 total。咱們來看下 JavaScript 是如何工做的

let price = 6
let quantity = 2
let total = 0
    
total = price * quantity
    
console.log(total)  // 12
price = 10
console.log(total)  // 12

這好像不是咱們的指望,咱們指望的是,改變了 price,total的值也會更新。如何作到這一點,並不難。

let price = 6
let quantity = 2
let total = 0
    
total = price * quantity
    
console.log(total)  // 12
price = 10
console.log(total)  // 12
    
const updateTotal = () => {
  total = price * quantity
}
    
updateTotal()
    
console.log(total)  // 20

咱們定義了一個方法 updateTotal,讓這個方法執行了咱們須要更新 total 的業務邏輯,再從新執行這個方法那麼 total 的值就改變了。

咱們能夠考慮下咱們想到達到什麼樣的目的,其實很簡單,就是當改變 price 或者 quantity 的時候 total 的值會跟着改變。學過設計模式的話,咱們很容易想到這個場景符合觀察者模式。

使用觀察者模式

觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都將獲得通知。

observer_pattern

如今咱們使用觀察者模式定義了觀察變量 price 與 quantity,若是它們的值發生變化,那麼依賴於它的計算屬性 total 將會獲得一個 notify,這個 notify 便是咱們的目標,去執行 updateTotal。

建立依賴類

把觀察者模式抽象爲一個依賴類

// 表明依賴類
class Dep {
  constructor () {
    this.subscribers = [] // 把全部目標收集到訂閱裏 
  }
  addSub (sub) {  // 當有可觀察目標時,添加到訂閱裏
    if (sub && !this.subscribers.includes(sub)) {
      // 只添加未添加過的訂閱
      this.subscribers.push(sub)
    } 
  }
  notify () {  // 當被觀察的屬性發生變更時通知全部依賴於它的對象
    this.subscribers.forEach(fn => fn()) // 從新執行全部訂閱過的目標方法
  }
}

那麼如何使變量 price 和 quantify 變得可觀察,在 Vue 2.x 中使用的是 Object.defineProperty ,本文會使用 Proxy 來實現。

// 使變量變爲一個可觀察的對象的屬性
const dataObj = {
  price: 6,
  quantity: 2
}
let total = 0
let target = null
    
class Dep { // 表明依賴類
    ...
}
    
const dep = new Dep()
    
data = new Proxy(dataObj, {
  get (obj, key) {
    dep.addSub(target)  // 將目標添加到訂閱中
    return obj[key]
  },
  set (obj, key, newVal) {
    obj[key] = newVal // 將新的值賦值給舊的值,引發值的變化
    dep.notify()  // 被觀察的屬性值發生變化,即通知全部依賴於它的對象
  }
})
    
total = data.price * data.quantity
    
console.log(total)  // 12
data.price = 10
console.log(total)  // 12
    
target = () => {
  total = data.price * data.quantity
}
target()
target = null
    
console.log(total)  // 20

上面代碼稍微有些凌亂,咱們重構一下,觀察者模式結合 Proxy 最終目的就是輸出被觀察的對象。咱們能夠抽象爲一個 observer

使用 Proxy 實現觀察者模式

// 將依賴類與 Proxy 封裝爲 observer,輸入一個普通對象,輸出爲被觀察的對象
const observer = dataObj => {
  const dep = new Dep()
    
  return new Proxy(dataObj, {
    get (obj, key) {
      dep.addSub(target)  // 將目標添加到訂閱中
      return obj[key]
    },
    set (obj, key, newVal) {
      obj[key] = newVal // 將新的值賦值給舊的值,引發值的變化
      dep.notify()  // 被觀察的屬性值發生變化,即通知全部依賴於它的對象
    }
  })
}
    
const data = observer(dataObj)

咱們注意到,其實每次咱們還要從新執行咱們的目標 target ,讓 total 值發生變化。這塊兒邏輯咱們能夠抽象爲一個 watcher ,讓它幫咱們作一些重複作的業務邏輯。

建立 watcher

const watcher = fn => {
  target = fn
  target()
  target = null
}
    
watcher(() => {
  total = data.price * data.quantity
})

咱們最終代碼優化爲:

// 使變量變爲一個可觀察的對象的屬性
const dataObj = {
  price: 6,
  quantity: 2
}
let total = 0
let target = null
    
// 表明依賴類
class Dep {
  ...
}
    
// 使用 Proxy 實現了觀察者模式
const observer = dataObj => {
  ...
}
    
const data = observer(dataObj)
    
// 高階函數,重複執行訂閱方法
const watcher = fn => {
  ...
}
    
watcher(() => {
  total = data.price * data.quantity
})
    
console.log(total)  // 12
data.price = 30
console.log(total) // 60

咱們最終實現了開始的想法,total 會根據 price 值的改變而改變。實現了簡單的響應式系統。

爲了縮小篇幅,上面的方法同時也有講過,即摺疊(簡化)起來,咱們再回到 Vue 是如何追蹤數據的依賴的那張圖。再看看咱們是如何實現的。

vue-reactive-proxy

總結一下

經過小例子咱們學習到的內容

  • 咱們學習到了經過建立一個 Dep 依賴類,來收集依賴關係,在訂閱者屬性被改變時,全部依賴於它的對象得以獲得一個通知。
  • 結合 Dep 依賴類,咱們使用 Proxy 實現了觀察者模式的 observer 方法
  • 咱們建立了一個 watcher 觀察者方便管理咱們要從新執行的業務邏輯,即咱們添加到訂閱裏須要執行的方法

結尾

本文講到了 Vue2.x 中響應式系統的一些弊端,在即將到來的 Vue 3.0 Updates 中都將獲得解決,在去年這時候尤大在 Plans for the Next Iteration of Vue.js 這篇博文中也有提到過。讓咱們期待 Vue 3.0 的到來吧。

原文: http://xlbd.me/build-a-reacti...
相關文章
相關標籤/搜索