Vue 採用聲明式編程替代過去的類 Jquery 的命令式編程,而且可以偵測數據的變化,更新視圖。這使得咱們能夠只關注數據自己,而不用手動處理數據到視圖的渲染,避免了繁瑣的 DOM 操做,提升了開發效率。不過理解其工做原理一樣重要,這樣能夠迴避一些常見的問題,下面咱們來介紹一下 Vue 是如何偵測數據並響應視圖的。vue
Vue 數據響應核心就是使用了 Object.defineProperty
方法( IE9 + ) 。node
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 方法,並接收新值做爲其參數。web
這裏提一句,在 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 的值,賦值同理。dom
如今,咱們已經知道如何去檢測數據的變化,而且作出一些響應了。函數
vue 的響應式系統依賴於三個重要的類:Dep 類、Watcher 類、Observer 類。this
Dep 類做爲發佈者的角色,Watcher 類做爲訂閱者的角色,Observer 類則是鏈接發佈者和訂閱者的紐帶,決定訂閱和發佈的時機。spa
咱們先看下面的代碼,來對發佈者和訂閱者有個初步的瞭解。.net
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,兩個訂閱者 watcher一、watcher2。讓 watcher一、watcher2 都訂閱 dep,當 dep 發送通知時,watcher一、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