綁定的基礎是 propertyChange
事件。如何得知 viewModel
成員值的改變一直是開發 MVVM
框架的首要問題。主流框架的處理有一下三大類:css
另外開發一套 API。典型框架:Backbone.js
Backbone 有本身的 模型類 和 集合類。這樣作雖然框架開發簡單運行效率也高,但開發者不得不使用這套 API 操做 viewModel,致使上手複雜、代碼繁瑣。html
髒檢查機制。典型框架:angularjs
特色是直接使用 JS 原生操做對象的語法操做 viewModel,開發者上手簡單、代碼簡單。但髒檢查機制隨之帶來的就是性能問題。這點在我另外的一篇博文 《Angular 1 深度解析:髒數據檢查與 angular 性能優化》 有詳細講解這裏不另加贅述。前端
替換屬性。典型框架:vuejs
vuejs 把開發者定義的 viewModel 對象(即 data 函數返回的對象)中全部的(除某些前綴開頭的)成員替換爲屬性。這樣既可使用 JS 原生操做對象的語法,又是主動觸發 propertyChange
事件,效率也高。但這種方法也有一些限制,後文會分析。vue
Object.observe 是谷歌對於簡化雙向綁定機制的嘗試,在 Chrome 49 中引入。然而因爲性能等問題,並無被其餘各大瀏覽器及 ES 標準所接受。掙扎了一段時間後谷歌 Chrome 團隊宣佈收回 Object.observe 的提議,並在 Chrome 50 中徹底刪除了 Object.observe 實現。react
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 屬性被改成 1
。express
有了 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 倉庫
相較於 vuejs 的屬性替換,Proxy 實現的綁定至少有以下三個優勢:
無需預先定義待綁定的屬性。
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
不須要,由於它監聽的是整個對象。
對數組相性良好。
雖然說數組裏的方法能夠替換(push、pop等),可是數組下標卻不能替換爲屬性,以至必須搞出一個 set 方法用於對數組下標賦值。
更容易調試的 viewModel 對象。
因爲 vuejs 把對象中的全部成員所有替換成了屬性,若是想直接用 Chrome 的原生調試工具查看屬性值,你不得不挨個去點屬性後面的 (...)
:由於獲取屬性的值實際上是執行了屬性的 get
方法,執行一個方法可能會產生反作用,Chrome 把這個決定權留給開發者。
Proxy
對象不須要。Proxy
的 set
方法只是一層包裝,Proxy
對象自身維護原始對象的值,天然也能夠直接拿出原始值給開發者看。查看一個 Proxy
對象,只須要展開其內置屬性 [[Target]]
便可看到原始對象的全部成員的值。你甚至還能夠看到包裝原始對象的哪些 get
、set
函數——若是你感興趣的話。
雖然說使用 Proxy
實現雙向綁定的優勢很明顯,可是缺點也很明顯:Proxy
是 ES2015
的特性,它沒法被編譯爲 ES5,也沒法 Polyfill。IE 天然全軍覆沒;其餘各大瀏覽器實現的時間也較晚:Chrome 4九、Safari 10。瀏覽器兼容性極大的限制了 Proxy
的使用。可是我相信,隨着時間的推移,基於 Proxy
的前端 MVVM
框架也會出如今開發者眼前。
注:本文同時發佈在個人 sf 專欄