深入淺出 Vue 系列 -- 數(shù)據(jù)劫持實(shí)現(xiàn)原理
一、前言
數(shù)據(jù)雙向綁定作為 Vue 核心功能之一,其實(shí)現(xiàn)原理主要分為兩部分:
- 數(shù)據(jù)劫持
- 發(fā)布訂閱模式
本篇文章主要介紹 Vue 實(shí)現(xiàn)數(shù)據(jù)劫持的思路,下一篇?jiǎng)t會(huì)介紹發(fā)布訂閱模式的設(shè)計(jì)。
二、針對(duì) Object 類型的劫持
對(duì)于 Object 類型,主要劫持其屬性的讀取與設(shè)置操作。在 JavaScript 中對(duì)象的屬性主要由一個(gè)字符串類型的“名稱”以及一個(gè)“屬性描述符”組成,屬性描述符包括以下選項(xiàng):
- value: 該屬性的值;
- writable: 僅當(dāng)值為 true 時(shí)表示該屬性可以被改變;
- get: getter (讀取器);
- set: setter (設(shè)置器);
- configurable: 僅當(dāng)值為 true 時(shí),該屬性可以被刪除以及屬性描述符可以被改變;
- enumerable: 僅當(dāng)值為 true 時(shí),該屬性可以被枚舉。
上述 setter 和 getter 方法就是供開發(fā)者自定義屬性的讀取與設(shè)置操作,而設(shè)置對(duì)象屬性的描述符則少不了 Object.defineProperty() 方法:
function defineReactive (obj, key) { let val = obj[key] Object.defineProperty(obj, key, { get () { console.log(' === 收集依賴 === ') console.log(' 當(dāng)前值為:' + val) return val }, set (newValue) { console.log(' === 通知變更 === ') console.log(' 當(dāng)前值為:' + newValue) val = newValue } }) } const student = { name: 'xiaoming' } defineReactive(student, 'name') // 劫持 name 屬性的讀取和設(shè)置操作
上述代碼通過 Object.defineProperty() 方法設(shè)置屬性的 setter 與 getter 方法,從而達(dá)到劫持 student 對(duì)象中的 name 屬性的讀取和設(shè)置操作的目的。
讀者可以發(fā)現(xiàn),該方法每次只能設(shè)置一個(gè)屬性,那么就需要遍歷對(duì)象來完成其屬性的配置:
Object.keys(student).forEach(key => defineReactive(student, key))
另外還必須是一個(gè)具體的屬性,這也非常的致命。
假如后續(xù)需要擴(kuò)展該對(duì)象,那么就必須手動(dòng)為新屬性設(shè)置 setter 和 getter 方法,**這就是為什么不在 data 中聲明的屬性無法自動(dòng)擁有雙向綁定效果的原因 **。(這時(shí)需要調(diào)用 Vue.set() 手動(dòng)設(shè)置)
以上便是對(duì)象劫持的核心實(shí)現(xiàn),但是還有以下重要的細(xì)節(jié)需要注意:
1、屬性描述符 - configurable
在 JavaScript 中,對(duì)象通過字面量創(chuàng)建時(shí),其屬性描述符默認(rèn)如下:
const foo = { name: '123' } Object.getOwnPropertyDescriptor(foo, 'name') // { value: '123', writable: true, enumerable: true, configurable: true }
前面也提到了 configurable 的值如果為 false,則無法再修改該屬性的描述符,所以在設(shè)置 setter 和 getter 方法時(shí),需要注意 configurable 選項(xiàng)的取值,否則在使用 Object.defineProperty() 方法時(shí)會(huì)拋出異常:
// 部分重復(fù)代碼 這里就不再羅列了。 function defineReactive (obj, key) { // ... const desc = Object.getOwnPropertyDescriptor(obj, key) if (desc && desc.configurable === false) { return } // ... }
而在 JavaScript 中,導(dǎo)致 configurable 值為 false 的情況還是很多的:
- 可能該屬性在此之前已經(jīng)通過 Object.defineProperty() 方法設(shè)置了 configurable 的值;
- 通過 Object.seal() 方法設(shè)置該對(duì)象為密封對(duì)象,只能修改該屬性的值并且不能刪除該屬性以及修改屬性的描述符;
- 通過 Object.freeze() 方法凍結(jié)該對(duì)象,相比較 Object.seal() 方法,它更為嚴(yán)格之處體現(xiàn)在不允許修改屬性的值。
2、默認(rèn) getter 和 setter 方法
另外,開發(fā)者可能已經(jīng)為對(duì)象的屬性設(shè)置了 getter 和 setter 方法,對(duì)于這種情況,Vue 當(dāng)然不能破壞開發(fā)者定義的方法,所以 Vue 中還要保護(hù)默認(rèn)的 getter 和 setter 方法:
// 部分重復(fù)代碼 這里就不再羅列了 function defineReactive (obj, key) { let val = obj[key] //.... // 默認(rèn) getter setter const getter = desc && desc.get const setter = desc && desc.set Object.defineProperty(obj, key, { get () { const value = getter ? getter.call(obj) : val // 優(yōu)先執(zhí)行默認(rèn)的 getter return value }, set (newValue) { const value = getter ? getter.call(obj) : val // 如果值相同則沒必要更新 === 的坑點(diǎn) NaN!!!! if (newValue === value || (value !== value && newValue !== newValue)) { return } if (getter && !setter) { // 用戶未設(shè)置 setter return } if (setter) { // 調(diào)用默認(rèn)的 setter 方法 setter.call(obj, newValue) } else { val = newValue } } }) }
3、遞歸屬性值
最后一種比較重要的情況就是屬性的值可能也是一個(gè)對(duì)象,那么在處理對(duì)象的屬性時(shí),需要遞歸處理其屬性值:
function defineReactive (obj, key) { let val = obj[key] // ... // 遞歸處理其屬性值 const childObj = observe(val) // ... }
遞歸循環(huán)引用對(duì)象很容易出現(xiàn)遞歸爆棧問題,對(duì)于這種情況,Vue 通過定義 ob 對(duì)象記錄已經(jīng)被設(shè)置過 getter 和 setter 方法的對(duì)象,從而避免遞歸爆棧的問題。
function isObject (val) { const type = val return val !== null && (type === 'object' || type === 'function') } function observe (value) { if (!isObject(value)) { return } let ob // 避免循環(huán)引用造成的遞歸爆棧問題 if (value.hasOwnProperty('__ob__') && value.__obj__ instanceof Observer) { ob = value.__ob__ } else if (Object.isExtensible(value)) { // 后續(xù)需要定義諸如 __ob__ 這樣的屬性,所以需要能夠擴(kuò)展 ob = new Observer(value) } return ob }
上述代碼中提到了對(duì)象的可擴(kuò)展性,在 JavaScript 中所有對(duì)象默認(rèn)都是可擴(kuò)展的,但同時(shí)也提供了相應(yīng)的方法允許對(duì)象不可擴(kuò)展:
const obj = { name: 'xiaoming' } Object.preventExtensions(obj) obj.age = 20 console.log(obj.age) // undefined
除了上述方法,還有前面提到的 Object.seal() 和 Object.freeze() 方法。
三、針對(duì) Array 類型的劫持
數(shù)組是一種特殊的對(duì)象,其下標(biāo)實(shí)際上就是對(duì)象的屬性,所以理論上是可以采用 Object.defineProperty() 方法處理數(shù)組對(duì)象。
但是 Vue 并沒有采用上述方法劫持?jǐn)?shù)組對(duì)象,筆者猜測(cè)主要由于以下兩點(diǎn):(讀者有更好的見解,歡迎留言。)
1、特殊的 length 屬性
數(shù)組對(duì)象的 length 屬性的描述符天生獨(dú)特:
const arr = [1, 2, 3] Object.getOwnPropertyDescriptor(arr, 'length').configurable // false
這就意味著無法通過 Object.defineProperty() 方法劫持 length 屬性的讀取和設(shè)置方法。
相比較對(duì)象的屬性,數(shù)組下標(biāo)變化地相對(duì)頻繁,并且改變數(shù)組長(zhǎng)度的方法也比較靈活,一旦數(shù)組的長(zhǎng)度發(fā)生變化,那么在無法自動(dòng)感知的情況下,開發(fā)者只能手動(dòng)更新新增的數(shù)組下標(biāo),這可是一個(gè)很繁瑣的工作。
2、數(shù)組的操作場(chǎng)景
數(shù)組主要的操作場(chǎng)景還是遍歷,而對(duì)于每一個(gè)元素都掛載一個(gè) get 和 set 方法,恐怕也是不小的性能負(fù)擔(dān)。
3、數(shù)組方法的劫持
最終 Vue 選擇劫持一些常用的數(shù)組操作方法,從而知曉數(shù)組的變化情況:
const methods = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice' ]
數(shù)組方法的劫持涉及到原型相關(guān)的知識(shí),首先數(shù)組實(shí)例大部分方法都是來源于 Array.prototype 對(duì)象。
但是這里不能直接篡改 Array.prototype 對(duì)象,這樣會(huì)影響所有的數(shù)組實(shí)例,為了避免這種情況,需要采用原型繼承得到一個(gè)新的原型對(duì)象:
const arrayProto = Array.prototype const injackingPrototype = Object.create(arrayProto)
拿到新的原型對(duì)象之后,再重寫這些常用的操作方法:
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) { // 對(duì)于新增的元素,繼續(xù)劫持 // ob.observeArray(inserted) } // 通知變化 return result } })
最后,更新劫持?jǐn)?shù)組實(shí)例的原型,在 ES6 之前,可以通過瀏覽器私有屬性 proto 指定原型,之后,便可以采用如下方法:
Object.setPrototypeOf(arr, injackingPrototype)
順便提一下,采用 Vue.set() 方法設(shè)置數(shù)組元素時(shí),Vue 內(nèi)部實(shí)際上是調(diào)用劫持后的 splice() 方法來觸發(fā)更新。
四、總結(jié)
由上述內(nèi)容可知,Vue 中的數(shù)據(jù)劫持分為兩大部分:
- 針對(duì) Object 類型,采用 Object.defineProperty() 方法劫持屬性的讀取和設(shè)置方法;
- 針對(duì) Array 類型,采用原型相關(guān)的知識(shí)劫持常用的函數(shù),從而知曉當(dāng)前數(shù)組發(fā)生變化。
并且 Object.defineProperty() 方法存在以下缺陷:
- 每次只能設(shè)置一個(gè)具體的屬性,導(dǎo)致需要遍歷對(duì)象來設(shè)置屬性,同時(shí)也導(dǎo)致了無法探測(cè)新增屬性;
- 屬性描述符 configurable 對(duì)其的影響是致命的。
而 ES6 中的 Proxy 可以完美的解決這些問題(目前兼容性是個(gè)大問題),這也是 Vue3.0 中的一個(gè)大動(dòng)作,有興趣的讀者可以查閱相關(guān)的資料。
以上所述是小編給大家介紹的數(shù)據(jù)劫持實(shí)現(xiàn)原理詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Vue中引入使用patch-package為依賴打補(bǔ)丁問題
這篇文章主要介紹了Vue中引入使用patch-package為依賴打補(bǔ)丁問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03Vue+ECharts+高德地圖API實(shí)現(xiàn)天氣預(yù)報(bào)數(shù)據(jù)可視化的教程
所謂數(shù)據(jù)可視化,我們可以理解為從宏觀角度來看一眼就能看出來整個(gè)數(shù)據(jù)的占比,走向,對(duì)于數(shù)據(jù)可視化,很多互聯(lián)網(wǎng)公司是很看重這一塊的,包括大廠,本就將給大家介紹如何通過Vue+ECharts+高德地圖API實(shí)現(xiàn)天氣預(yù)報(bào)數(shù)據(jù)可視化2023-06-06vue-cli 使用vue-bus來全局控制的實(shí)例講解
今天小編就為大家分享一篇 vue-cli使用vue-bus來全局控制的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09vue+watermark-dom實(shí)現(xiàn)頁(yè)面水印效果(示例代碼)
watermark.js 是基于 DOM 對(duì)象實(shí)現(xiàn)的 BS 系統(tǒng)的水印,確保系統(tǒng)保密性,安全性,降低數(shù)據(jù)泄密風(fēng)險(xiǎn),簡(jiǎn)單輕量,支持多屬性配置,本文將通過 vue 結(jié)合 watermark-dom 庫(kù),教大家實(shí)現(xiàn)簡(jiǎn)單而有效的頁(yè)面水印效果,感興趣的朋友跟隨小編一起看看吧2024-07-07在vue+element ui框架里實(shí)現(xiàn)lodash的debounce防抖
今天小編就為大家分享一篇在vue+element ui框架里實(shí)現(xiàn)lodash的debounce防抖,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11vue實(shí)現(xiàn)節(jié)點(diǎn)增刪改功能
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)節(jié)點(diǎn)增刪改功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09vue中 數(shù)字相加為字串轉(zhuǎn)化為數(shù)值的例子
今天小編就為大家分享一篇vue中 數(shù)字相加為字串轉(zhuǎn)化為數(shù)值的例子,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11Vue全局共享數(shù)據(jù)之globalData,vuex,本地存儲(chǔ)的使用
這篇文章主要介紹了Vue全局共享數(shù)據(jù)之globalData,vuex,本地存儲(chǔ)的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10