Vue 采用聲明式編程替代過去的類 Jquery 的命令式編程,並且能夠偵測數據的變化,更新視圖。這使得我們可以只關注數據本身,而不用手動處理數據到視圖的渲染,避免了繁瑣的 DOM 操作,提高了開發效率。不過理解其工作原理同樣重要,這樣可以回避一些常見的問題,下面我們來介紹一下 Vue 是如何偵測數據並響應視圖的。
Object.defineProperty
Vue 數據響應核心就是使用了 Object.defineProperty
方法( IE9 + ) 。
var obj = {}; Object.defineProperty(obj, 'msg', { get () { console.log('get'); }, set (newVal) { console.log('set', newVal); } }); obj.msg // get obj.msg = 'hello world' // set hello world
取 obj 對象中 msg 的值時會調用 get 方法,給 msg 賦值時會調用 set 方法,並接收新值作為其參數。
這里提一句,在 Vue 中我們調用數據是直接 this.xxx ,而數據其實是 this.data.xxx,原來 Vue 在初始化數據的時候會遍歷 data 並代理這些數據。
Object.keys(this.data).forEach((key) => { this.proxyKeys(key); }); proxyKeys (key) { Object.defineProperty(this, key, { enumerable: false, configurable: true, get() { return this.data[key]; }, set(newVal) { this.data[key] = newVal; } }); }
上面可以看到,取 this.key 的值其實是取 this.data.key 的值,賦值同理。
現在,我們已經知道如何去檢測數據的變化,並且做出一些響應了。
觀察者模式 ( 發布者-訂閱者模式 )
vue 的響應式系統依賴於三個重要的類:Dep 類、Watcher 類、Observer 類。
Dep 類作為發布者的角色,Watcher 類作為訂閱者的角色,Observer 類則是連接發布者和訂閱者的紐帶,決定訂閱和發布的時機。
我們先看下面的代碼,來對發布者和訂閱者有個初步的了解。
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach(watcher => { watcher.update(); }); } } class Watcher { constructor() { } update() { // 接收通知后的處理方法 } } const dep = new Dep(); // 發布者 dep const watcher1 = new Watcher(); // 訂閱者1 watcher1 const watcher2 = new Watcher(); // 訂閱者2 watcher2 dep.addSub(watcher1); // watcher1 訂閱 dep dep.addSub(watcher2); // watcher2 訂閱 dep dep.notify(); // dep 發送通知
上面我們定義了一個發布者 dep,兩個訂閱者 watcher1、watcher2。讓 watcher1、watcher2 都訂閱 dep,當 dep 發送通知時,watcher1、watcher2 都能做出各自的響應。
現在我們已經了解了發布者和訂閱者的關系,那么剩下的就是訂閱和發布的時機。什么時候訂閱?什么時候發布?想到上面提到的 Object.defineProperty ,想必你已經有了答案。
我們來看 Observer 類的實現:
class Observer { constructor(data) { this.data = data; this.walk(); } walk() { Object.keys(this.data).forEach(key => { this.defineReactive(this.data, key, this.data[key]); }); } defineReactive(data, key, value) { const dep = new Dep(); if ( value && typeof value === 'object' ) { new Observer(value); } Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { if (Dep.target) { dep.addSub(Dep.target); // 訂閱者訂閱 Dep.target 即當前 Watcher 類的實例(訂閱者) } return value; }, set(newVal) { if (newVal === value) { return false; } value = newVal; dep.notify(); // 發布者發送通知 } }); } }
在 Observer 類中,為 data 的每個屬性都實例化一個 Dep 類,即發布者。並且在取值時讓訂閱者(有多個,因為 data 中的每個屬性都可以被應用在多個地方)訂閱,在賦值時發布者發布通知,讓訂閱者做出各自的響應。
這里需要提的是 Dep.target,這其實是 Watcher 類的實例,我們可以看看 Watcher 的詳細代碼:
class Watcher { constructor(vm, exp, cb) { this.vm = vm; this.exp = exp; // data 屬性名 this.cb = cb; // 回調函數 // 將自己添加到訂閱器 this.value = this.getValue(); } update() { const value = this.vm.data[this.exp]; const oldValue = this.value; if (value !== oldValue) { this.value = value; this.cb.call(this.vm, value, oldValue); // 執行回調函數 } } getValue() { Dep.target = this; // 將自己賦值給 Dep.target const value = this.vm.data[this.exp]; // 取值操作觸發訂閱者訂閱 Dep.target = null; return value; } }
Watcher 類在構造函數中執行了一個 getValue 方法,將自己賦值給 Dep.target ,並且執行了取值操作,這樣就成功的完成了訂閱操作。一旦數據發生變化,即有了賦值操作,發布者就會發送通知,訂閱者就會執行自己的 update 方法來響應這次數據變化。
數據的雙向綁定
數據的雙向綁定即數據和視圖之間的同步,視圖隨着數據變化而變化,反之亦然。我們知道 Vue 是支持數據的雙向綁定的,主要應用於表單,是通過 v-model 指令來實現的。而通過上面介紹的知識我們是可以知道如何實現視圖隨着數據變化的,那么如何讓數據也隨着視圖變化而變化呢?其實也很簡單,只要給有 v-model 指令的節點監聽相應的事件即可,在事件回調中來改變相應的數據。這一切都 Compile 類中完成,假設有一個 input 標簽應用了 v-model 指令,在開始編譯模板時,遇到 v-model 指令時會執行:更新 dom 節點的值,訂閱者訂閱,事件監聽。
compileModel (node, vm, exp) { let val = vm[exp]; // 更新內容 this.modelUpdater(node, val); // 添加訂閱 new Watcher(vm, exp, (value) => { // 數據改變時的回調函數 this.modelUpdater(node, value); }); // 事件監聽 node.addEventListener('input', (e) => { const newValue = e.target.value; if (val === newValue) { return false; } vm[exp] = newValue; val = newValue; }); }
當我們在文本框中輸入數據時,會給原有 data 中的某個屬性 a 賦值,這時候會觸發發布者發起通知,那么所有屬性 a 的訂閱者都能夠同步到最新的數據。
最后,附上一個小 demo
更多精彩內容,歡迎關注微信公眾號~