使用 Proxy 實現簡單的 MVVM 模型

綁定實現的歷史

綁定的基礎是 propertyChange 事件。如何得知 viewModel 成員值的改變一直是開發 MVVM 框架的首要問題。主流框架的處理有一下三大類:css

  1. 另外開發一套 API。典型框架:Backbone.js
    Backbone 有本身的 模型類集合類。這樣作雖然框架開發簡單運行效率也高,但開發者不得不使用這套 API 操做 viewModel,致使上手複雜、代碼繁瑣。html

  2. 髒檢查機制。典型框架:angularjs
    特色是直接使用 JS 原生操做對象的語法操做 viewModel,開發者上手簡單、代碼簡單。但髒檢查機制隨之帶來的就是性能問題。這點在我另外的一篇博文 《Angular 1 深度解析:髒數據檢查與 angular 性能優化》 有詳細講解這裏不另加贅述。前端

  3. 替換屬性。典型框架:vuejs
    vuejs 把開發者定義的 viewModel 對象(即 data 函數返回的對象)中全部的(除某些前綴開頭的)成員替換爲屬性。這樣既可使用 JS 原生操做對象的語法,又是主動觸發 propertyChange 事件,效率也高。但這種方法也有一些限制,後文會分析。vue

Object.observe

Object.observe 是谷歌對於簡化雙向綁定機制的嘗試,在 Chrome 49 中引入。然而因爲性能等問題,並無被其餘各大瀏覽器及 ES 標準所接受。掙扎了一段時間後谷歌 Chrome 團隊宣佈收回 Object.observe 的提議,並在 Chrome 50 中徹底刪除了 Object.observe 實現。react

Proxy

Proxy(代理)是 ES2015 加入的新特性,用於對某些基本操做定義自定義行爲,相似於其餘語言中的面向切面編程。它的其中一個做用就是用於(部分)替代 Object.observe 以實現雙向綁定。git

例若有一個對象angularjs

let viewModel = {};

能夠構造對應的代理類實現對 viewModel 的屬性賦值操做的監聽:github

viewModel = new Proxy(viewModel, {
  set(obj, prop, value) {
    if (obj[prop] !== value) {
      obj[prop] = value;
      console.log(`${prop} 屬性被改成 ${value}`);
    }
    return true;
  }
});

這時全部對 viewModel 的屬性賦值的操做都不會直接生效,而是將這個操做轉發給 Proxy 中註冊的 set 方法,其中的參數 obj 是原始對象(注意不能直接用 a,不然還會觸發代理函數,形成無限遞歸),prop 是被賦值的屬性名,value 是待賦的值。 若是有:正則表達式

viewModel.test = 1;

這時就會輸出 test 屬性被改成 1express

用 Proxy 實現簡單的單向綁定。

有了 Proxy 就能夠得知 viewModel 中屬性的變動了,還須要更新頁面上綁定此屬性的元素。

簡單起見,咱們用 this 表示 viewModel 自己,使用 this.XXX 就表示依賴 XXX 屬性。有 DOM 以下:

<div my-bind="'str1 + str2 = ' + (this.str1 + this.str2)"></div>
  <div my-bind="'num1 - num2 = ' + (this.num1 - this.num2)"></div>

首先要得到全部使用了單向綁定的元素:

const bindingElements = [...document.querySelectorAll('[my-bind]')];

獲取綁定表達式:

bindingElements.forEach(el => {
  const expression = el.getAttribute('my-bind');
});

因爲得到的表達式是個字符串,須要構造一個函數去執行它,獲得表達式的結果:

const expression = el.getAttribute('my-bind');
const result = new Function('"use strict";\nreturn ' + expression).call(viewModel);

代碼中會動態建立一個函數,內容就是將字符串解析執行後將其結果返回(相似 eval,但更安全)。將結果放到頁面上就能夠了:

el.textContent = result;

與上文的 viewModel 結合起來:

const bindingElements = [...document.querySelectorAll('[my-bind]')];

window.viewModel = new Proxy({}, { // 設置全局變量方便調試
  set(obj, prop, value) {
    if (obj[prop] !== value) {
      obj[prop] = value;

      bindingElements.forEach(el => {
        const expression = el.getAttribute('my-bind');
        const result = new Function('"use strict";\nreturn ' + expression)
          .call(obj);
        el.textContent = result;
      });
    }
    return true;
  }
});

若是實際放在瀏覽器中運行的話,改變 viewModel 中屬性的值就會觸發頁面的更新。

示例中寫了循環會更新全部綁定元素,比較好的方式是隻更新對當前變動屬性有依賴的元素。這時就要分析綁定表達式的屬性依賴。 簡單起見可使用正則表達式解析屬性依賴:

let match;
while (match = /this(?:\.(\w+))+/g.exec(expression)) {
  match[1] // 屬性依賴
}

添加事件綁定

事件綁定即綁定原生事件,在事件觸發時執行綁定表達式,表達式調用 viewModel 中的某個回調函數。

click 事件爲例。依然是獲取全部綁定了 click 事件的元素,並執行表達式(表達式的值被丟棄)。與單項綁定不一樣的是:執行表達式須要傳入事件的 event 參數。

[...document.querySelectorAll('[my-click]')].forEach(el => {
  const expression = el.getAttribute('my-click');
  const fn = new Function('$event', '"use strict";\n' + expression);
  el.addEventListener('click', event => {
    fn.call(viewModel, event);
  });
});

Function 對象的構造函數,前 n-1 個參數是生成的函數對象的參數名,最後一個是函數體。代碼中構造了包含一個 $event 參數的函數,函數體就是直接執行綁定表達式。

雙向綁定

雙向綁定就是單項綁定和事件綁定的結合體。綁定元素的 input 事件來修改 viewModel 的屬性,而後再單項綁定元素的 value 屬性修改元素的值。

這裏是一個較爲完整的示例:http://sandbox.runjs.cn/show/7wqpuofo。完整的代碼放在個人 GitHub 倉庫

使用 Proxy 實現雙向綁定的優缺點

相較於 vuejs 的屬性替換,Proxy 實現的綁定至少有以下三個優勢:

  1. 無需預先定義待綁定的屬性。
    vuejs 要作屬性(getter, setter 方法)替換,首先須要知道有哪些屬性須要替換,這樣致使必須預先定義須要替換的屬性,也就是 vuejs 中的 data 方法。vuejs 中 data 方法必須定義完整全部綁定屬性,不然對應綁定不能正常工做。
    Vue 不能檢測到對象屬性的添加或刪除Property or method "XXX" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.Proxy 不須要,由於它監聽的是整個對象。

  2. 對數組相性良好。
    雖然說數組裏的方法能夠替換(push、pop等),可是數組下標卻不能替換爲屬性,以至必須搞出一個 set 方法用於對數組下標賦值

  3. 更容易調試的 viewModel 對象。
    因爲 vuejs 把對象中的全部成員所有替換成了屬性,若是想直接用 Chrome 的原生調試工具查看屬性值,你不得不挨個去點屬性後面的 (...):由於獲取屬性的值實際上是執行了屬性的 get 方法,執行一個方法可能會產生反作用,Chrome 把這個決定權留給開發者。
    Proxy 對象不須要。Proxyset 方法只是一層包裝,Proxy 對象自身維護原始對象的值,天然也能夠直接拿出原始值給開發者看。查看一個 Proxy 對象,只須要展開其內置屬性 [[Target]] 便可看到原始對象的全部成員的值。你甚至還能夠看到包裝原始對象的哪些 getset 函數——若是你感興趣的話。

雖然說使用 Proxy 實現雙向綁定的優勢很明顯,可是缺點也很明顯:ProxyES2015 的特性,它沒法被編譯爲 ES5,也沒法 Polyfill。IE 天然全軍覆沒;其餘各大瀏覽器實現的時間也較晚:Chrome 4九、Safari 10。瀏覽器兼容性極大的限制了 Proxy 的使用。可是我相信,隨着時間的推移,基於 Proxy 的前端 MVVM 框架也會出如今開發者眼前。

注:本文同時發佈在個人 sf 專欄

相關文章
相關標籤/搜索