vue作為前端使用廣泛的三大框架(react、vue、Angular)之一,vue2.x的雙向數據綁定是基於Object.defineProperty實現。
vue2.x雙向數據綁定解析
vue2.x是利用Object.defineProperty劫持對象或對象的屬性的訪問器,在屬性值發生變化時獲取屬性值變化, 從而進行后續操作。
1、Object.defineProperty在js中的描述:
Object.defineProperty(obj, prop, descriptor) 直接在一個對象上定義一個屬性,或者修改一個對象的現有 屬性,並返回這個對象。
參數:obj 要在其上定義屬性的對象;prop 要定義或修改的屬性的名稱;descriptor 將被定義或修改的屬性描述符。
返回值: 傳遞給函數的對象obj
// 定義一個對象 const data={name:'peak',age:10} // 遍歷對象 實現對對象的屬性進行劫持 Object.keys(data).forEach((key) => { Object.defineProperty(data, key, { // 當且僅當該屬性的enumerable為true時,該屬性才能夠出現在對象的枚舉屬性中 enumerable: true, // 當且僅當該屬性的 configurable 為 true 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的對象上被刪除 configurable: true, get: ()=>{ // 一個給屬性提供 getter 的方法 console.info(`get ${key}-${val}`) return val; }, set: (newVal)=>{ // 一個給屬性提供 setter 的方法 // 當屬性值發生變化時我們可以進行額外操作 如調用監聽器 if(newVal === val ){ // 如果未發生變化 不做其他操作 return; } console.log(`觸發視圖更新函數${newVal}`); }, }); }); data.age=25 // 觸發set方法
2、基於Object.defineProperty的數據劫持優勢以及實現方式
Object.defineProperty的對象以及對象屬性的劫持有以下優勢:
(1)無需顯示調用,如Vue2.x使用Object.defineProperty對象以及對象屬性的劫持+發布訂閱模式,只要數據發生變化直接通知變化 並驅動視圖更新。
(2)可在set函數中精確得知變化數據而不用逐個遍歷屬性獲取變化值,減少性能損耗。
實現思路:
(1)利用Object.defineProperty重新定義一遍目標對象,完成對目標對象的劫持,在屬性值變化后即觸發set方法 后通知訂閱者,告訴該對象的某個屬性值發生了變化。
(2)解析器Compile解析模板中的指令,收集指令所依賴的方法和數據,等待數據變化然后進行渲染。
(3)Watcher在收到屬性值發生變化后,根據解析器Compile提供的指令進行視圖渲染。
為更好的說明vue2.x的響應式原理,下面vue2.x的源碼引用了Vue源碼解讀
監聽數據變化
對data進行改造,所有屬性設置set&get,用於在屬性獲取或者設置時,添加邏輯
// 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方法 // 此處的問題是:並不是每次Dep.target有值時都需要添加到訂閱者管理員中去管理,需要對訂閱者去重,不影響整體思路,不去管它 if(Dep.target){ dep.addSub(Dep.target) } return val }, set: (newVal) => { console.log('new value seted') 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){ this.subs.push(sub) } notify(){ // 通知所有的訂閱者(Watcher),觸發訂閱者的相應邏輯處理 this.subs.forEach((sub) => sub.update()) } }
訂閱者
每個訂閱者都是對某條數據的訂閱,訂閱者維護着每一次更新之前的數據,將其和更新之后的數據進行對比,如果發生了變化,則執行相應的業務邏輯,並更新訂閱者中維護的數據的值
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
將數據代理到Vue實例上,真實數據存儲於實例的_data屬性中
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 } }) } }
調用這個極簡版演示數據雙向綁定原理的Vue
import Vue from './Vue'; let demo = new Vue({ data: { 'a': { 'ab': { 'c': 'C' } }, 'b': { 'bb': 'BB' }, 'c': 'C' } }); demo.$watch('c', () => console.log('c is changed')) // get value demo.c = 'CCC' // new value seted // get value // c is changed demo.c = 'DDD' // new value seted // get value // c is changed demo.a // get value demo.a.ab = { 'd': 'D' } // get value // get value // new value seted console.log(demo.a.ab) // get value // get value // {get d: (), set d: ()} demo.a.ab.d = 'DD' // get value // get value // new value seted console.log(demo.a.ab); // get value // get value // {get d: (), set d: ()}
總結:
在一些技術博客上,有人指出Object.defineProperty存在缺陷,只能監聽到非數組對象的變化,而監聽不到數組的變化,實際上這是錯誤的理解,Object.defineProperty是可以監聽數組變化的,只是從性能/體驗的性價比考慮,放棄了這個特性,vue設置
7個變異數組(push
、pop
、shift
、unshift
、splice
、sort
、reverse
)改用hack的方式解決數組變化的問題。
參考資料