vue實現數據響應式,是經過數據劫持偵測數據變化,發佈訂閱模式進行依賴收集與視圖更新,換句話說是Observe,Watcher以及Compile三者相互配合。javascript
變量
換成數據
,綁定更新函數,添加訂閱者,收到通知就執行更新函數實現響應式的第一步就是能偵測數r據的變化,在Vue2.x是經過ES5的方法Object.defineProperty()實現對象屬性的偵聽,在Vue3.x中使用了ES6提供的Proxy對對象進行代理。html
Object.definePropertyvue
function observe(obj) { if (!obj || typeof obj !== "object") { return; } Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); function defineReactive(obj, key, value) { //遞歸子屬性 observe(value); //訂閱器 const dp = new Dep(); Object.defineProperty(obj, key, { configurable: true, //可刪除 enumerable: true, //可枚舉遍歷 get: function () { /* 將Dep.target(即當前的Watcher對象存入dep的subs中) */ dp.addSub(Dep.target); return value; }, set: function (newValue) { //遞歸新的子屬性 observe(newValue); if (value !== newValue) { value = newValue; /* 在set的時候觸發dep的notify來通知全部的Watcher對象更新視圖 */ dp.notify(); } }, }); } }
Proxy實現代理java
let target = { name: " xiao" }; let handler = { get(target, key) { if (typeof target[key] === "object" && target[key] !== "null") { return new Proxy(target[key], handler); } return target[key]; }, set: function (target, key, value) { target[key] = value; }, }; target = new Proxy(target, handler);
//Dep訂閱者,依賴收集器 class Dep { constructor() { /* 用來存放Watcher對象的數組 */ this.subs = []; } /* 在subs中添加一個Watcher對象 */ addSub(sub) { this.subs.push(sub); } /* 在subs中添加一個Watcher對象 */ notify() { this.subs.forEach((sub) => { sub.update(); }); } } //用 addSub 方法能夠在目前的 Dep 對象中增長一個 Watcher 的訂閱操做; //用 notify 方法通知目前 Dep 對象的 subs 中的全部 Watcher 對象觸發更新操做。
class Watcher { constructor(obj, key, cb) { /* 在new一個Watcher對象時將該對象賦值給Dep.target,在observe get中會用到 */ Dep.target = this; this.obj = obj; this.key = key; this.cb = cb; //觸發getter,依賴收集 this.value = obj[key]; //收集完置空Dep.target,防止重複收集 Dep.target = null; } update() { //得到新值 this.value = obj[this.key]; console.log("視圖更新"); } }
//指令處理類 const compileUtile = { getVal(expr,vm){ //reduce用的好啊 return expr.split('.').reduce((data,curentval)=>{ return data[curentval]; },vm.$data) }, html(node,expr,vm){ new Watcher(vm,expr,(newVal)=>{ this.updater.htmlUpdate(node,newVal); }) const value = this.getVal(expr,vm); this.updater.htmlUpdate(node,value); }, //更新函數 updater:{ htmlUpdate(node,value){ node.innerHTML= value; }, } } //Compile指令解析器 class Compile{ //各類正則匹配vue指令和表達式,替換數據 }
Object.defineProperty與Proxy的區別?node
爲何要依賴收集?api
數據劫持的目的是在屬性變化的時候觸發視圖更新,依賴收集能夠收集到哪些地方使用到了相關屬性,屬性變化時,就能夠通知到全部的地方去更新視圖,對於沒有使用的屬性,也能夠避免無用的數據比對更新數組
Dep和Watcher的關係(多對多)app
watcher和Dep什麼時候建立ide
由於Object.defineProperty
不能監聽數組長度變化,因此Vue使用了函數劫持
的方式,重寫了數組的方法,Vue將data中的數組進行了原型鏈重寫,指向了本身定義的數組原型方法。這樣當調用數組api時,能夠通知依賴更新。若是數組中包含着引用類型,會對數組中的引用類型再次遞歸遍歷進行監控。這樣就實現了監測數組變化。函數
1 // src/core/observer/array.js 2 3 // 獲取數組的原型Array.prototype,上面有咱們經常使用的數組方法 4 const arrayProto = Array.prototype 5 // 建立一個空對象arrayMethods,並將arrayMethods的原型指向Array.prototype 6 export const arrayMethods = Object.create(arrayProto) 7 8 // 列出須要重寫的數組方法名 9 const methodsToPatch = [ 10 'push', 11 'pop', 12 'shift', 13 'unshift', 14 'splice', 15 'sort', 16 'reverse' 17 ] 18 // 遍歷上述數組方法名,依次將上述重寫後的數組方法添加到arrayMethods對象上 19 methodsToPatch.forEach(function (method) { 20 // 保存一份當前的方法名對應的數組原始方法 21 const original = arrayProto[method] 22 // 將重寫後的方法定義到arrayMethods對象上,function mutator() {}就是重寫後的方法 23 def(arrayMethods, method, function mutator (...args) { 24 // 調用數組原始方法,並傳入參數args,並將執行結果賦給result 25 const result = original.apply(this, args) 26 // 當數組調用重寫後的方法時,this指向該數組,當該數組爲響應式時,就能夠獲取到其__ob__屬性 27 const ob = this.__ob__ 28 let inserted 29 switch (method) { 30 case 'push': 31 case 'unshift': 32 inserted = args 33 break 34 case 'splice': 35 inserted = args.slice(2) 36 break 37 } 38 if (inserted) ob.observeArray(inserted) 39 // 將當前數組的變動通知給其訂閱者 40 ob.dep.notify() 41 // 最後返回執行結果result 42 return result 43 }) 44 })
def就是經過Object.defineProperty重寫value,也就是自定義的幾個數組方法
function def(obj,key,val,enumble){ Object.defineProperty(obj,key,{ enumble:!!enumble, configrable:true, writeble:true, val:val }) }
observe方法裏面加入數組的處理,
__proto__
屬性,就把__protp__
屬性指向重寫的方法__proto__
屬性,就把重寫的方法定義到對象上實例上// src/core/observer/index.js export class Observer { ... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } ... } function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }