vue中的數(shù)據(jù)雙向綁定,其實(shí)一句話就可以說(shuō)清楚了:利用 Object.defineProperty(),來(lái)劫持各個(gè)屬性的setter/getter,并且把內(nèi)部解耦為 Observer, Dep, 并使用 Watcher 相連。
那根據(jù)這句話我們可以把整一個(gè)簡(jiǎn)單的MVVM框架粗分為以下四個(gè)模塊:
1.模板編譯(Compile)
2.數(shù)據(jù)劫持(Observer)
3.訂閱發(fā)布(Dep)
4.觀察者(Watcher)
我們就根據(jù)這四個(gè)模塊來(lái)分析、手寫一個(gè)MVVM框架。
想看源碼的,請(qǐng)直接下滑到最后。
MVVM類
和Vue類似,我們構(gòu)建一個(gè)MVVM類,通過(guò)new指令創(chuàng)建一個(gè)MVVM實(shí)例,并傳入一個(gè)類型為對(duì)象的參數(shù)option,包含當(dāng)前實(shí)例的作用域el和模板綁定的數(shù)據(jù)data。
class MVVM {
constructor(options) {
// 掛載實(shí)例
this.$el = options.el;
this.$data = options.data;
// 編譯模板
if(this.$el) {
// 數(shù)據(jù)劫持 把對(duì)象的所有屬性 改成帶set 和 get 方法的
new Observer(this.$data)
// 將數(shù)據(jù)代理到實(shí)例上,直接操作實(shí)例即可,不需要通過(guò)vm.$data來(lái)進(jìn)行操作
this.proxyData(this.$data)
// 用數(shù)據(jù)和元素進(jìn)行編譯
new Compile(this.$el, this)
}
}
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newValue) {
data[key] = newValue
}
})
})
}
}
MVVM類整合了所有的模塊,作為連接Compile和Observer的橋梁。
模板編譯(Compile)
Compile
compile在編譯模板的時(shí)候,其實(shí)是從指令和文本兩個(gè)方面來(lái)處理的。
class Compile {
constructor(el, vm) {
// 判斷是否為DOM,若不是,自己獲取
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 1. 將真實(shí)DOM放進(jìn)內(nèi)存中
let fragment = this.node2fragment(this.el);
// 2. 開(kāi)始編譯 提取想要的元素節(jié)點(diǎn) v-model 和 文本節(jié)點(diǎn) {{}}
this.compile(fragment);
// 3. 將編譯好的 fragment 重新放回頁(yè)面
this.el.appendChild(fragment);
}
}
/**
* 輔助方法
* 是否為元素節(jié)點(diǎn)
* @isElementNode
* 是否為指令
* @isDirective
*/
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) {
return name.includes("v-");
}
/**
* 核心方法
*/
compileElement(node) {
// v-model v-text
let attrs = node.attributes; // 取出當(dāng)前節(jié)點(diǎn)的屬性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 判斷屬性名是否包含 v-model
// 取到對(duì)應(yīng)的值,放到節(jié)點(diǎn)中
let expr = attr.value;
let [, type] = attrName.split("-"); //解構(gòu)賦值v-model-->model
// 調(diào)用對(duì)應(yīng)的編譯方法, 編譯哪個(gè)節(jié)點(diǎn),用數(shù)據(jù)替換掉表達(dá)式
CompileUtil[type](node, this.vm, expr);
}
});
}
compileText(node) {
let expr = node.textContent; // 取出文本中的內(nèi)容
let reg = /\{\{([^]+)\}\}/g; // {{a}} {} {{c}}
if (reg.test(expr)) {
// 調(diào)用編譯文本的方法,編輯哪個(gè)節(jié)點(diǎn),用數(shù)據(jù)替換掉表達(dá)式
CompileUtil["text"](node, this.vm, expr);
}
}
// 遞歸
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 如果是元素的節(jié)點(diǎn),則繼續(xù)深入檢查
// 編譯元素
this.compileElement(node);
this.compile(node);
} else {
// 文本節(jié)點(diǎn)
// 編譯文本
this.compileText(node);
}
});
// Array.from()方法是將一個(gè)類數(shù)組對(duì)象或者可遍歷對(duì)象轉(zhuǎn)換成一個(gè)真正的數(shù)組
}
// 將el中的內(nèi)容全部放進(jìn)內(nèi)存中
node2fragment(el) {
// 文檔碎片 內(nèi)存中的 dom 節(jié)點(diǎn)
let fragment = document.createDocumentFragment();
let firstChild;
// 把值賦給變量 取不到后返回null,null作為條件
while ((firstChild = el.firstChild)) {
// 使用appendChild() 方法從一個(gè)元素向另一個(gè)元素中移動(dòng)
fragment.appendChild(firstChild);
}
return fragment; // 內(nèi)存中的節(jié)點(diǎn)
}
}
CompileUtil
CompileUtil是一個(gè)對(duì)象工具,配合Copmpile使用。
let CompileUtil = {
model(node, vm, expr) {
let updateFn = this.updater["modelUpdater"];
/**
*
* 這里應(yīng)該加一個(gè)監(jiān)控,數(shù)據(jù)變化了 應(yīng)該調(diào)用watch的callback
* (這里只是記錄原始的值 watcher的update沒(méi)有執(zhí)行,只有屬性的set執(zhí)行的時(shí)候,才會(huì)執(zhí)行cb回調(diào),重新進(jìn)行真實(shí)數(shù)據(jù)綁定)
*
*/
new Watcher(vm, expr, newValue => {
// 當(dāng)值變化后會(huì)調(diào)用cb 將新的值傳遞過(guò)來(lái)
updateFn && updateFn(node, this.getVal(vm, expr));
});
node.addEventListener("input", e => {
let newValue = e.target.value;
//監(jiān)聽(tīng)輸入事件,將輸入的內(nèi)容設(shè)置到對(duì)應(yīng)數(shù)據(jù)上
this.setVal(vm, expr, newValue);
});
updateFn && updateFn(node, this.getVal(vm, expr));
},
text(node, vm, expr) {
// 文本處理
let updateFn = this.updater["textUpdater"];
let value = this.getTextVal(vm, expr);
expr.replace(/\{\{((?:.|\r?\n)+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], newValue => {
// 如果數(shù)據(jù)變化了,文本節(jié)點(diǎn)需要重新獲取依賴的屬性,更新文本中的內(nèi)容
updateFn && updateFn(node, this.getTextVal(vm, expr));
});
});
updateFn && updateFn(node, value);
},
getTextVal(vm, expr) {
// 獲取編譯文本后的結(jié)果
let value = this.parseText(expr);
let result = '';
value.tokens.forEach((item) => {
if(item.hasOwnProperty('@binding')) {
result += this.getVal(vm, item['@binding'])
} else {
result += item
}
})
return result
},
parseText(text) {
const tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
if (!tagRE.test(text)) {
return;
}
const tokens = [];
const rawTokens = [];
let lastIndex = (tagRE.lastIndex = 0);
let match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) {
rawTokens.push((tokenValue = text.slice(lastIndex, index)));
tokens.push(JSON.stringify(tokenValue));
}
// tag token
const exp = match[1].trim();
tokens.push(`_s(${exp})`);
rawTokens.push({ "@binding": exp });
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join("+"),
tokens: rawTokens
};
},
setVal(vm, expr, value) {
expr = expr.split(".");
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return (prev[next] = value);
}
return prev[next];
}, vm.$data);
},
getVal(vm, expr) {
// 獲取實(shí)例上對(duì)應(yīng)的數(shù)據(jù)
expr = expr.split("."); // {{message.a}} [message, a]
// vm.$data.message => vm.$data.message.a
return expr.reduce((prev, next) => {
return prev[next.trim()];
}, vm.$data);
/**
* 關(guān)于 reduce:
* arr.reduce(callback,[initialValue])
*/
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
}
};
再次認(rèn)識(shí)到正則表達(dá)式的重要性。
在處理{{}}模板引擎的時(shí)候,遇到一個(gè)bug,在一個(gè)DOM節(jié)點(diǎn)里,如果有個(gè)有多個(gè){{}}{{}}會(huì)顯示為undefined,后來(lái)仔細(xì)閱讀了vueJs的源碼,借鑒其中parseText()方法,進(jìn)行處理,得以解決。
數(shù)據(jù)劫持(Observer)
什么是數(shù)據(jù)劫持?
在訪問(wèn)或修改對(duì)象的某個(gè)屬性時(shí),通過(guò)一段代碼攔截這個(gè)行為,進(jìn)行額外的操作,或者修改返回的結(jié)果。
數(shù)據(jù)劫持的作用是什么?
它是雙向數(shù)據(jù)綁定的核心方法,通過(guò)劫持對(duì)象屬性的setter和getter操作,監(jiān)聽(tīng)數(shù)據(jù)的變化,同時(shí)也是后期ES6中很多語(yǔ)法糖底層實(shí)現(xiàn)的核心方法。
使用Object.defineProperty()做數(shù)據(jù)劫持,有什么弊端?
1、不能監(jiān)聽(tīng)數(shù)組的變化
2、必須遍歷對(duì)象的每個(gè)屬性
3、必須深層遍歷嵌套的對(duì)象
MVVM中的數(shù)據(jù)劫持
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 要對(duì)這個(gè)data數(shù)據(jù),將原有的屬性改成set和get的形式
// defineProperty針對(duì)的是對(duì)象
if(!data || typeof data !== 'object') {
return
}
// 將數(shù)據(jù)一一劫持,先獲取到data的key和value
Object.keys(data).forEach(key => {
// 定義響應(yīng)式變化
this.defineReactive(data, key, data[key])
this.observe(data[key]) //深度遞歸劫持
})
// 關(guān)于Object.keys() 返回一個(gè)包含對(duì)象的屬性名稱的數(shù)組
}
// 定義響應(yīng)式
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep(); // 每個(gè)變化的數(shù)據(jù) 都會(huì)對(duì)應(yīng)一個(gè)數(shù)組,這個(gè)數(shù)組是存放所有更新的操作
Object.defineProperty(obj, key, {
enumerable: true, // 是否能在for...in循環(huán)中遍歷出來(lái)或在Object.keys中列舉出來(lái)
configurable: true, // false,不可修改、刪除目標(biāo)屬性或修改屬性性以下特性
get() {
Dep.target && dep.addSub(Dep.target)
return value;
},
set(newValue) {
if(newValue != value) {
that.observe(newValue); // 如果設(shè)置的是對(duì)象,繼續(xù)劫持
value = newValue;
dep.notify(); //通知所有人 數(shù)據(jù)更新了
}
}
})
}
}
訂閱發(fā)布(Dep)
其實(shí)發(fā)布訂閱說(shuō)白了就是把要執(zhí)行的函數(shù)統(tǒng)一存儲(chǔ)在一個(gè)數(shù)組subs中管理,當(dāng)達(dá)到某個(gè)執(zhí)行條件時(shí),循環(huán)這個(gè)數(shù)組并執(zhí)行每一個(gè)成員。
class Dep {
constructor() {
// 訂閱數(shù)組
this.subs = [];
}
// 添加訂閱
addSub(watcher) {
this.subs.push(watcher);
}
// 將消息通知給所有人
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
觀察者(Watcher)
Watcher 類的作用是,獲取更改前的值存儲(chǔ)起來(lái),并創(chuàng)建一個(gè) update 實(shí)例方法,當(dāng)值被更改時(shí),執(zhí)行實(shí)例的 callback 以達(dá)到視圖的更新。
class Watcher{ // 因?yàn)橐@取 oldValue,所以需要“數(shù)據(jù)”和“表達(dá)式”
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先獲取 oldValue 保存下來(lái)
this.value = this.get();
}
getVal(vm, expr) {
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next.trim()]
}, vm.$data);
}
get() {
// 在取值之前先將 watcher 保存到 Dep 上
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value;
}
// 對(duì)外暴露的方法,如果值改變就可以調(diào)用這個(gè)方法來(lái)更新
update() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if (newValue != oldValue) {
this.cb(newValue);
}
}
}
最后
最后當(dāng)然是要檢測(cè)一下,我們的寫的代碼是不是能正常運(yùn)行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 雙向數(shù)據(jù)綁定 靠的是表單 -->
<input type="text" v-model="message.a" />
<div>{{ message.a }} 啦啦啦</div>
{{ message.a }}
{{ b }}
</div>
</body>
</html>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="dep.js"></script>
<script src="mvvm.js"></script>
<script>
let vm = new MVVM({
el: "#app",
data: {
message: { a: "wlf" },
b: "biubiubiu"
}
});
</script>
總結(jié)
我們根據(jù)下圖(參考《深入淺出vue.js》),將整個(gè)流程再梳理一遍:

在 new MVVM() 后, MVVM 會(huì)進(jìn)行初始化即實(shí)例化MVVM,在這個(gè)過(guò)程中,模板綁定的數(shù)據(jù)data通過(guò)Observer數(shù)據(jù)劫持,轉(zhuǎn)換成了getter/setter的形式,來(lái)監(jiān)聽(tīng)數(shù)據(jù)的變化,當(dāng)被設(shè)置的對(duì)象被讀取的時(shí)候會(huì)執(zhí)行getter函數(shù),當(dāng)它被賦值的時(shí)候會(huì)執(zhí)行setter函數(shù)。
當(dāng)頁(yè)面渲染的時(shí)候,會(huì)讀取所需對(duì)象的值,這個(gè)時(shí)候會(huì)觸發(fā)getter函數(shù)從而將Watcher添加到Dep中進(jìn)行依賴收集,添加訂閱。
當(dāng)對(duì)象的值發(fā)生變化時(shí),會(huì)觸發(fā)對(duì)應(yīng)的setter函數(shù),setter會(huì)調(diào)用dep.notify()通知之前依賴收集得到的 Dep 中的每一個(gè) Watcher,也就是遍歷subs這個(gè)數(shù)組,告訴它們自己的值改變了,需要重新渲染視圖。這時(shí)候這些 Watcher就會(huì)開(kāi)始調(diào)用 update() 來(lái)更新視圖。