Proxy實現vue MVVM實踐

小牧_QAQ發表於2018-11-29

vueconf(2018hangzhou)大會剛剛過去,vue作者尤大大向我們展示了vue3.0的進展,並介紹vue3.0的一些改動,其中最令我期待的就是重寫資料監聽機制。

回顧vue2.x的雙向資料繫結

談起vue的雙向資料繫結,我們首先能想到的就是ES5中Object.defineProperty,利用重寫屬性的getset,我們可以完成資料劫持監聽,使用觀察者模式,在資料發生改變的時候,通知訂閱者更新狀態。
我們就針對Observer觀察者部分寫了一個簡易的程式碼如下:


function Observer (data) {
  this.data = data
  this.walk(data)
}
Observer.prototype = {
  walk: function (data) {
    let me = this
    Object.keys(data).forEach(function (key) {
      me.convert(key, data[key])
    })
  },
  
  convert: function (key, val) {
    this.defineReactive(this.data, key, val)
  },

  defineReactive: function (data, key, val) {
    let dep = new Dep()
    let childObj = observe()

    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false,
      get: function () {
        if (Dep.target) {
            dep.depend()  // 新增訂閱者
        }
        return val
      },
      set: function (newVal) {
        if (newVal === val)  return

        val = newVal
        childObj = observe(newVal)
        dep.notify()  // 通知訂閱器
      }
    })
  }
}
function observe (value, vm) {
  if (!value || typeof value !== 'object') {
      return
  }

  return new Observer(value)
}

複製程式碼

以上程式碼中我們定義了一個Observer建構函式,即觀察者。利用Object.defineProperty我們將傳入物件的所有屬性(包含子屬性)全部進行資料監聽,並在get方法中,在訂閱器裡新增一條訂閱。一旦某屬性發生改變,通知到訂閱器。

Dep訂閱器,Compile指令,Watcher訂閱者的程式碼就不再分析,mvvm的總體結構可以由下圖看出

Proxy實現vue MVVM實踐
整個過程是一個觀察者、訂閱器、訂閱者、指令器四部分的事情,各司其職。

簡單介紹Proxy

Proxy是ES6中新增的建構函式,它可以理解為在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾改寫。Proxy原意是代理,在這裡我們可以理解為“代理”某些操作。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`proxy get ${key}`)
    return Reflect.get(target, key, receiver)
  },
  set: function (target, key, value, receiver) {
    console.log(`proxy set ${key}`)
    return Reflect.set(target, key, value, receiver)
  }
})
複製程式碼

上面程式碼對一個空物件架設了一層攔截,我們可以在Proxy的第二個引數中傳入一個handler物件,物件中可以定義攔截行為。
getset中,我們都用到了ReflectReflect物件與Proxy物件一樣,也是ES6位了操作物件而提供的新API。Reflect物件的方法與Proxy物件的方法一一對應,比如Proxy方法攔截target物件的屬性賦值行為。它採用Reflect.set方法將值賦值給物件的屬性,確保完成原有的行為,然後再部署額外的功能。
根據以上程式碼我們寫一段測試程式碼:

obj.text = 'hello world!' 
// proxy set text
var _text = obj.text
// proxy get text
複製程式碼

Proxy改寫觀察者

利用以上Proxy的一些特性,我們修改程式碼如下:

function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }

  let dep = new Dep()
  return new Proxy(value, {
    get: function (target, key, receiver) {
      if (Dep.target) {
        dep.depend()
      }
      return Reflect.get(target, key, receiver)
    },
    set: function (target, key, value, receiver) {
      dep.notify()
      return Reflect.set(target, key, value, receiver)
    }
  })
}
複製程式碼

我們將傳入的物件直接替換為Proxy物件,入參handlergetset中的新增訂閱者和通知訂閱器邏輯保持不變。
整個過程沒有做其他多餘的判斷,由於Vue3.0還沒有釋出,沒有實際原始碼可以借鑑,所以以上只是個人實現的簡單版本(完整程式碼)。將整個mvvm運用到html中,以下是執行後的效果(沒做gif,湊合看吧):

Proxy實現vue MVVM實踐

重寫資料監聽機制的好處

  1. 放棄了Object.defineProperty,基於 Proxy 觀察者機制以滿足全語言覆蓋及更好的效能。加上其它方法的優化改動,vue3.0可以提速一倍/記憶體使用降低一半;
  2. Observer模組將可以單獨作為一個庫來使用。

可能產生的問題

很遺憾的是,ES6的Proxy無法被轉譯為ES5,所以它將不被IE所支援。對於這個問題,Vue3.0將給出IE11的相容方案,即在IE11下,還是使用的Object.defineProperty機制。

參考資料

ECMAScript 6 入門(阮一峰):es6.ruanyifeng.com/#docs/proxy
vue 3.0 更新計劃:更快,更小,讓開發者更輕鬆: www.oschina.net/news/101906…
為什麼Proxy可以優化vue的資料監聽機制: juejin.im/post/5bfe33…

相關文章