本文主要抽離Vue源碼中數據雙向綁定的核心代碼,解析Vue是如何實現數據的雙向綁定
核心思想是ES5的Object.defineProperty()和發佈-訂閱模式數組
全部,咱們從代碼的角度將總體分爲三個部分:監聽數據變化、管理訂閱者、訂閱者this
使用ES5中的Object.defineProperty將data中的屬性修改成訪問者屬性雙向綁定
// Dep用於訂閱者的存儲和收集,將在下面實現 import Dep from 'Dep' // Observer類用於給data屬性添加set&get方法 export default class Observer{ constructor(value){ this.value = value this.walk(value) } walk(value){ Object.keys(value).forEach(key => this.convert(key, value[key])) } convert(key, val){ defineReactive(this.value, key, val) } } export function defineReactive(obj, key, val){ // 用於存放某個屬性的全部訂閱者 var dep = new Dep() // 給當前屬性的值添加監聽 var chlidOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=> { console.log('get value') // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中 // target指向一個Watcher實例,每一個Watcher都是一個訂閱者 // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法 if(Dep.target){ dep.addSub(Dep.target) } return val }, set: (newVal) => { console.log('new value setted') if(val === newVal) return val = newVal // 對新值進行監聽 chlidOb = observe(newVal) // 通知全部訂閱者,數值被改變了 dep.notify() } }) } export function observe(value){ // 當值不存在,或者不是複雜數據類型時,再也不須要繼續深刻監聽 if(!value || typeof value !== 'object'){ return } return new Observer(value) }
對訂閱者進行收集,存儲和通知代理
export default class Dep{ constructor(){ this.subs = [] } addSub(sub){ // 在收集訂閱者的時候,須要對subs中的訂閱者進行去重,這邊不詳細解析 this.subs.push(sub) } notify(){ // 通知全部的訂閱者(Watcher),觸發訂閱者的相應邏輯處理 this.subs.forEach((sub) => sub.update()) } }
每一個watcher對象都是對data中每一個屬性的訂閱,是多對一的關係,每一個watcher只能對應一個data屬性,而一個data屬性能夠對應多個watchercode
import Dep from 'Dep' export default class Watcher{ constructor(vm, expOrFn, cb){ this.vm = vm // 被訂閱的數據必定來自於當前Vue實例 this.cb = cb // 當數據更新時想要作的事情 this.expOrFn = expOrFn // 被訂閱的數據 this.val = this.get() // 維護更新以前的數據 } // 對外暴露的接口,用於在訂閱的數據被更新時,由訂閱者管理員(Dep)調用 update(){ this.run() } run(){ const val = this.get() if(val !== this.val){ this.val = val; this.cb.call(this.vm) } } get(){ // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者 Dep.target = this const val = this.vm._data[this.expOrFn] // 置空,用於下一個Watcher使用 Dep.target = null return val; } }
下邊咱們建立一個簡易的Vue來實際運行下對數據的監聽server
import Observer, {observe} from 'Observer' import Watcher from 'Watcher' export default class Vue{ constructor(options = {}){ // 簡化了$options的處理 this.$options = options // 簡化了對data的處理 let data = this._data = this.$options.data // 將全部data最外層屬性代理到Vue實例上 Object.keys(data).forEach(key => this._proxy(key)) // 監聽數據 observe(data) } // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者 $watch(expOrFn, cb){ new Watcher(this, expOrFn, cb) } _proxy(key){ Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: (val) => { this._data[key] = val } }) } }
import Vue from './Vue'; let demo = new Vue({ data: { 'a': { 'ab': { 'c': 'C' } }, 'b': [ 'bb': 'BB', 'bbb': 'BBB' ], 'c': 'C' } }); demo.$watch('c', () => console.log('c is changed')); // get value demo.$watch('a.ab', () => console.log('a.ab is changed')); demo.$watch('b', () => console.log('b is changed')); // get value demo.c = 'CCC'; // new value setted // get value // c is changed demo.a.ab = 'AB'; // get value // new value setted demo.b.push({'bbbb': 'BBBB'}); // get value
根據實例的輸出結果,咱們很奇怪的發現,只有對簡單的數據監聽才能實現數據雙向綁定。對象
demo.$watch('a.ab', () => console.log('a.ab is changed'))
註冊訂閱者並無調用getter
demo.a.ab = 'AB'
有監聽到數據的變化,並無調用對應的callbackdemo.b.push({'bbbb': 'BBBB'})
對數值進行操做,並無調用對應的callback這是爲何呢?由於咱們對數據的監聽的實現,目前僅限於簡單對應,對於某個屬性內部有更多複雜屬性時,就無能爲力了。接口
爲了實現進一步對數據和複雜對象的監聽,請戳Vue源碼解析---數組的雙向綁定和Vue源碼解析---複雜隊形的雙向綁定get