前言
Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)净蚤。比如我們修改了數(shù)據(jù)锥咸,那么依賴這些數(shù)據(jù)的視圖都會(huì)進(jìn)行更新,大大提高了我們的"搬磚"效率柬采,回想一下初學(xué) JS 的時(shí)候海量的 Dom操作.......欢唾,Vue 通過數(shù)據(jù)驅(qū)動(dòng)視圖,極大的將我們從繁瑣的DOM操作中解放出來粉捻。
如下圖礁遣,我們改變了 msg 的值,視圖也響應(yīng)式的進(jìn)行了更新
Vue 響應(yīng)式原理
我們先看 vue 官網(wǎng)的圖肩刃,其實(shí)不太清晰祟霍,我初看的時(shí)候也是一臉懵逼的.:
再看下面這張圖,響應(yīng)式原理涵蓋在里面了(圖片來源于網(wǎng)絡(luò)):
梳理一下流程:
- Vue 初始化 => 劫持 data 設(shè)置 get盈包、set (攔截?cái)?shù)據(jù)讀寫)
- Compile 解析模板 => 生成 watcher => 讀取 data沸呐,觸發(fā) get 方法 => Dep 收集依賴(watcher)
- 數(shù)據(jù)變化 => 觸發(fā) set方法 => 通知 Dep 中的所有 watcher => 視圖更新
對(duì)于 Observer、Dep 和 Watcher 這三大金剛 呢燥,我初學(xué)的時(shí)候也是傻傻的分不清楚很懵崭添,我的理解是:
Dep(dependence) 即依賴收集器,收集 Watcher 即觀察者叛氨。
Watcher 即觀察者呼渣,觀察數(shù)據(jù),數(shù)據(jù)變化時(shí)更新對(duì)應(yīng)的視圖(dom)寞埠。
Observer 即劫持者屁置,通過 Object.defineProperty() 給數(shù)據(jù)設(shè)置 get 和 set 方法:
- get: 當(dāng)某個(gè)地方用到數(shù)據(jù)時(shí),如下 h1畸裳、h2 標(biāo)簽都用到了 msg 數(shù)據(jù)缰犁,即觀察 msg 數(shù)據(jù) 的兩個(gè) watcher 將被放入 msg 數(shù)據(jù)的依賴收集器 Dep 中淳地。
data() {
return {
msg: 'hello vue',
}
},
<h1>{{msg}}</h1>
<h2>{{msg}}</h2>
- set:當(dāng) msg 數(shù)據(jù)改變的時(shí)候怖糊,遍歷 Dep 依賴收集器帅容,通知所有 Watcher 更新視圖,即更新 h1伍伤、h2 標(biāo)簽內(nèi)的文本內(nèi)容
實(shí)現(xiàn) Vue 的響應(yīng)式系統(tǒng)
通過上面分析并徘,可知每一個(gè)數(shù)據(jù)有一個(gè)依賴收集器 Dep,Dep 里面存放用到該數(shù)據(jù)的 Watcher扰魂,如下圖所示(圖片來源于網(wǎng)絡(luò)):
1. Dep
我們先實(shí)現(xiàn) Dep麦乞,Dep 我們可以用數(shù)組來模擬,它應(yīng)該有兩個(gè)方法:
- add劝评,收集 Watcher
- notify姐直,數(shù)據(jù)變化的時(shí)候通知 Watcher 更新視圖
# 依賴收集器
class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
# 添加觀察者
this.subs.push(watcher);
}
notify() {
# 通知每一個(gè)觀察者更新視圖
this.subs.forEach(watcher => watcher.update());
}
}
2. Watcher
Watcher 實(shí)現(xiàn)如下,其中 cb 是更新視圖的方法蒋畜,關(guān)鍵點(diǎn)在于 oldVal声畏,它有兩個(gè)用處:
- Dep 觸發(fā) update 方法時(shí),比對(duì)新舊值姻成,若有變化才更新插龄,避免不必要的視圖更新
- 初始化的時(shí)候,會(huì)獲取舊值科展,會(huì)觸發(fā)數(shù)據(jù)的 get 方法均牢,在此時(shí)可以把依賴注入到 Dep 中(即依賴收集)
# 觀察者,用于更新視圖
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
# 視圖更新函數(shù)
this.cb = cb;
# 舊值
this.oldVal = this.getOldVal();
}
getOldVal() {
# 傳遞watch自己
Dep.target = this;
# 獲取值的時(shí)候會(huì)觸發(fā) get 方法才睹,把自己 push 進(jìn) deps[] 里
const oldVal = compileUtils.getVal(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
# 獲取新值
const newVal = compileUtils.getVal(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
Dep.target = this
的用處是相當(dāng)于設(shè)置了一個(gè)全局變量讓 Dep 能收集到 watcher 自己徘跪,后面 Dep.target = null
用處是銷毀全局變量
3. Observer
Observer 實(shí)現(xiàn)如下,通過 Object.defineProperty 攔截?cái)?shù)據(jù)的讀寫操作:
- get 收集依賴琅攘,注意判斷 Dep.target 是否有值真椿,因?yàn)槟0褰馕龅臅r(shí)候也會(huì)讀取數(shù)據(jù)觸發(fā) get 方法
- set 通知依賴收集器,更新視圖
// 數(shù)據(jù)劫持
class Observer {
constructor(data) {
this.observer(data, key, data[key]);
}
observer(obj, key, value) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
# 防止視圖初始化的時(shí)候也被收集到Dep中
Dep.target && dep.addSub(Dep.target);
return value;
},
set: newVal => {
this.observer(newVal);
if (newVal !== value) {
value = newVal;
# 通知依賴收集器乎澄,有變化
dep.notify();
}
},
});
}
}
4. Compile
到這里我們已經(jīng)實(shí)現(xiàn)了 Observer突硝、Dep 和 Watcher,實(shí)現(xiàn)了數(shù)據(jù)的響應(yīng)式追蹤置济,可是還有一個(gè)點(diǎn)沒打通解恰,那就是依賴收集,那么依賴什么時(shí)候收集呢浙于?換言之我們?cè)趺粗滥男?shù)據(jù)依賴了哪些視圖呢护盈?
在 Vue 解析模板的時(shí)候,實(shí)際上我們已經(jīng)知道了哪些 Dom 依賴了哪些數(shù)據(jù)羞酗,所以是在 compile 的時(shí)候完成了模板解析并完成了依賴收集腐宋。
Compile 實(shí)現(xiàn)如下,省略大部分 dom 操作相關(guān)代碼,可以用 DocumentFragment 文檔碎片提升性能胸竞,邏輯比較簡(jiǎn)單欺嗤,我們?cè)?dom 解析數(shù)據(jù)的時(shí)候生成了對(duì)應(yīng)的 watcher,并完成了依賴收集:
# 編譯類卫枝,輸出真實(shí)Dom
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
# 獲取文檔對(duì)象
const fragment = this.nodeFragment(this.el);
# 編譯
this.compile(fragment);
# 掛載回app
this.el.appendChild(fragment);
}
# 是否元素節(jié)點(diǎn)
isElementNode(node) {
return node.nodeType === 1;
}
# 獲取文檔碎片
nodeFragment(el) {
# do something
}
compile(fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach(node => {
if (this.isElementNode(node)) {
# 元素節(jié)點(diǎn)
# do something
} else {
# 文本節(jié)點(diǎn)
# do something
}
})
}
}
# 根據(jù)不同指令 執(zhí)行不同的編譯操作
const compileUtils = {
# v-text
text(node, expr, vm) {
const value = vm.$data[expr];
# 創(chuàng)建觀察者 完成依賴收集
new Watcher(vm, expr, newVal => {
node.textContent = value;
});
node.textContent = value;
},
};
至此一個(gè)響應(yīng)式的系統(tǒng)就已經(jīng)完了
雙向數(shù)據(jù)綁定
什么是雙向數(shù)據(jù)綁定
上面我們實(shí)現(xiàn)了響應(yīng)式的系統(tǒng)煎饼,但只是單向的,即數(shù)據(jù)驅(qū)動(dòng)視圖校赤,什么是雙向數(shù)據(jù)綁定呢吆玖?如下圖:
我們常見的 v-model, 就是雙向數(shù)據(jù)綁定马篮,其實(shí)它是一個(gè)語法糖:
<input v-model="msg" />
等價(jià)于 =>
<input :value="msg" @input="msg = $event.target.value" />
實(shí)現(xiàn)
雙向數(shù)據(jù)綁定即:
- 數(shù)據(jù)改變 => 視圖更新
- 視圖改變 => 數(shù)據(jù)改變 => 視圖更新
比如最簡(jiǎn)單的 input沾乘,我們只需要監(jiān)聽 input 事件,文本發(fā)生變化時(shí)更新數(shù)據(jù)浑测,觸發(fā)數(shù)據(jù)的 set 方法意鲸,通知所有的 watcher 更新視圖
我們?cè)谀0寰幾g的時(shí)候,給 dom 元素綁定相應(yīng)的事件尽爆,如 input 標(biāo)簽綁定 input 事件并指定更新數(shù)據(jù)的回調(diào)函數(shù):
const compileUtils = {
# v-model
model(node, expr, vm) {
const value = vm.$data[expr];
# 創(chuàng)建觀察者 完成依賴收集
new Watcher(vm, expr, newVal => {
node.value = value;
});
node.addEventListener('input', (e) => {
# 更新數(shù)據(jù)怎顾,觸發(fā)數(shù)據(jù)的 set 方法
vm.$data[expr] = newVal;
});
node.value = value;
},
};
至此大功告成
源碼
END