為什么Vue3.0使用Proxy實(shí)現(xiàn)數(shù)據(jù)監(jiān)聽(defineProperty表示不背這個(gè)鍋)
導(dǎo) 讀
vue3.0中,響應(yīng)式數(shù)據(jù)部分棄用了 Object.defineProperty
,使用 Proxy
來代替它。本文將主要通過以下方面來分析為什么vue選擇棄用 Object.defineProperty
。
Object.defineProperty
真的無法監(jiān)測數(shù)組下標(biāo)的變化嗎?- 分析vue2.x中對(duì)數(shù)組
Observe
部分源碼 - 對(duì)比
Object.defineProperty
和Proxy
一、無法監(jiān)控到數(shù)組下標(biāo)的變化?
在一些技術(shù)博客上看到過這樣一種說法,認(rèn)為 Object.defineProperty
有一個(gè)缺陷是無法監(jiān)聽數(shù)組變化:
無法監(jiān)控到數(shù)組下標(biāo)的變化,導(dǎo)致直接通過數(shù)組的下標(biāo)給數(shù)組設(shè)置值,不能實(shí)時(shí)響應(yīng)。所以vue才設(shè)置了7個(gè)變異數(shù)組( push
、 pop
、 shift
、 unshift
、 splice
、 sort
、 reverse
)的 hack
方法來解決問題。
Object.defineProperty
的第一個(gè)缺陷,無法監(jiān)聽數(shù)組變化。 然而Vue的文檔提到了Vue是可以檢測到數(shù)組變化的,但是只有以下八種方法, vm.items[indexOfItem] = newValue
這種是無法檢測的。
這種說法是有問題的,事實(shí)上, Object.defineProperty
本身是可以監(jiān)控到數(shù)組下標(biāo)的變化的,只是在 Vue 的實(shí)現(xiàn)中,從性能/體驗(yàn)的性價(jià)比考慮,放棄了這個(gè)特性。
下面我們通過一個(gè)例子來為 Object.defineProperty
正名:
function defineReactive(data, key, value) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function defineGet() { console.log(`get key: ${key} value: ${value}`) return value }, set: function defineSet(newVal) { console.log(`set key: ${key} value: ${newVal}`) value = newVal } }) } function observe(data) { Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]) }) } let arr = [1, 2, 3] observe(arr)
上面代碼對(duì)數(shù)組arr的每個(gè)屬性通過 Object.defineProperty
進(jìn)行劫持,下面我們對(duì)數(shù)組arr進(jìn)行操作,看看哪些行為會(huì)觸發(fā)數(shù)組的 getter
和 setter
方法。
1. 通過下標(biāo)獲取某個(gè)元素和修改某個(gè)元素的值
可以看到,通過下標(biāo)獲取某個(gè)元素會(huì)觸發(fā) getter
方法, 設(shè)置某個(gè)值會(huì)觸發(fā) setter
方法。
接下來,我們?cè)僭囈幌聰?shù)組的一些操作方法,看看是否會(huì)觸發(fā)。
2. 數(shù)組的 push 方法
push
并未觸發(fā) setter
和 getter
方法,數(shù)組的下標(biāo)可以看做是對(duì)象中的 key
,這里 push
之后相當(dāng)于增加了下索引為3的元素,但是并未對(duì)新的下標(biāo)進(jìn)行 observe
,所以不會(huì)觸發(fā)。
3. 數(shù)組的 unshift 方法
我擦,發(fā)生了什么?
unshift
操作會(huì)導(dǎo)致原來索引為0,1,2,3的值發(fā)生變化,這就需要將原來索引為0,1,2,3的值取出來,然后重新賦值,所以取值的過程觸發(fā)了 getter
,賦值時(shí)觸發(fā)了 setter
。
下面我們嘗試通過索引獲取一下對(duì)應(yīng)的元素:
只有索引為0,1,2的屬性才會(huì)觸發(fā) getter
。
這里我們可以對(duì)比對(duì)象來看,arr數(shù)組初始值為[1, 2, 3],即只對(duì)索引為0,1,2執(zhí)行了 observe
方法,所以無論后來數(shù)組的長度發(fā)生怎樣的變化,依然只有索引為0,1,2的元素發(fā)生變化才會(huì)觸發(fā),其他的新增索引,就相當(dāng)于對(duì)象中新增的屬性,需要再手動(dòng) observe
才可以。
4. 數(shù)組的 pop 方法
當(dāng)移除的元素為引用為2的元素時(shí),會(huì)觸發(fā) getter
。
刪除了索引為2的元素后,再去修改或獲取它的值時(shí),不會(huì)再觸發(fā) setter
和 getter
。
這和對(duì)象的處理是同樣的,數(shù)組的索引被刪除后,就相當(dāng)于對(duì)象的屬性被刪除一樣,不會(huì)再去觸發(fā) observe
。
到這里,我們可以簡單的總結(jié)一下結(jié)論。
Object.defineProperty
在數(shù)組中的表現(xiàn)和在對(duì)象中的表現(xiàn)是一致的,數(shù)組的索引就可以看做是對(duì)象中的 key
。
- 通過索引訪問或設(shè)置對(duì)應(yīng)元素的值時(shí),可以觸發(fā)
getter
和setter
方法 - 通過
push
或unshift
會(huì)增加索引,對(duì)于新增加的屬性,需要再手動(dòng)初始化才能被observe
。 - 通過
pop
或shift
刪除元素,會(huì)刪除并更新索引,也會(huì)觸發(fā)setter
和getter
方法。
所以, Object.defineProperty
是有監(jiān)控?cái)?shù)組下標(biāo)變化的能力的,只是vue2.x放棄了這個(gè)特性。
二、vue對(duì)數(shù)組的observe做了哪些處理?
vue的 Observer
類定義在 core/observer/index.js
中。
可以看到,vue的 Observer
對(duì)數(shù)組做了單獨(dú)的處理。
hasProto
是判斷數(shù)組的實(shí)例是否有 __proto__
屬性,如果有 __proto__
屬性就會(huì)執(zhí)行 protoAugment
方法,將 arrayMethods
重寫到原型上。 hasProto
定義如下。
arrayMethods
是對(duì)數(shù)組的方法進(jìn)行重寫,定義在 core/observer/array.js
中, 下面是這部分源碼的分析。
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' // 復(fù)制數(shù)組構(gòu)造函數(shù)的原型,Array.prototype也是一個(gè)數(shù)組。 const arrayProto = Array.prototype // 創(chuàng)建對(duì)象,對(duì)象的__proto__指向arrayProto,所以arrayMethods的__proto__包含數(shù)組的所有方法。 export const arrayMethods = Object.create(arrayProto) // 下面的數(shù)組是要進(jìn)行重寫的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ // 遍歷methodsToPatch數(shù)組,對(duì)其中的方法進(jìn)行重寫 methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] // def方法定義在lang.js文件中,是通過object.defineProperty對(duì)屬性進(jìn)行重新定義。 // 即在arrayMethods中找到我們要重寫的方法,對(duì)其進(jìn)行重新定義 def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { // 上面已經(jīng)分析過,對(duì)于push,unshift會(huì)新增索引,所以需要手動(dòng)observe case 'push': case 'unshift': inserted = args break // splice方法,如果傳入了第三個(gè)參數(shù),也會(huì)有新增索引,所以也需要手動(dòng)observe case 'splice': inserted = args.slice(2) break } // push,unshift,splice三個(gè)方法觸發(fā)后,在這里手動(dòng)observe,其他方法的變更會(huì)在當(dāng)前的索引上進(jìn)行更新,所以不需要再執(zhí)行ob.observeArray if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
三 Object.defineProperty VS Proxy
上面已經(jīng)知道 Object.defineProperty
對(duì)數(shù)組和對(duì)象的表現(xiàn)是一致的,那么它和 Proxy
對(duì)比存在哪些優(yōu)缺點(diǎn)呢?
1. Object.defineProperty只能劫持對(duì)象的屬性,而Proxy是直接代理對(duì)象。
由于 Object.defineProperty
只能對(duì)屬性進(jìn)行劫持,需要遍歷對(duì)象的每個(gè)屬性,如果屬性值也是對(duì)象,則需要深度遍歷。而 Proxy
直接代理對(duì)象,不需要遍歷操作。
2. Object.defineProperty對(duì)新增屬性需要手動(dòng)進(jìn)行Observe。
由于 Object.defineProperty
劫持的是對(duì)象的屬性,所以新增屬性時(shí),需要重新遍歷對(duì)象,對(duì)其新增屬性再使用 Object.defineProperty
進(jìn)行劫持。
也正是因?yàn)檫@個(gè)原因,使用vue給 data
中的數(shù)組或?qū)ο笮略鰧傩詴r(shí),需要使用 vm.$set
才能保證新增的屬性也是響應(yīng)式的。
下面看一下vue的 set
方法是如何實(shí)現(xiàn)的, set
方法定義在 core/observer/index.js
,下面是核心代碼。
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { // 如果target是數(shù)組,且key是有效的數(shù)組索引,會(huì)調(diào)用數(shù)組的splice方法, // 我們上面說過,數(shù)組的splice方法會(huì)被重寫,重寫的方法中會(huì)手動(dòng)Observe // 所以vue的set方法,對(duì)于數(shù)組,就是直接調(diào)用重寫splice方法 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 對(duì)于對(duì)象,如果key本來就是對(duì)象中的屬性,直接修改值就可以觸發(fā)更新 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // vue的響應(yīng)式對(duì)象中都會(huì)添加了__ob__屬性,所以可以根據(jù)是否有__ob__屬性判斷是否為響應(yīng)式對(duì)象 const ob = (target: any).__ob__ // 如果不是響應(yīng)式對(duì)象,直接賦值 if (!ob) { target[key] = val return val } // 調(diào)用defineReactive給數(shù)據(jù)添加了 getter 和 setter, // 所以vue的set方法,對(duì)于響應(yīng)式的對(duì)象,就會(huì)調(diào)用defineReactive重新定義響應(yīng)式對(duì)象,defineReactive 函數(shù) defineReactive(ob.value, key, val) ob.dep.notify() return val }
在 set
方法中,對(duì) target
是數(shù)組和對(duì)象做了分別的處理, target
是數(shù)組時(shí),會(huì)調(diào)用重寫過的 splice
方法進(jìn)行手動(dòng) Observe
。
對(duì)于對(duì)象,如果 key
本來就是對(duì)象的屬性,則直接修改值觸發(fā)更新,否則調(diào)用 defineReactive
方法重新定義響應(yīng)式對(duì)象。
如果采用 proxy
實(shí)現(xiàn), Proxy
通過 set(target, propKey, value, receiver)
攔截對(duì)象屬性的設(shè)置,是可以攔截到對(duì)象的新增屬性的。
不止如此, Proxy
對(duì)數(shù)組的方法也可以監(jiān)測到,不需要像上面vue2.x源碼中那樣進(jìn)行 hack
。
完美!??!
3. Proxy支持13種攔截操作,這是defineProperty所不具有的
get(target, propKey, receiver):攔截對(duì)象屬性的讀取,比如 proxy.foo
和 proxy['foo']
。
set(target, propKey, value, receiver):攔截對(duì)象屬性的設(shè)置,比如 proxy.foo = v
或 proxy['foo'] = v
,返回一個(gè)布爾值。
has(target, propKey):攔截 propKey in proxy
的操作,返回一個(gè)布爾值。
deleteProperty(target, propKey):攔截 delete proxy[propKey]
的操作,返回一個(gè)布爾值。
ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)
、 Object.getOwnPropertySymbols(proxy)
、 Object.keys(proxy)
、 for...in
循環(huán),返回一個(gè)數(shù)組。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名,而 Object.keys()
的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性。
getOwnPropertyDescriptor(target, propKey):攔截 Object.getOwnPropertyDescriptor(proxy, propKey)
,返回屬性的描述對(duì)象。
defineProperty(target, propKey, propDesc):攔截 Object.defineProperty(proxy, propKey, propDesc)
、 Object.defineProperties(proxy, propDescs)
,返回一個(gè)布爾值。
preventExtensions(target):攔截 Object.preventExtensions(proxy)
,返回一個(gè)布爾值。
getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy)
,返回一個(gè)對(duì)象。
isExtensible(target):攔截 Object.isExtensible(proxy)
,返回一個(gè)布爾值。
setPrototypeOf(target, proto):攔截 Object.setPrototypeOf(proxy, proto)
,返回一個(gè)布爾值。如果目標(biāo)對(duì)象是函數(shù),那么還有兩種額外操作可以攔截。
apply(target, object, args):攔截 Proxy
實(shí)例作為函數(shù)調(diào)用的操作,比如 proxy(...args)
、 proxy.call(object, ...args)
、 proxy.apply(...)
。
construct(target, args):攔截 Proxy
實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作,比如 new proxy(...args)
。
4. 新標(biāo)準(zhǔn)性能紅利
Proxy
作為新標(biāo)準(zhǔn),長遠(yuǎn)來看,JS引擎會(huì)繼續(xù)優(yōu)化 Proxy
,但 getter
和 setter
基本不會(huì)再有針對(duì)性優(yōu)化。
5. Proxy兼容性差
可以看到, Proxy
對(duì)于IE瀏覽器來說簡直是災(zāi)難。
并且目前并沒有一個(gè)完整支持 Proxy
所有攔截方法的Polyfill方案,有一個(gè)google編寫的proxy-polyfill 也只支持了 get,set,apply,construct 四種攔截,可以支持到IE9+和Safari 6+。
四 總結(jié)
- Object.defineProperty 對(duì)數(shù)組和對(duì)象的表現(xiàn)一直,并非不能監(jiān)控?cái)?shù)組下標(biāo)的變化,vue2.x中無法通過數(shù)組索引來實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)的自動(dòng)更新是vue本身的設(shè)計(jì)導(dǎo)致的,不是 defineProperty 的鍋。
- Object.defineProperty 和 Proxy 本質(zhì)差別是,defineProperty 只能對(duì)屬性進(jìn)行劫持,所以出現(xiàn)了需要遞歸遍歷,新增屬性需要手動(dòng) Observe 的問題。
- Proxy 作為新標(biāo)準(zhǔn),瀏覽器廠商勢(shì)必會(huì)對(duì)其進(jìn)行持續(xù)優(yōu)化,但它的兼容性也是塊硬傷,并且目前還沒有完整的polifill方案。
參考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
http://www.dbjr.com.cn/article/171872.htm
https://zhuanlan.zhihu.com/p/35080324
http://es6.ruanyifeng.com/#docs/proxy
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
打包組件報(bào)錯(cuò):Error:Cannot?find?module?'vue/compiler-sfc&ap
最近遇到這樣的問題,vue組件庫搭建過程中使用webpack打包組件時(shí)報(bào)錯(cuò),本文給大家分享打包組件報(bào)錯(cuò):Error:?Cannot?find?module?‘vue/compiler-sfc‘的解決方法,感興趣的朋友一起看看吧2023-12-12Vue.js路由實(shí)現(xiàn)選項(xiàng)卡簡單實(shí)例
這篇文章主要為大家詳細(xì)介紹了Vue.js路由實(shí)現(xiàn)選項(xiàng)卡簡單實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07vue監(jiān)聽頁面滾動(dòng)到某個(gè)高度觸發(fā)事件流程
這篇文章主要介紹了vue監(jiān)聽頁面滾動(dòng)到某個(gè)高度觸發(fā)事件流程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04vue element-ui el-table組件自定義合計(jì)(summary-method)的坑
這篇文章主要介紹了vue element-ui el-table組件自定義合計(jì)(summary-method)的坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02利用VUE框架,實(shí)現(xiàn)列表分頁功能示例代碼
本篇文章主要介紹了利用VUE框架,實(shí)現(xiàn)列表分頁功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01vue實(shí)現(xiàn)動(dòng)態(tài)進(jìn)度條效果
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)動(dòng)態(tài)進(jìn)度條效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09