您好,登錄后才能下訂單哦!
一、前言
數據雙向綁定作為 Vue 核心功能之一,其實現原理主要分為兩部分:
本篇文章主要介紹 Vue 實現數據劫持的思路,下一篇則會介紹發布訂閱模式的設計。
二、針對 Object 類型的劫持
對于 Object 類型,主要劫持其屬性的讀取與設置操作。在 JavaScript 中對象的屬性主要由一個字符串類型的“名稱”以及一個“屬性描述符”組成,屬性描述符包括以下選項:
上述 setter 和 getter 方法就是供開發者自定義屬性的讀取與設置操作,而設置對象屬性的描述符則少不了 Object.defineProperty() 方法:
function defineReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { console.log(' === 收集依賴 === ') console.log(' 當前值為:' + val) return val }, set (newValue) { console.log(' === 通知變更 === ') console.log(' 當前值為:' + newValue) val = newValue } }) } const student = { name: 'xiaoming' } defineReactive(student, 'name') // 劫持 name 屬性的讀取和設置操作
上述代碼通過 Object.defineProperty() 方法設置屬性的 setter 與 getter 方法,從而達到劫持 student 對象中的 name 屬性的讀取和設置操作的目的。
讀者可以發現,該方法每次只能設置一個屬性,那么就需要遍歷對象來完成其屬性的配置:
Object.keys(student).forEach(key => defineReactive(student, key))
另外還必須是一個具體的屬性,這也非常的致命。
假如后續需要擴展該對象,那么就必須手動為新屬性設置 setter 和 getter 方法,**這就是為什么不在 data 中聲明的屬性無法自動擁有雙向綁定效果的原因 **。(這時需要調用 Vue.set() 手動設置)
以上便是對象劫持的核心實現,但是還有以下重要的細節需要注意:
1、屬性描述符 - configurable
在 JavaScript 中,對象通過字面量創建時,其屬性描述符默認如下:
const foo = { name: '123' } Object.getOwnPropertyDescriptor(foo, 'name') // { value: '123', writable: true, enumerable: true, configurable: true }
前面也提到了 configurable 的值如果為 false,則無法再修改該屬性的描述符,所以在設置 setter 和 getter 方法時,需要注意 configurable 選項的取值,否則在使用 Object.defineProperty() 方法時會拋出異常:
// 部分重復代碼 這里就不再羅列了。 function defineReactive (obj, key) { // ... const desc = Object.getOwnPropertyDescriptor(obj, key) if (desc && desc.configurable === false) { return } // ... }
而在 JavaScript 中,導致 configurable 值為 false 的情況還是很多的:
2、默認 getter 和 setter 方法
另外,開發者可能已經為對象的屬性設置了 getter 和 setter 方法,對于這種情況,Vue 當然不能破壞開發者定義的方法,所以 Vue 中還要保護默認的 getter 和 setter 方法:
// 部分重復代碼 這里就不再羅列了 function defineReactive (obj, key) { let val = obj[key] //.... // 默認 getter setter const getter = desc && desc.get const setter = desc && desc.set Object.defineProperty(obj, key, { get () { const value = getter ? getter.call(obj) : val // 優先執行默認的 getter return value }, set (newValue) { const value = getter ? getter.call(obj) : val // 如果值相同則沒必要更新 === 的坑點 NaN!!!! if (newValue === value || (value !== value && newValue !== newValue)) { return } if (getter && !setter) { // 用戶未設置 setter return } if (setter) { // 調用默認的 setter 方法 setter.call(obj, newValue) } else { val = newValue } } }) }
3、遞歸屬性值
最后一種比較重要的情況就是屬性的值可能也是一個對象,那么在處理對象的屬性時,需要遞歸處理其屬性值:
function defineReactive (obj, key) { let val = obj[key] // ... // 遞歸處理其屬性值 const childObj = observe(val) // ... }
遞歸循環引用對象很容易出現遞歸爆棧問題,對于這種情況,Vue 通過定義 ob 對象記錄已經被設置過 getter 和 setter 方法的對象,從而避免遞歸爆棧的問題。
function isObject (val) { const type = val return val !== null && (type === 'object' || type === 'function') } function observe (value) { if (!isObject(value)) { return } let ob // 避免循環引用造成的遞歸爆棧問題 if (value.hasOwnProperty('__ob__') && value.__obj__ instanceof Observer) { ob = value.__ob__ } else if (Object.isExtensible(value)) { // 后續需要定義諸如 __ob__ 這樣的屬性,所以需要能夠擴展 ob = new Observer(value) } return ob }
上述代碼中提到了對象的可擴展性,在 JavaScript 中所有對象默認都是可擴展的,但同時也提供了相應的方法允許對象不可擴展:
const obj = { name: 'xiaoming' } Object.preventExtensions(obj) obj.age = 20 console.log(obj.age) // undefined
除了上述方法,還有前面提到的 Object.seal() 和 Object.freeze() 方法。
三、針對 Array 類型的劫持
數組是一種特殊的對象,其下標實際上就是對象的屬性,所以理論上是可以采用 Object.defineProperty() 方法處理數組對象。
但是 Vue 并沒有采用上述方法劫持數組對象,筆者猜測主要由于以下兩點:(讀者有更好的見解,歡迎留言。)
1、特殊的 length 屬性
數組對象的 length 屬性的描述符天生獨特:
const arr = [1, 2, 3] Object.getOwnPropertyDescriptor(arr, 'length').configurable // false
這就意味著無法通過 Object.defineProperty() 方法劫持 length 屬性的讀取和設置方法。
相比較對象的屬性,數組下標變化地相對頻繁,并且改變數組長度的方法也比較靈活,一旦數組的長度發生變化,那么在無法自動感知的情況下,開發者只能手動更新新增的數組下標,這可是一個很繁瑣的工作。
2、數組的操作場景
數組主要的操作場景還是遍歷,而對于每一個元素都掛載一個 get 和 set 方法,恐怕也是不小的性能負擔。
3、數組方法的劫持
最終 Vue 選擇劫持一些常用的數組操作方法,從而知曉數組的變化情況:
const methods = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice' ]
數組方法的劫持涉及到原型相關的知識,首先數組實例大部分方法都是來源于 Array.prototype 對象。
但是這里不能直接篡改 Array.prototype 對象,這樣會影響所有的數組實例,為了避免這種情況,需要采用原型繼承得到一個新的原型對象:
const arrayProto = Array.prototype const injackingPrototype = Object.create(arrayProto)
拿到新的原型對象之后,再重寫這些常用的操作方法:
methods.forEach(method => { const originArrayMethod = arrayProto[method] injackingPrototype[method] = function (...args) { const result = originArrayMethod.apply(this, args) let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) { // 對于新增的元素,繼續劫持 // ob.observeArray(inserted) } // 通知變化 return result } })
最后,更新劫持數組實例的原型,在 ES6 之前,可以通過瀏覽器私有屬性 proto 指定原型,之后,便可以采用如下方法:
Object.setPrototypeOf(arr, injackingPrototype)
順便提一下,采用 Vue.set() 方法設置數組元素時,Vue 內部實際上是調用劫持后的 splice() 方法來觸發更新。
四、總結
由上述內容可知,Vue 中的數據劫持分為兩大部分:
并且 Object.defineProperty() 方法存在以下缺陷:
而 ES6 中的 Proxy 可以完美的解決這些問題(目前兼容性是個大問題),這也是 Vue3.0 中的一個大動作,有興趣的讀者可以查閱相關的資料。
以上所述是小編給大家介紹的數據劫持實現原理詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。