一、數據綁定
之前的 一篇文章 中,系統的學習了 vue.js 中數據綁定的使用方式,但是對于其基本原理卻缺乏更進一步的理解,本篇將嘗試理解其中的思路,并實現一個類似的簡單示例。
注:vue.js 官網文檔也進行了說明,雙向綁定的 v-model 所引入的視圖(View)到模型(Model)更新,不過是基于 input 事件的一個語法糖,這里不再考慮雙向綁定中視圖到數據的更新,而重點關注模型(Model)到視圖(View)的更新(單向綁定)。
二、基本理論依據——觀察者模式
關于觀察者模式的理論此處不再贅述,而類似 vue.js 之類的 MVVM 框架,基本都是由 View-Model 層擔任觀察者,觀察 Model 層數據的數據變化,并將其通知給 View 層,進而實現 Model 層更新時,View 隨之更新的效果。
三、實操
(一)方案一
較老的實現方案或者說要求兼容較為古老的瀏覽器的框架中,它們一般是由框架自身提供出來一套 get/set 方法給開發者,開發者可以通過這個方法讀 / 寫模型里面的數據,而避免開發者直接接觸到原始數據。在實現 set 這個方法時,除了修改模型數據的時候,同時也會檢查數據的更新是否需要通知視圖進行更新。
下面來一個簡單的例子(可以點擊這里查看效果):
<div id="app"></div>
<button id="add">add</button>
<script>
var app = document.getElementById('app');
var btn = document.getElementById('add');
var model = (function () {
var data = {
counter: 0
};
app.innerHTML = data.counter;
return {
set(key, val) {
if(val !== data[key]) {
data[key] = val;
app.innerHTML = data.counter;
}
},
get(key) {
return data[key];
}
}
})();
btn.onclick = function () {
model.set('counter', model.get('counter') + 1);
};
</script>
每次我們點擊 add 按鈕的時候,頁面上 #app
元素的內容便會 +1。這里主要依靠的就是我們自己維護的一個核心數據以及與之對應的 get/set 方法。這里我們通過立即執行匿名函數的形式,在這里封入了一個 data
對象,這一個 data 對象是不能直接被開發者接觸到的,因為如果開發者不小心通過直接修改 data
對象的方式,將不會觸發視圖更新。我們通過閉包的方式,返回了一個包含了 get/set 方法的對象,這個對象最后被賦值給了變量 model
。而 model
上的 get/set 方法作為在前面的匿名函數中定義的方法,擁有訪問這個閉包內數據的特權。我們通過 model.get('counter')
可以獲取到當前的數據的值,通過 model.set('counter', xxxxx)
可以設置當前數據的值,這一切的讀寫行為對于框架而言都是可見的,框架便有機會在必要的時候(比如這里 data.counter
被修改的時候),更新視圖(比如這里的 app.innerHTML = data.counter;
)。上面這段代碼只是一個示例,僅供參考,一般框架的實現方式遠比這個來得復雜。
(二)方案二
得益于 ES5 為我們新增的 getter/setter 特性,我們可以有更簡潔、高效的方式實現“觀察者”這一功能。如果你對它們還不熟,可以參考 MDN 相關文檔。
下面來一個示例(可以點擊這里查看效果):
<div id="app"></div>
<button id="add">add</button>
<script>
var app = document.getElementById('app');
var btn = document.getElementById('add');
var model = {
counter: 0,
set counter (val) {
this.counter = val;
},
get counter() {
return this.counter;
}
};
app.innerHTML = model.counter;
btn.onclick = function () {
model.counter += 1;
};
</script>
等等,小編,你騙我?你這個怎么沒有顯示出來?而且點擊按鈕的時候還提示 “Uncaught RangeError: Maximum call stack size exceeded”
?
這個地方確實是一個常見的坑,主要問題出在我們的 getter 上,我們在 getter 函數中執行 this.counter
相當于再次調用了 getter,這里最終就變成了無限遞歸調用,最終導致調用棧溢出,所以提示了 “Maximum call stack size exceeded”
。這里是使用 getter 很常見的誤區,小編一開始也是想當然地寫成了這種形式,囧。。。
那我們改進一下吧(可以點擊這里查看效果):
<div id="app"></div>
<button id="add">add</button>
<script>
var app = document.getElementById('app');
var btn = document.getElementById('add');
var model = (function () {
var data = {
counter: 0
};
app.innerHTML = data.counter;
return {
set counter (val) {
data.counter = val;
app.innerHTML = val;
},
get counter() {
return data.counter;
}
};
})();
btn.onclick = function () {
model.counter += 1;
};
</script>
哇,小編,你有毒吧?你不是說使用 getter/setter 更簡潔、高效嗎?為啥還是引入了一個閉包,搞得看起來那么復雜。
為了解決調用棧溢出的問題,我們嘗試維護了兩個不同的數據對象。一個是我們的真正的模型數據 data
,它包含了真正的數據信息;另一個則是我們定義為 model
的對象,它實際上并不是真正的模型,而是我們模型的一個代理。嘗試讀寫 model
對象的時候最終將去讀寫 data
對象。而這里的閉包,主要還是考慮到防止開發者直接讀寫 data
對象。如果不喜歡,在支持的情況下,也可以通過塊級作用域或者其它方式對其進行屏蔽。
至于簡潔的問題,對比
model.set('counter', model.get('counter') + 1);
和
model.counter += 1;
應該不難看出吧。。。
而高效地話,前者是通過我們自己維護的 get/set 方法,而后者使用原生的 getter/setter,一般原生的代碼都是經過優化的,我們自己實現的在效率上根本遠遠趕不上。
四、結論
(一)自己實現 get/set 方法
優點:兼容性好;
確定:需要自己去實現一套“觀察者”,使用體驗不好(必須通過指定的方式讀寫數據),且效率相對較低;
(二)基于原生的 getter/setter
優點:實現簡單,使用體驗好(讀寫和其它一般的屬性無異),且效率相對較高;
缺點:ES5 才開始支持,較古老瀏覽器可能不支持(這也是 vue.js 不支持 ie8 的原因)。
注:以上內容,純屬小編在瞎扯,僅供參考~