欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript數(shù)據(jù)綁定實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 MVVM 庫(kù)

 更新時(shí)間:2016年04月08日 14:34:40   作者:SegmentFault  
MVVM 是 Web 前端一種非常流行的開發(fā)模式,利用 MVVM 可以使我們的代碼更專注于處理業(yè)務(wù)邏輯而不是去關(guān)心DOM 操作。接下來(lái)通過(guò)本文給大家介紹JavaScript使用數(shù)據(jù)綁定實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 MVVM 庫(kù),感興趣的朋友一起學(xué)習(xí)吧

推薦閱讀:

實(shí)現(xiàn)非常簡(jiǎn)單的js雙向數(shù)據(jù)綁定

MVVM 是 Web 前端一種非常流行的開發(fā)模式,利用 MVVM 可以使我們的代碼更專注于處理業(yè)務(wù)邏輯而不是去關(guān)心 DOM 操作。目前著名的 MVVM 框架有 vue, avalon , react 等,這些框架各有千秋,但是實(shí)現(xiàn)的思想大致上是相同的:數(shù)據(jù)綁定 + 視圖刷新。出于好奇和一顆愿意折騰的心,我自己也沿著這個(gè)方向?qū)懥艘粋€(gè)最簡(jiǎn)單的 MVVM 庫(kù) ( mvvm.js ),總共 2000 多行代碼,指令的命名和用法與 vue 相似,在這里分享一下實(shí)現(xiàn)的原理以及我的代碼組織思路。

思路整理

MVVM 在概念上是真正將視圖與數(shù)據(jù)邏輯分離的模式,ViewModel 是整個(gè)模式的重點(diǎn)。要實(shí)現(xiàn) ViewModel 就需要將數(shù)據(jù)模型(Model)和視圖(View)關(guān)聯(lián)起來(lái),整個(gè)實(shí)現(xiàn)思路可以簡(jiǎn)單的總結(jié)成 5 點(diǎn):

實(shí)現(xiàn)一個(gè) Compiler 對(duì)元素的每個(gè)節(jié)點(diǎn)進(jìn)行指令的掃描和提??;

實(shí)現(xiàn)一個(gè) Parser 去解析元素上的指令,能夠把指令的意圖通過(guò)某個(gè)刷新函數(shù)更新到 dom 上(中間可能需要一個(gè)專門負(fù)責(zé)視圖刷新的模塊)比如解析節(jié)點(diǎn) <p v-show="isShow"></p> 時(shí)先取得 Model 中 isShow 的值,再根據(jù) isShow 更改 node.style.display 從而控制元素的顯示和隱藏;

實(shí)現(xiàn)一個(gè) Watcher 能將 Parser 中每條指令的刷新函數(shù)和對(duì)應(yīng) Model 的字段聯(lián)系起來(lái);

實(shí)現(xiàn)一個(gè) Observer 使得能夠?qū)?duì)象的所有字段進(jìn)行值的變化監(jiān)測(cè),一旦發(fā)生變化時(shí)可以拿到最新的值并觸發(fā)通知回調(diào);

利用 Observer 在 Watcher 中建立一個(gè)對(duì) Model 的監(jiān)聽 ,當(dāng) Model 中的一個(gè)值發(fā)生變化時(shí),監(jiān)聽被觸發(fā),Watcher 拿到新值后調(diào)用在步驟 2 中關(guān)聯(lián)的那個(gè)刷新函數(shù),就可以實(shí)現(xiàn)數(shù)據(jù)變化的同時(shí)刷新視圖的目的。

效果示例

首先粗看下最終的使用示例,與其他 MVVM 框架的實(shí)例化大同小異:

<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 分成了五個(gè)模塊去實(shí)現(xiàn): 編譯模塊 Compiler 、解析模塊 Parser 、視圖刷新模塊 Updater 、數(shù)據(jù)訂閱模塊 Watcher 和 數(shù)據(jù)監(jiān)聽模塊 Observer 。流程可以簡(jiǎn)述為:Compiler 編譯好指令后將指令信息交給解析器 Parser 解析,Parser 更新初始值并向 Watcher 訂閱數(shù)據(jù)的變化,Observer 監(jiān)測(cè)到數(shù)據(jù)的變化然后反饋給 Watcher ,Watcher 再將變化結(jié)果通知 Updater 找到對(duì)應(yīng)的刷新函數(shù)進(jìn)行視圖的刷新。

上述流程如圖所示:

下文就介紹下這五個(gè)模塊實(shí)現(xiàn)的基本原理(代碼只貼重點(diǎn)部分,完整的實(shí)現(xiàn)請(qǐng)到我的 Github 翻閱)

1. 編譯模塊 Compiler

Compiler 的職責(zé)主要是對(duì)元素的每個(gè)節(jié)點(diǎn)進(jìn)行指令的掃描和提取。因?yàn)榫幾g和解析的過(guò)程會(huì)多次遍歷整個(gè)節(jié)點(diǎn)樹,所以為了提高編譯效率在 MVVM 構(gòu)造函數(shù)內(nèi)部先將 element 轉(zhuǎn)成一個(gè)文檔碎片形式的副本 fragment 編譯對(duì)象是這個(gè)文檔碎片而不應(yīng)該是目標(biāo)元素,待全部節(jié)點(diǎn)編譯完成后再將文檔碎片添加回到原來(lái)的真實(shí)節(jié)點(diǎn)中。

vm.complieElement 實(shí)現(xiàn)了對(duì)元素所有節(jié)點(diǎn)的掃描和指令提?。?/p>

vm.complieElement = function(fragment, root) {
var node, childNodes = fragment.childNodes;
// 掃描子節(jié)點(diǎn)
for (var i = 0; i < childNodes.length; i++) {
node = childNodes[i];
if (this.hasDirective(node)) {
this.$unCompileNodes.push(node);
}
// 遞歸掃描子節(jié)點(diǎn)的子節(jié)點(diǎn)
if (node.childNodes.length) {
this.complieElement(node, false);
}
}
// 掃描完成,編譯所有含有指令的節(jié)點(diǎn)
if (root) {
this.compileAllNodes();
}
}

vm.compileAllNodes 方法將會(huì)對(duì) this.$unCompileNodes 中的每個(gè)節(jié)點(diǎn)進(jìn)行編譯(將指令信息交給 Parser ),編譯完一個(gè)節(jié)點(diǎn)后就從緩存隊(duì)列中移除它,同時(shí)檢查 this.$unCompileNodes.length 當(dāng) length === 0 時(shí)說(shuō)明全部編譯完成,可以將文檔碎片追加到真實(shí)節(jié)點(diǎn)上了。

2. 指令解析模塊 Parser

當(dāng)編譯器 Compiler 把每個(gè)節(jié)點(diǎn)的指令提取出來(lái)后就可以給到解析器解析了。每一個(gè)指令都有不同的解析方法,所有指令的解析方法只要做好兩件事:一是將數(shù)據(jù)值更新到視圖上(初始狀態(tài)),二是將刷新函數(shù)訂閱到 Model 的變化監(jiān)測(cè)中。這里以解析 v-text 為例描述一個(gè)指令的大致解析方法:

parser.parseVText = function(node, model) {
// 取得 Model 中定義的初始值 
var text = this.$model[model];
// 更新節(jié)點(diǎn)的文本
node.textContent = text;
// 對(duì)應(yīng)的刷新函數(shù):
// updater.updateNodeTextContent(node, text);
// 在 watcher 中訂閱 model 的變化
watcher.watch(model, function(last, old) {
node.textContent = last;
// updater.updateNodeTextContent(node, text);
});
}

3. 數(shù)據(jù)訂閱模塊 Watcher

上個(gè)例子,Watcher 提供了一個(gè) watch 方法來(lái)對(duì)數(shù)據(jù)變化進(jìn)行訂閱,一個(gè)參數(shù)是模型字段 model 另一個(gè)是回調(diào)函數(shù),回調(diào)函數(shù)是要通過(guò) Observer 來(lái)觸發(fā)的,參數(shù)傳入新值 last 和 舊值 old , Watcher 拿到新值后就可以找到 model 對(duì)應(yīng)的回調(diào)(刷新函數(shù))進(jìn)行更新視圖了。model 和 刷新函數(shù)是一對(duì)多的關(guān)系,即一個(gè) model 可以有任意多個(gè)處理它的回調(diào)函數(shù)(刷新函數(shù)),比如: v-text="title" 和 v-html="title" 兩個(gè)指令共用一個(gè)數(shù)據(jù)模型字段。

添加數(shù)據(jù)訂閱 watcher.watch 實(shí)現(xiàn)方式為:

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;
}
// 建立緩存回調(diào)函數(shù)的數(shù)組
if (!callbacks[field]) {
callbacks[field] = [];
}
// 緩存回調(diào)函數(shù)
callbacks[field].push([callback, context]);
}

當(dāng)數(shù)據(jù)模型的 field 字段發(fā)生改變時(shí),Watcher 就會(huì)觸發(fā)緩存數(shù)組中訂閱了 field 的所有回調(diào)。

4. 數(shù)據(jù)監(jiān)聽模塊 Observer

Observer 是整個(gè) mvvm 實(shí)現(xiàn)的核心基礎(chǔ),看過(guò)有一篇文章說(shuō) O.o (Object.observe) 將會(huì)引爆數(shù)據(jù)綁定革命,給前端帶來(lái)巨大影響力,不過(guò)很可惜,ES7 草案已經(jīng)將 O.o 給廢棄了!目前也沒有瀏覽器支持!所幸的是還有 Object.defineProperty 通過(guò)攔截對(duì)象屬性的存取描述符(get 和 set) 可以模擬一個(gè)簡(jiǎn)單的 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);
// 觸發(fā)變化回調(diào)
this.triggerChange(prop, newValue, oldValue);
}
}
});

然后還有個(gè)問(wèn)題就是數(shù)組操作 ( push, shift 等) 該如何監(jiān)測(cè)?所有的 MVVM 框架都是通過(guò)重寫該數(shù)組的原型來(lái)實(shí)現(xiàn)的:

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);
// 觸發(fā)回調(diào)
self.triggerChange(this, method);
return result;
});
});
array.__proto__ = arrayMethods;
}

這個(gè)實(shí)現(xiàn)方式是從 vue 中參考來(lái)的,覺得用的很妙,不過(guò)數(shù)組的 length 屬性是不能夠被監(jiān)聽到的,所以在 MVVM 中應(yīng)避免操作 array.length

5. 視圖刷新模塊 Updater

Updater 在五個(gè)模塊中是最簡(jiǎn)單的,只需要負(fù)責(zé)每個(gè)指令對(duì)應(yīng)的刷新函數(shù)即可。其他四個(gè)模塊經(jīng)過(guò)一系列的折騰,把最后的成果交給到 Updater 進(jìn)行視圖或者事件的更新,比如 v-text 的刷新函數(shù)為:

updater.updateNodeTextContent = function(node, text) {
node.textContent = text;
}

v-bind:style 的刷新函數(shù):

updater.updateNodeStyle = function(node, propperty, value) {
node.style[propperty] = value;
}

雙向數(shù)據(jù)綁定的實(shí)現(xiàn)

表單元素的雙向數(shù)據(jù)綁定是 MVVM 的一個(gè)最大特點(diǎn)之一:

其實(shí)這個(gè)神奇的功能實(shí)現(xiàn)原理也很簡(jiǎn)單,要做的只有兩件事:一是數(shù)據(jù)變化的時(shí)候更新表單值,二是反過(guò)來(lái)表單值變化的時(shí)候更新數(shù)據(jù),這樣數(shù)據(jù)的值就和表單的值綁在了一起。

數(shù)據(jù)變化更新表單值利用前面說(shuō)的 Watcher 模塊很容易就可以做到:

watcher.watch(model, function(last, old) {
input.value = last;
});'

表單變化更新數(shù)據(jù)只需要實(shí)時(shí)監(jiān)聽表單的值得變化事件并更新數(shù)據(jù)模型對(duì)應(yīng)字段即可:

var model = this.$model;
input.addEventListenr('change', function() {
model[field] = this.value;
});‘

其他表單 radio, checkbox 和 select 都是一樣的原理。

以上,整個(gè)流程以及每個(gè)模塊的基本實(shí)現(xiàn)思路都講完了,第一次在社區(qū)發(fā)文章,語(yǔ)言表達(dá)能力不太好,如有說(shuō)的不對(duì)寫的不好的地方,希望大家能夠批評(píng)指正!

結(jié)語(yǔ)

折騰這個(gè)簡(jiǎn)單的 mvvm.js 是因?yàn)樵瓉?lái)自己的框架項(xiàng)目中用的是 vue.js 但是只是用到了它的指令系統(tǒng),一大堆功能只用到四分之一左右,就想著只是實(shí)現(xiàn) data-binding 和 view-refresh 就夠了,結(jié)果沒找這樣的 javascript 庫(kù),所以我自己就造了這么一個(gè)輪子。

雖說(shuō)功能和穩(wěn)定性遠(yuǎn)不如 vue 等流行 MVVM 框架,代碼實(shí)現(xiàn)可能也比較粗糙,但是通過(guò)造這個(gè)輪子還是增長(zhǎng)了很多知識(shí)的 ~ 進(jìn)步在于折騰嘛!

目前我的 mvvm.js 只是實(shí)現(xiàn)了最本的功能,以后我會(huì)繼續(xù)完善、健壯它,如有興趣歡迎一起探討和改進(jìn)~

相關(guān)文章

最新評(píng)論