JavaScript資料繫結實現一個簡單的 MVVM 庫,javascriptmvvm
推薦閱讀:
實現非常簡單的js雙向資料繫結
MVVM 是 Web 前端一種非常流行的開發模式,利用 MVVM 可以使我們的代碼更專註於處理商務邏輯而不是去關心 DOM 操作。目前著名的 MVVM 架構有 vue, avalon , react 等,這些架構各有千秋,但是實現的思想大致上是相同的:資料繫結 + 視圖重新整理。出於好奇和一顆願意折騰的心,我自己也沿著這個方向寫了一個最簡單的 MVVM 庫 ( mvvm.js ),總共 2000 多行代碼,指令的命名和用法與 vue 相似,在這裡分享一下實現的原理以及My Code組織思路。
思路整理
MVVM 在概念上是真正將視圖與資料邏輯分離的模式,ViewModel 是整個模式的重點。要實現 ViewModel 就需要將資料模型(Model)和視圖(View)關聯起來,整個實現思路可以簡單的總結成 5 點:
實現一個 Compiler 對元素的每個節點進行指令的掃描和提取;
實現一個 Parser 去解析元素上的指令,能夠把指令的意圖通過某個重新整理函數更新到 dom 上(中間可能需要一個專門負責視圖重新整理的模組)比如解析節點 <p v-show="isShow"></p> 時先取得 Model 中 isShow 的值,再根據 isShow 更改 node.style.display 從而控制元素的顯示和隱藏;
實現一個 Watcher 能將 Parser 中每條指令的重新整理函數和對應 Model 的欄位聯絡起來;
實現一個 Observer 使得能夠對對象的所有欄位進行值的變化監測,一旦發生變化時可以拿到最新的值並觸發通知回調;
利用 Observer 在 Watcher 中建立一個對 Model 的監聽 ,當 Model 中的一個值發生變化時,監聽被觸發,Watcher 拿到新值後調用在步驟 2 中關聯的那個重新整理函數,就可以實現資料變化的同時重新整理視圖的目的。
效果樣本
首先粗看下最終的使用樣本,與其他 MVVM 架構的執行個體化大同小異:
<div id="mobile-list"><h1 v-text="title"></h1><ul><li v-for="item in brands"><b v-text="item.name"></b><span v-show="showRank">Rank: {{item.rank}}</span></li></ul></div>var element = document.querySelector('#mobile-list');var vm = new MVVM(element, {'title' : 'Mobile List','showRank': true,'brands' : [{'name': 'Apple', 'rank': 1},{'name': 'Galaxy', 'rank': 2},{'name': 'OPPO', 'rank': 3}]});vm.set('title', 'Top 3 Mobile Rank List'); // => <h1>Top 3 Mobile Rank List</h1>
模組劃分
我把 MVVM 分成了五個模組去實現: 編譯模組 Compiler 、解析模組 Parser 、視圖重新整理模組 Updater 、資料訂閱模組 Watcher 和 資料監聽模組 Observer 。流程可以簡述為:Compiler 編譯好指令後將指令資訊交給解析器 Parser 解析,Parser 更新初始值並向 Watcher 訂閱資料的變化,Observer 監測到資料的變化然後反饋給 Watcher ,Watcher 再將變化結果通知 Updater 找到對應的重新整理函數進行視圖的重新整理。
上述流程:
下文就介紹下這五個模組實現的基本原理(代碼只貼重點部分,完整的實現請到我的 Github 翻閱)
1. 編譯模組 Compiler
Compiler 的職責主要是對元素的每個節點進行指令的掃描和提取。因為編譯和解析的過程會多次遍曆整個節點樹,所以為了提高編譯效率在 MVVM 建構函式內部先將 element 轉成一個文檔片段形式的副本 fragment 編譯對象是這個文檔片段而不應該是目標元素,待全部節點編譯完成後再將文檔片段添加回到原來的真實節點中。
vm.complieElement 實現了對元素所有節點的掃描和指令提取:
vm.complieElement = function(fragment, root) {var node, childNodes = fragment.childNodes;// 掃描子節點for (var i = 0; i < childNodes.length; i++) {node = childNodes[i];if (this.hasDirective(node)) {this.$unCompileNodes.push(node);}// 遞迴掃描子節點的子節點if (node.childNodes.length) {this.complieElement(node, false);}}// 掃描完成,編譯所有含有指令的節點if (root) {this.compileAllNodes();}}
vm.compileAllNodes 方法將會對 this.$unCompileNodes 中的每個節點進行編譯(將指令資訊交給 Parser ),編譯完一個節點後就從緩衝隊列中移除它,同時檢查 this.$unCompileNodes.length 當 length === 0 時說明全部編譯完成,可以將文檔片段追加到真實節點上了。
2. 指令解析模組 Parser
當編譯器 Compiler 把每個節點的指令提取出來後就可以給到解析器解析了。每一個指令都有不同的解析方法,所有指令的解析方法只要做好兩件事:一是將資料值更新到視圖上(初始狀態),二是將重新整理函數訂閱到 Model 的變化監測中。這裡以解析 v-text 為例描述一個指令的大致解析方法:
parser.parseVText = function(node, model) {// 取得 Model 中定義的初始值 var text = this.$model[model];// 更新節點的文本node.textContent = text;// 對應的重新整理函數:// updater.updateNodeTextContent(node, text);// 在 watcher 中訂閱 model 的變化watcher.watch(model, function(last, old) {node.textContent = last;// updater.updateNodeTextContent(node, text);});}
3. 資料訂閱模組 Watcher
上個例子,Watcher 提供了一個 watch 方法來對資料變化進行訂閱,一個參數是模型欄位 model 另一個是回呼函數,回呼函數是要通過 Observer 來觸發的,參數傳入新值 last 和 舊值 old , Watcher 拿到新值後就可以找到 model 對應的回調(重新整理函數)進行更新視圖了。model 和 重新整理函數是一對多的關係,即一個 model 可以有任意多個處理它的回呼函數(重新整理函數),比如: v-text="title" 和 v-html="title" 兩個指令共用一個資料模型欄位。
添加資料訂閱 watcher.watch 實現方式為:
watcher.watch = function(field, callback, context) {var callbacks = this.$watchCallbacks;if (!Object.hasOwnProperty.call(this.$model, field)) {console.warn('The field: ' + field + ' does not exist in model!');return;}// 建立緩衝回呼函數的數組if (!callbacks[field]) {callbacks[field] = [];}// 緩衝回呼函數callbacks[field].push([callback, context]);}
當資料模型的 field 欄位發生改變時,Watcher 就會觸發緩衝數組中訂閱了 field 的所有回調。
4. 資料監聽模組 Observer
Observer 是整個 mvvm 實現的核心基礎,看過有一篇文章說 O.o (Object.observe) 將會引爆資料繫結革命,給前端帶來巨大影響力,不過很可惜,ES7 草案已經將 O.o 給廢棄了!目前也沒有瀏覽器支援!所幸的是還有 Object.defineProperty 通過攔截對象屬性的存取描述符(get 和 set) 可以類比一個簡單的 Observer :
// 攔截 object 的 prop 屬性的 get 和 set 方法Object.defineProperty(object, prop, {get: function() {return this.getValue(object, prop);},set: function(newValue) {var oldValue = this.getValue(object, prop);if (newValue !== oldValue) {this.setValue(object, newValue, prop);// 觸發變化回調this.triggerChange(prop, newValue, oldValue);}}});
然後還有個問題就是數組操作 ( push, shift 等) 該如何監測?所有的 MVVM 架構都是通過重寫該數組的原型來實現的:
observer.rewriteArrayMethods = function(array) {var self = this;var arrayProto = Array.prototype;var arrayMethods = Object.create(arrayProto);var methods = 'push|pop|shift|unshift|splice|sort|reverse'.split('|');methods.forEach(function(method) {Object.defineProperty(arrayMethods, method, function() {var i = arguments.length;var original = arrayProto[method];var args = new Array(i);while (i--) {args[i] = arguments[i];}var result = original.apply(this, args);// 觸發回調self.triggerChange(this, method);return result;});});array.__proto__ = arrayMethods;}
這個實現方式是從 vue 中參考來的,覺得用的很妙,不過數組的 length 屬性是不能夠被監聽到的,所以在 MVVM 中應避免操作 array.length
5. 視圖重新整理模組 Updater
Updater 在五個模組中是最簡單的,只需要負責每個指令對應的重新整理函數即可。其他四個模組經過一系列的折騰,把最後的成果交給到 Updater 進行視圖或者事件的更新,比如 v-text 的重新整理函數為:
updater.updateNodeTextContent = function(node, text) {node.textContent = text;}
v-bind:style 的重新整理函數:
updater.updateNodeStyle = function(node, propperty, value) {node.style[propperty] = value;}
雙向資料繫結的實現
表單元素的雙向資料繫結是 MVVM 的一個最大特點之一:
其實這個神奇的功能實現原理也很簡單,要做的只有兩件事:一是資料變化的時候更新表單值,二是反過來表單值變化的時候更新資料,這樣資料的值就和表單的值綁在了一起。
資料變化更新表單值利用前面說的 Watcher 模組很容易就可以做到:
watcher.watch(model, function(last, old) {input.value = last;});'
表單變化更新資料只需要即時監聽表單的值得變化事件並更新資料模型對應欄位即可:
var model = this.$model;input.addEventListenr('change', function() {model[field] = this.value;});‘
其他表單 radio, checkbox 和 select 都是一樣的原理。
以上,整個流程以及每個模組的基本實現思路都講完了,第一次在社區發文章,語言表達能力不太好,如有說的不對寫的不好的地方,希望大家能夠批評指正!
結語
折騰這個簡單的 mvvm.js 是因為原來自己的架構項目中用的是 vue.js 但是只是用到了它的指令系統,一大堆功能只用到四分之一左右,就想著只是實現 data-binding 和 view-refresh 就夠了,結果沒找這樣的 javascript 庫,所以我自己就造了這麼一個輪子。
雖說功能和穩定性遠不如 vue 等流行 MVVM 架構,代碼實現可能也比較粗糙,但是通過造這個輪子還是增長了很多知識的 ~ 進步在於折騰嘛!
目前我的 mvvm.js 只是實現了最本的功能,以後我會繼續完善、健壯它,如有興趣歡迎一起探討和改進~
您可能感興趣的文章:
- 查詢繫結資料島的表格中的文本並修改顯示方式的js代碼
- Json資料非同步綁定到介面的Table並且自動重新整理原理及代碼
- javascript:json資料的頁面綁定範例程式碼
- JavaScript實現LI列表資料繫結的方法
- angularjs學習筆記之雙向資料繫結
- 實現非常簡單的js雙向資料繫結
- 輕鬆實現javascript資料雙向繫結
- 深入學習AngularJS中資料的雙向繫結機制
- 詳解JavaScript的AngularJS架構中的範圍與資料繫結
- 執行個體剖析AngularJS架構中資料的雙向繫結運用