# 前言
? MVVM 是與 MVC 進(jìn)化出來的,區(qū)別在與將view層的數(shù)據(jù)變動直接響應(yīng)到viewModel層上而不是響應(yīng)給model荤西,其表現(xiàn)上最大的區(qū)別就在于雙向數(shù)據(jù)綁定功能
?
# 雙向數(shù)據(jù)綁定原理簡介
發(fā)布者-訂閱者模式(Backbone.js) 一般通過sub, pub的方式實現(xiàn)數(shù)據(jù)和視圖的綁定監(jiān)聽澜搅,更新數(shù)據(jù)方式通常做法是 vm.set('property', value)
臟值檢查(Angular.js) 通過一定的事件觸發(fā)檢測數(shù)據(jù)是否有變更來決定是否需要更新視圖伍俘。如用戶操作ng-click邪锌,XHR事件勉躺,瀏覽器Location變更事件 (timeout ,
digest() 或 $apply()等
數(shù)據(jù)劫持(vue.js)
? Vue 的雙向數(shù)據(jù)綁定的原理關(guān)心兩個要點
-
數(shù)據(jù)劫持: 通過
Object.definedProperty()
劫持并監(jiān)聽數(shù)據(jù)變動 -
觀察者模式: 通過
發(fā)布者-訂閱者
模式實現(xiàn)數(shù)據(jù)更新
?
# Object.definedProperty()
? 這是一個原生JavaScript標(biāo)準(zhǔn)庫中的一個方法饵溅,被稱作是對象屬性的精確添加和修改。用于直接在一個對象上定義一個新屬性妇萄,或者修改一個對象的現(xiàn)有屬性蜕企, 并返回這個對象。
Object.defineProperty(obj, prop, descriptor)
? 從圖上可以看得出來轻掩,這個“精確添加和修改”的方法最重要的是屬性操作符的配置。需要提到的一點是懦底,屬性操作符分類:存取操作符
和 數(shù)據(jù)操作符
唇牧。上圖也有表示绰播,指定時兩中操作符只能選擇其一豁延,不能混用,否者報錯轧葛。
? 被操作的屬性如果不存在杆查,則會創(chuàng)建這個屬性扮惦;如果已存在,則執(zhí)行更新亲桦。
?
添加屬性
常用的
var obj = {};
obj.a = 1;
這種添加屬性的辦法實質(zhì)上就是執(zhí)行的如下操作崖蜜。需要強調(diào)的是:直接賦值的操作符設(shè)置的都是true,而這些操作符他們本身的默認(rèn)值都是false客峭,注意區(qū)分纳猪。
var obj = {};
// 在對象中添加一個屬性與數(shù)據(jù)描述符的示例
Object.defineProperty(o, "attr", {
value : 37,
writable : true,
enumerable : true,
configurable : true
});
【解讀】本例的意思是 給對象obj
添加屬性attr
,它的值為37桃笙,并且設(shè)置屬性attr
的值可以被修改(writable)氏堤,加入到可枚舉隊列中以便for-in / for-of
等操作可以遍歷到(enumerable),設(shè)置配置屬性可以修改搏明,且該屬性可以被delete obj.attr
刪除(configurable)鼠锈,所以有以下操作
obj.attr = 20;
console.log( obj.attr ); // 20
delete obj.attr;
console.log(obj.attr); // undefined
?
修改屬性(writable)
正常情況下屬性都需要修改功能,需要設(shè)置操作符writable值為true星著。當(dāng)設(shè)置為false時购笆,并不會拋出錯誤,但是值不會被修改虚循。在嚴(yán)格模式下同欠,會拋出錯誤
throw Error:xx is read-only
var o = {}; // Creates a new object
Object.defineProperty(o, 'a', {
value: 37,
writable: false
});
console.log(o.a); // logs 37
o.a = 25; // No error thrown
console.log(o.a); // logs 37.
?
是否可被for...in和Object.keys()獲妊(enumerable)
var o = {};
Object.defineProperty(o, "a", { value : 1, enumerable: true });
Object.defineProperty(o, "b", { value : 2, enumerable: false });
Object.defineProperty(o, "c", { value : 3 }); // enumerable 默認(rèn)是false
o.d = 4; // 采用默認(rèn)賦值的方式,屬性操作符enumerable為true(configurable也是true)
for (var i of o) {
console.log(i); // 打印 'a' 和 'd'
}
Object.keys(o); // ["a", "d"]
o.propertyIsEnumerable('a'); // true
o.propertyIsEnumerable('b'); // false
?
設(shè)置屬性不可被刪除 (configurable)
? configurable
特性表示對象的屬性是否可以被刪除铺遂,以及除writable
特性外的其他特性是否可以被修改衫哥。【這里并不是說設(shè)置configurable
之后就不能設(shè)置其他操作符襟锐,而是不能修改撤逢,如下enumerable
屬性】
var o = {};
Object.defineProperty(o, "a", {
get : function(){return 1;},
enumerable: true,
configurable: false
});
// 以下例子會拋出異常,因為屬性a的值不可被修改了
Object.defineProperty(o, "a", {value : 12});
console.log(o.a); // logs 1
delete o.a; // 不會拋出錯誤粮坞,但執(zhí)行不成功
console.log(o.a); // logs 1
當(dāng)然蚊荣,如果把configurable
設(shè)置為true
,delete o.a
就能將a
屬性正確刪除
?
能實現(xiàn) 數(shù)據(jù)劫持
的存取操作符 get/set
? get()
和set()
是Object.definedProperty()
中非常重要的兩個操作符莫杈,當(dāng)屬性被訪問時互例,get
方法被執(zhí)行,傳入的參數(shù)列表為空筝闹。當(dāng)屬性被修改時媳叨,只有一個參數(shù),即新值丁存。注意參數(shù)列表雖然沒體現(xiàn)但默認(rèn)都帶有this
對象肩杈,并且需要注意this
有可能是本身屬性,也有可能是繼承屬性解寝。另外扩然,訪問器屬性的會"覆蓋"同名的普通屬性,因為訪問器屬性會被優(yōu)先訪問聋伦,與其同名的普通屬性則會被忽略夫偶。
| obj.attr = 10
這種直接賦值操作對應(yīng)的 getter 和 setter 邏輯如下
var obj = { attr: 10 }
Object.definedProperty(o, 'attr', {
enumerable : true,
configurable : true,
get: function() {
console.log('get方法被調(diào)用了');
return this.attr
},
set: function(newValue) {
console.log('set方法被調(diào)用了,參數(shù)是: ' + newVal);
this.attr = newValue
}
})
obj.attr; // get方法被調(diào)用了
obj.attr = 3; // set方法被調(diào)用了觉增,參數(shù)是: 3
?
| 數(shù)據(jù)劫持的實現(xiàn)
? 正因為get
和set
的存在兵拢,我們可以在其中自行添加一些處理邏輯
// 屬性操作符配置
var pattern = {
enumerable : true,
configurable : true,
get: function () {
return 'I alway return this string,whatever you have assigned';
},
set: function (newVal) {
this.myname = 'this is my name string';
}
};
function TestDefineSetAndGet() {
Object.defineProperty(this, 'attr', pattern);
};
var instance = new TestDefineSetAndGet();
instance.attr = 'baby';
console.log(instance.attr); // I alway return this string,whatever you have assigned
console.log(instance.myname); // this is my name string
?
# 極簡的雙向數(shù)據(jù)綁定
? 此例實現(xiàn)的效果是:隨文本框輸入文字的變化,
span
中會同步顯示相同的文字內(nèi)容逾礁;在js或控制臺顯式的修改 obj.hello
的值说铃,視圖會相應(yīng)更新。這樣就實現(xiàn)了 model => view
以及 view => model
的雙向綁定嘹履。?
# Vue 實現(xiàn)雙向數(shù)據(jù)綁定
?整理了一下腻扇,要實現(xiàn)mvvm的雙向綁定,就必須要實現(xiàn)以下幾點:
1砾嫉、實現(xiàn)一個數(shù)據(jù)監(jiān)聽器Observer幼苛,能夠?qū)?shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽,如有變動可拿到最新值并通知訂閱者
2焕刮、實現(xiàn)一個指令解析器Compile舶沿,對每個元素節(jié)點的指令進(jìn)行掃描和解析墙杯,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù)
3括荡、實現(xiàn)一個Watcher高镐,作為連接Observer和Compile的橋梁,能夠訂閱并收到每個屬性變動的通知一汽,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù)避消,從而更新視圖
4低滩、mvvm入口函數(shù)召夹,整合以上三者
|實現(xiàn)Observer - 監(jiān)聽每個數(shù)據(jù)的變化
? 現(xiàn)在我們知道可以利用Obeject.defineProperty()
來監(jiān)聽屬性變動, 那么將需要observe的數(shù)據(jù)對象進(jìn)行遞歸遍歷恕沫,包括子屬性對象的屬性监憎,都加上 setter和getter。這樣的話婶溯,給這個對象的某個值賦值鲸阔,就會觸發(fā)setter,那么就能監(jiān)聽到了數(shù)據(jù)變化迄委。相關(guān)的代碼可以是這樣的
var data = {name: 'Crain'}
observe(data);
data.name = "Ocaka"; // 監(jiān)聽到數(shù)據(jù)變化 Crain => Ocaka (set方法中打印)
function observe() {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
})
}
function defineReactive(data, key, val) {
observe(val); // 深度遞歸
Object.defineProperty(data, key, {
enumerable: true, // 可以被枚舉
configurable: false, // 不能再被defined
get: function() {
return val;
},
set: function(newVal) {
console.log('監(jiān)聽到變化 ', val, '=>', newVal)
val = newVal
}
})
}
?
| 將數(shù)據(jù)變化通知訂閱者
? 我們已經(jīng)可以監(jiān)聽每個數(shù)據(jù)的變化了褐筛,那么監(jiān)聽到變化之后就是怎么通知訂閱者了,所以接下來我們需要實現(xiàn)一個消息訂閱器叙身,很簡單渔扎,維護(hù)一個數(shù)組,用來收集訂閱者信轿,數(shù)據(jù)變動觸發(fā)notify
晃痴,再調(diào)用訂閱者的update
方法,代碼改善之后是這樣:
// ..省略
function definedReactive(data, key, val) {
var dep = new Dep(); // 實例化一個訂閱器
observe(val); // 深度遞歸
Object.defineProperty(data, key, {
// ... 省略
set: function(newVal) {
if (val === newVal) return;
console.log('監(jiān)聽到變化 ', val, '=>', newVal);
val = newVal;
dep.notify(); // 通知所有訂閱者
}
});
}
// Dep訂閱器構(gòu)造函數(shù)
function Dep() {
this.subs = []; // 收集訂閱者
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
那么問題來了财忽,誰是訂閱者倘核?怎么往訂閱器添加訂閱者?
沒錯即彪,上面的思路整理中我們已經(jīng)明確訂閱者應(yīng)該是Watcher
, 而且var dep = new Dep();
是在 defineReactive
方法內(nèi)部定義的紧唱,所以想通過dep
添加訂閱者,就必須要在閉包內(nèi)操作隶校,所以我們可以在 getter
里面動手腳:
// Observer.js
// ...省略
Object.defineProperty(data, key, {
get: function() {
// 由于需要在閉包內(nèi)添加watcher漏益,所以通過Dep定義一個全局target屬性,暫存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key]; // 這里會觸發(fā)屬性的getter惠况,從而添加訂閱者
Dep.target = null;
}
}
? 這里已經(jīng)實現(xiàn)了一個Observer了遭庶,已經(jīng)具備了監(jiān)聽數(shù)據(jù)和數(shù)據(jù)變化通知訂閱者的功能,完整代碼稠屠。那么接下來就是實現(xiàn)Compile了
| 實現(xiàn) Compile
? compile
主要做的事情是解析模板指令峦睡,將模板中的變量替換成數(shù)據(jù)翎苫,然后初始化渲染頁面視圖,并將每個指令對應(yīng)的節(jié)點綁定更新函數(shù)榨了,添加監(jiān)聽數(shù)據(jù)的訂閱者煎谍,一旦數(shù)據(jù)有變動,收到通知龙屉,更新視圖呐粘,如圖所示:
因為遍歷解析的過程有多次操作dom節(jié)點,為提高性能和效率转捕,會先將跟節(jié)點el轉(zhuǎn)換成文檔碎片fragment進(jìn)行解析編譯操作作岖,解析完成,再將fragment添加回原來的真實dom節(jié)點中
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// 將原生節(jié)點拷貝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};
compileElement方法將遍歷所有節(jié)點及其子節(jié)點五芝,進(jìn)行掃描解析編譯痘儡,調(diào)用對應(yīng)的指令渲染函數(shù)進(jìn)行數(shù)據(jù)渲染,并調(diào)用對應(yīng)的指令更新函數(shù)進(jìn)行綁定枢步,詳看代碼及注釋說明:
Compile.prototype = {
// ... 省略
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; // 表達(dá)式文本
// 按元素節(jié)點方式編譯
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遍歷編譯子節(jié)點
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 規(guī)定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令為 v-text
var attrName = attr.name; // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (me.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};
// 指令處理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ...省略
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化視圖
updaterFn && updaterFn(node, vm[exp]);
// 實例化訂閱者沉删,此操作會在對應(yīng)的屬性消息訂閱器中添加了該訂閱者watcher
new Watcher(vm, exp, function(value, oldValue) {
// 一旦屬性值有變化,會收到通知執(zhí)行此更新函數(shù)醉途,更新視圖
updaterFn && updaterFn(node, value, oldValue);
});
}
};
// 更新函數(shù)
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
// ...省略
};
這里通過遞歸遍歷保證了每個節(jié)點及子節(jié)點都會解析編譯到矾瑰,包括了{{}}
表達(dá)式聲明的文本節(jié)點。指令的聲明規(guī)定是通過特定前綴的節(jié)點屬性來標(biāo)記隘擎,如<span v-text="content" other-attr
中v-text
便是指令殴穴,而other-attr
不是指令,只是普通的屬性嵌屎。
監(jiān)聽數(shù)據(jù)推正、綁定更新函數(shù)的處理是在compileUtil.bind()
這個方法中,通過new Watcher()
添加回調(diào)來接收數(shù)據(jù)變化的通知
至此宝惰,一個簡單的Compile就完成了植榕,完整代碼。接下來要看看Watcher這個訂閱者的具體實現(xiàn)了
?
| 實現(xiàn)Watcher
Watcher訂閱者作為Observer和Compile之間通信的橋梁尼夺,主要做的事情是:
1尊残、在自身實例化時往屬性訂閱器(dep
)里面添加自己
2、自身必須有一個update()
方法
3淤堵、待屬性變動dep.notice()
通知時寝衫,能調(diào)用自身的update()
方法,并觸發(fā)Compile
中綁定的回調(diào)拐邪,則功成身退慰毅。
如果有點亂,可以回顧下前面的思路整理
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 此處為了觸發(fā)屬性的getter扎阶,從而在dep添加自己汹胃,結(jié)合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 屬性值變化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 執(zhí)行Compile中綁定的回調(diào)婶芭,更新視圖
}
},
get: function() {
Dep.target = this; // 將當(dāng)前訂閱者指向自己
var value = this.vm[exp]; // 觸發(fā)getter,添加自己到屬性訂閱器中
Dep.target = null; // 添加完畢着饥,重置
return value;
}
};
// 這里再次列出Observer和Dep犀农,方便理解
Object.defineProperty(data, key, {
get: function() {
// 由于需要在閉包內(nèi)添加watcher,所以可以在Dep定義一個全局target屬性宰掉,暫存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // 調(diào)用訂閱者的update方法呵哨,通知變化
});
}
};
實例化Watcher
的時候,調(diào)用get()
方法轨奄,通過Dep.target = watcherInstance
標(biāo)記訂閱者是當(dāng)前watcher實例孟害,強行觸發(fā)屬性定義的getter
方法,getter
方法執(zhí)行的時候戚绕,就會在屬性的訂閱器dep
添加當(dāng)前watcher實例纹坐,從而在屬性值有變化的時候枝冀,watcherInstance就能收到更新通知舞丛。
ok, Watcher也已經(jīng)實現(xiàn)了,完整代碼果漾。
基本上vue中數(shù)據(jù)綁定相關(guān)比較核心的幾個模塊也是這幾個球切,猛戳這里 , 在src
目錄可找到vue源碼。
最后來講講MVVM入口文件的相關(guān)邏輯和實現(xiàn)吧绒障,相對就比較簡單了~
?
| 實現(xiàn) MVVM
MVVM作為數(shù)據(jù)綁定的入口吨凑,整合Observer、Compile和Watcher三者户辱,通過Observer來監(jiān)聽自己的model數(shù)據(jù)變化鸵钝,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁庐镐,達(dá)到數(shù)據(jù)變化 -> 視圖更新恩商;視圖交互變化(input) -> 數(shù)據(jù)model變更的雙向綁定效果。
一個簡單的MVVM構(gòu)造器是這樣子:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
但是這里有個問題必逆,從代碼中可看出監(jiān)聽的數(shù)據(jù)對象是options.data
怠堪,每次需要更新視圖,則必須通過var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq';
這樣的方式來改變數(shù)據(jù)名眉。
顯然不符合我們一開始的期望粟矿,我們所期望的調(diào)用方式應(yīng)該是這樣的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';
所以這里需要給MVVM實例添加一個屬性代理的方法,使訪問vm的屬性代理為訪問vm._data的屬性损拢,改造后的代碼如下:
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
// 屬性代理陌粹,實現(xiàn) vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};
這里主要還是利用了Object.defineProperty()
這個方法來劫持了vm實例對象的屬性的讀寫權(quán),使讀寫vm實例的屬性轉(zhuǎn)成讀寫了vm._data
的屬性值福压,達(dá)到魚目混珠的效果