導 讀
vue3.0中,響應式數據部分棄用了 Object.defineProperty ,使用 Proxy 來代替它。本文將主要通過以下方面來分析為什么vue選擇棄用 Object.defineProperty 。
Object.defineProperty 真的無法監測數組下標的變化嗎?Observe 部分源碼Object.defineProperty 和 Proxy一、無法監控到數組下標的變化?
在一些技術博客上看到過這樣一種說法,認為 Object.defineProperty 有一個缺陷是無法監聽數組變化:
無法監控到數組下標的變化,導致直接通過數組的下標給數組設置值,不能實時響應。所以vue才設置了7個變異數組( push 、 pop 、 shift 、 unshift 、 splice 、 sort 、 reverse )的 hack 方法來解決問題。
Object.defineProperty 的第一個缺陷,無法監聽數組變化。 然而Vue的文檔提到了Vue是可以檢測到數組變化的,但是只有以下八種方法, vm.items[indexOfItem] = newValue 這種是無法檢測的。
這種說法是有問題的,事實上, Object.defineProperty 本身是可以監控到數組下標的變化的,只是在 Vue 的實現中,從性能/體驗的性價比考慮,放棄了這個特性。
下面我們通過一個例子來為 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)
上面代碼對數組arr的每個屬性通過 Object.defineProperty 進行劫持,下面我們對數組arr進行操作,看看哪些行為會觸發數組的 getter 和 setter 方法。
1. 通過下標獲取某個元素和修改某個元素的值

可以看到,通過下標獲取某個元素會觸發 getter 方法, 設置某個值會觸發 setter
方法。
接下來,我們再試一下數組的一些操作方法,看看是否會觸發。
2. 數組的 push 方法

push 并未觸發 setter 和 getter 方法,數組的下標可以看做是對象中的 key ,這里 push 之后相當于增加了下索引為3的元素,但是并未對新的下標進行 observe ,所以不會觸發。
3. 數組的 unshift 方法

我擦,發生了什么?
unshift 操作會導致原來索引為0,1,2,3的值發生變化,這就需要將原來索引為0,1,2,3的值取出來,然后重新賦值,所以取值的過程觸發了 getter ,賦值時觸發了 setter 。
下面我們嘗試通過索引獲取一下對應的元素:

只有索引為0,1,2的屬性才會觸發 getter 。
這里我們可以對比對象來看,arr數組初始值為[1, 2, 3],即只對索引為0,1,2執行了 observe 方法,所以無論后來數組的長度發生怎樣的變化,依然只有索引為0,1,2的元素發生變化才會觸發,其他的新增索引,就相當于對象中新增的屬性,需要再手動 observe 才可以。
4. 數組的 pop 方法

當移除的元素為引用為2的元素時,會觸發 getter 。

刪除了索引為2的元素后,再去修改或獲取它的值時,不會再觸發 setter 和 getter 。
這和對象的處理是同樣的,數組的索引被刪除后,就相當于對象的屬性被刪除一樣,不會再去觸發 observe 。
到這里,我們可以簡單的總結一下結論。
Object.defineProperty 在數組中的表現和在對象中的表現是一致的,數組的索引就可以看做是對象中的 key 。
getter 和 setter 方法push 或 unshift 會增加索引,對于新增加的屬性,需要再手動初始化才能被 observe 。pop 或 shift 刪除元素,會刪除并更新索引,也會觸發 setter 和 getter 方法。所以, Object.defineProperty 是有監控數組下標變化的能力的,只是vue2.x放棄了這個特性。
二、vue對數組的observe做了哪些處理?
vue的 Observer 類定義在 core/observer/index.js 中。

可以看到,vue的 Observer 對數組做了單獨的處理。

hasProto 是判斷數組的實例是否有 __proto__ 屬性,如果有 __proto__ 屬性就會執行 protoAugment 方法,將 arrayMethods 重寫到原型上。 hasProto 定義如下。

arrayMethods 是對數組的方法進行重寫,定義在 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'
// 復制數組構造函數的原型,Array.prototype也是一個數組。
const arrayProto = Array.prototype
// 創建對象,對象的__proto__指向arrayProto,所以arrayMethods的__proto__包含數組的所有方法。
export const arrayMethods = Object.create(arrayProto)
// 下面的數組是要進行重寫的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
// 遍歷methodsToPatch數組,對其中的方法進行重寫
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// def方法定義在lang.js文件中,是通過object.defineProperty對屬性進行重新定義。
// 即在arrayMethods中找到我們要重寫的方法,對其進行重新定義
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
// 上面已經分析過,對于push,unshift會新增索引,所以需要手動observe
case 'push':
case 'unshift':
inserted = args
break
// splice方法,如果傳入了第三個參數,也會有新增索引,所以也需要手動observe
case 'splice':
inserted = args.slice(2)
break
}
// push,unshift,splice三個方法觸發后,在這里手動observe,其他方法的變更會在當前的索引上進行更新,所以不需要再執行ob.observeArray
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
三 Object.defineProperty VS Proxy
上面已經知道 Object.defineProperty 對數組和對象的表現是一致的,那么它和 Proxy 對比存在哪些優缺點呢?
1. Object.defineProperty只能劫持對象的屬性,而Proxy是直接代理對象。
由于 Object.defineProperty 只能對屬性進行劫持,需要遍歷對象的每個屬性,如果屬性值也是對象,則需要深度遍歷。而 Proxy 直接代理對象,不需要遍歷操作。
2. Object.defineProperty對新增屬性需要手動進行Observe。
由于 Object.defineProperty 劫持的是對象的屬性,所以新增屬性時,需要重新遍歷對象,對其新增屬性再使用 Object.defineProperty 進行劫持。
也正是因為這個原因,使用vue給 data 中的數組或對象新增屬性時,需要使用 vm.$set 才能保證新增的屬性也是響應式的。
下面看一下vue的 set 方法是如何實現的, 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是數組,且key是有效的數組索引,會調用數組的splice方法,
// 我們上面說過,數組的splice方法會被重寫,重寫的方法中會手動Observe
// 所以vue的set方法,對于數組,就是直接調用重寫splice方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 對于對象,如果key本來就是對象中的屬性,直接修改值就可以觸發更新
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// vue的響應式對象中都會添加了__ob__屬性,所以可以根據是否有__ob__屬性判斷是否為響應式對象
const ob = (target: any).__ob__
// 如果不是響應式對象,直接賦值
if (!ob) {
target[key] = val
return val
}
// 調用defineReactive給數據添加了 getter 和 setter,
// 所以vue的set方法,對于響應式的對象,就會調用defineReactive重新定義響應式對象,defineReactive 函數
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
在 set 方法中,對 target 是數組和對象做了分別的處理, target 是數組時,會調用重寫過的 splice 方法進行手動 Observe 。
對于對象,如果 key 本來就是對象的屬性,則直接修改值觸發更新,否則調用 defineReactive 方法重新定義響應式對象。
如果采用 proxy 實現, Proxy 通過 set(target, propKey, value, receiver) 攔截對象屬性的設置,是可以攔截到對象的新增屬性的。

不止如此, Proxy 對數組的方法也可以監測到,不需要像上面vue2.x源碼中那樣進行 hack 。

完美?。?!
3. Proxy支持13種攔截操作,這是defineProperty所不具有的
get(target, propKey, receiver):攔截對象屬性的讀取,比如 proxy.foo 和 proxy['foo'] 。
set(target, propKey, value, receiver):攔截對象屬性的設置,比如 proxy.foo = v 或 proxy['foo'] = v ,返回一個布爾值。
has(target, propKey):攔截 propKey in proxy 的操作,返回一個布爾值。
deleteProperty(target, propKey):攔截 delete proxy[propKey] 的操作,返回一個布爾值。
ownKeys(target):攔截 Object.getOwnPropertyNames(proxy) 、 Object.getOwnPropertySymbols(proxy) 、 Object.keys(proxy) 、 for...in 循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而 Object.keys() 的返回結果僅包括目標對象自身的可遍歷屬性。
getOwnPropertyDescriptor(target, propKey):攔截 Object.getOwnPropertyDescriptor(proxy, propKey) ,返回屬性的描述對象。
defineProperty(target, propKey, propDesc):攔截 Object.defineProperty(proxy, propKey, propDesc) 、 Object.defineProperties(proxy, propDescs) ,返回一個布爾值。
preventExtensions(target):攔截 Object.preventExtensions(proxy) ,返回一個布爾值。
getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy) ,返回一個對象。
isExtensible(target):攔截 Object.isExtensible(proxy) ,返回一個布爾值。
setPrototypeOf(target, proto):攔截 Object.setPrototypeOf(proxy, proto) ,返回一個布爾值。如果目標對象是函數,那么還有兩種額外操作可以攔截。
apply(target, object, args):攔截 Proxy 實例作為函數調用的操作,比如 proxy(...args) 、 proxy.call(object, ...args) 、 proxy.apply(...) 。
construct(target, args):攔截 Proxy 實例作為構造函數調用的操作,比如 new proxy(...args) 。
4. 新標準性能紅利
Proxy 作為新標準,長遠來看,JS引擎會繼續優化 Proxy ,但 getter 和 setter 基本不會再有針對性優化。
5. Proxy兼容性差

可以看到, Proxy 對于IE瀏覽器來說簡直是災難。
并且目前并沒有一個完整支持 Proxy 所有攔截方法的Polyfill方案,有一個google編寫的proxy-polyfill 也只支持了 get,set,apply,construct 四種攔截,可以支持到IE9+和Safari 6+。
四 總結
參考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
https://www.jb51.net/article/171872.htm
https://zhuanlan.zhihu.com/p/35080324
http://es6.ruanyifeng.com/#docs/proxy
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。