實現(xiàn) Vue 的響應式系統(tǒng)
作者:小豪看世界
來源:SegmentFault 思否
前言
Vue 最獨特的特性之一,是其非侵入性的響應式系統(tǒng)。比如我們修改了數(shù)據(jù),那么依賴這些數(shù)據(jù)的視圖都會進行更新,大大提高了我們的"搬磚"效率,回想一下初學 JS 的時候海量的 Dom操作~.~......,Vue 通過數(shù)據(jù)驅(qū)動視圖,極大的將我們從繁瑣的DOM操作中解放出來。
如下圖,我們改變了 msg 的值,視圖也響應式的進行了更新

Vue 響應式原理
我們先看 vue 官網(wǎng)的圖,其實不太清晰,我初看的時候也是一臉懵逼的~.~:

再看下面這張圖,響應式原理涵蓋在里面了(圖片來源于網(wǎng)絡):

梳理一下流程:
Vue 初始化 => 劫持 data 設(shè)置 get、set (攔截數(shù)據(jù)讀寫) Compile 解析模板 => 生成 watcher => 讀取 data,觸發(fā) get 方法 => Dep 收集依賴(watcher) 數(shù)據(jù)變化 => 觸發(fā) set方法 => 通知 Dep 中的所有 watcher => 視圖更新
對于 Observer、Dep 和 Watcher 這三大金剛 ,我初學的時候也是傻傻的分不清楚很懵,我的理解是:
Dep(dependence)?即依賴收集器,收集 Watcher 即觀察者。
Watcher 即觀察者,觀察數(shù)據(jù),數(shù)據(jù)變化時更新對應的視圖(dom)。
Observer 即劫持者,通過 Object.defineProperty()?給數(shù)據(jù)設(shè)置 get 和 set 方法:
get: 當某個地方用到數(shù)據(jù)時,如下 h1、h2 標簽都用到了 msg 數(shù)據(jù),即觀察 msg 數(shù)據(jù) 的兩個 watcher 將被放入 msg 數(shù)據(jù)的依賴收集器 Dep 中。
data() {return {msg: 'hello vue',}},{{msg}}
{{msg}}
set:當 msg 數(shù)據(jù)改變的時候,遍歷 Dep 依賴收集器,通知所有 Watcher 更新視圖,即更新 h1、h2 標簽內(nèi)的文本內(nèi)容
實現(xiàn) Vue 的響應式系統(tǒng)
通過上面分析,可知每一個數(shù)據(jù)有一個依賴收集器 Dep,Dep 里面存放用到該數(shù)據(jù)的 Watcher,如下圖所示(圖片來源于網(wǎng)絡):

1. Dep
我們先實現(xiàn) Dep,Dep 我們可以用數(shù)組來模擬,它應該有兩個方法:
add,收集 Watcher notify,數(shù)據(jù)變化的時候通知 Watcher 更新視圖
# 依賴收集器class Dep {constructor() {this.subs = [];}addSub(watcher) {# 添加觀察者this.subs.push(watcher);}notify() {# 通知每一個觀察者更新視圖this.subs.forEach(watcher => watcher.update());}}
2. Watcher
Watcher 實現(xiàn)如下,其中 cb 是更新視圖的方法,關(guān)鍵點在于 oldVal,它有兩個用處:
Dep 觸發(fā) update 方法時,比對新舊值,若有變化才更新,避免不必要的視圖更新 初始化的時候,會獲取舊值,會觸發(fā)數(shù)據(jù)的 get 方法,在此時可以把依賴注入到 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;# 獲取值的時候會觸發(fā) get 方法,把自己 push 進 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?的用處是相當于設(shè)置了一個全局變量讓 Dep 能收集到 watcher 自己,后面?Dep.target = null?用處是銷毀全局變量
3. Observer
Observer 實現(xiàn)如下,通過 Object.defineProperty 攔截數(shù)據(jù)的讀寫操作:
get 收集依賴,注意判斷 Dep.target 是否有值,因為模板解析的時候也會讀取數(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() {# 防止視圖初始化的時候也被收集到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)實現(xiàn)了 Observer、Dep 和 Watcher,實現(xiàn)了數(shù)據(jù)的響應式追蹤,可是還有一個點沒打通,那就是依賴收集,那么依賴什么時候收集呢?換言之我們怎么知道哪些數(shù)據(jù)依賴了哪些視圖呢?
在 Vue 解析模板的時候,實際上我們已經(jīng)知道了哪些 Dom 依賴了哪些數(shù)據(jù),所以是在 compile 的時候完成了模板解析并完成了依賴收集。
Compile 實現(xiàn)如下,省略大部分 dom 操作相關(guān)代碼,可以用 DocumentFragment 文檔碎片提升性能,邏輯比較簡單,我們在 dom 解析數(shù)據(jù)的時候生成了對應的 watcher,并完成了依賴收集:
# 編譯類,輸出真實Domclass Compile {constructor(el, vm) {this.el = this.isElementNode(el) ? el : document.querySelector(el);this.vm = vm;# 獲取文檔對象const fragment = this.nodeFragment(this.el);# 編譯this.compile(fragment);# 掛載回appthis.el.appendChild(fragment);}# 是否元素節(jié)點isElementNode(node) {return node.nodeType === 1;}# 獲取文檔碎片nodeFragment(el) {# do something}compile(fragment) {const childNodes = fragment.childNodes;[...childNodes].forEach(node => {if (this.isElementNode(node)) {# 元素節(jié)點# do something} else {# 文本節(jié)點# do something}})}}# 根據(jù)不同指令 執(zhí)行不同的編譯操作const compileUtils = {# v-texttext(node, expr, vm) {const value = vm.$data[expr];# 創(chuàng)建觀察者 完成依賴收集new Watcher(vm, expr, newVal => {node.textContent = value;});node.textContent = value;},};
至此一個響應式的系統(tǒng)就已經(jīng)完了
雙向數(shù)據(jù)綁定
什么是雙向數(shù)據(jù)綁定
上面我們實現(xiàn)了響應式的系統(tǒng),但只是單向的,即數(shù)據(jù)驅(qū)動視圖,什么是雙向數(shù)據(jù)綁定呢?如下圖:

我們常見的 v-model, 就是雙向數(shù)據(jù)綁定,其實它是一個語法糖:
等價于 =>
實現(xiàn)
雙向數(shù)據(jù)綁定即:
數(shù)據(jù)改變 => 視圖更新 視圖改變 => 數(shù)據(jù)改變 => 視圖更新

比如最簡單的 input,我們只需要監(jiān)聽 input 事件,文本發(fā)生變化時更新數(shù)據(jù),觸發(fā)數(shù)據(jù)的 set 方法,通知所有的 watcher 更新視圖
我們在模板編譯的時候,給 dom 元素綁定相應的事件,如 input 標簽綁定 input 事件并指定更新數(shù)據(jù)的回調(diào)函數(shù):
const compileUtils = {# v-modelmodel(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;},};
至此大功告成

