vue3.0中,響應式數據部分棄用了 Object.defineProperty
,使用 Proxy
來代替它。本文將主要經過如下方面來分析爲何vue選擇棄用 Object.defineProperty
。javascript
Object.defineProperty
真的沒法監測數組下標的變化嗎?Observe
部分源碼Object.defineProperty
和 Proxy
在一些技術博客上看到過這樣一種說法,認爲 Object.defineProperty
有一個缺陷是沒法監聽數組變化:前端
沒法監控到數組下標的變化,致使直接經過數組的下標給數組設置值,不能實時響應。因此vue才設置了7個變異數組(
push
、pop
、shift
、unshift
、splice
、sort
、reverse
)的hack
方法來解決問題。vue
Object.defineProperty
的第一個缺陷,沒法監聽數組變化。 然而Vue的文檔提到了Vue是能夠檢測到數組變化的,可是隻有如下八種方法,vm.items[indexOfItem] = newValue
這種是沒法檢測的。java
這種說法是有問題的,事實上,Object.defineProperty
自己是能夠監控到數組下標的變化的,只是在 Vue 的實現中,從性能/體驗的性價比考慮,放棄了這個特性。git
下面咱們經過一個例子來爲 Object.defineProperty
正名:es6
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
方法。github
getter
方法, 設置某個值會觸發
setter
方法。
接下來,咱們再試一下數組的一些操做方法,看看是否會觸發。segmentfault
push
並未觸發 setter
和 getter
方法,數組的下標能夠看作是對象中的 key
,這裏 push
以後至關於增長了下索引爲3的元素,可是並未對新的下標進行 observe
,因此不會觸發。數組
我擦,發生了什麼?瀏覽器
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
才能夠。
當移除的元素爲引用爲2的元素時,會觸發 getter
。
刪除了索引爲2的元素後,再去修改或獲取它的值時,不會再觸發 setter
和 getter
。
這和對象的處理是一樣的,數組的索引被刪除後,就至關於對象的屬性被刪除同樣,不會再去觸發 observe
。
到這裏,咱們能夠簡單的總結一下結論。
Object.defineProperty
在數組中的表現和在對象中的表現是一致的,數組的索引就能夠看作是對象中的 key
。
getter
和 setter
方法push
或 unshift
會增長索引,對於新增長的屬性,須要再手動初始化才能被 observe
。pop
或 shift
刪除元素,會刪除並更新索引,也會觸發 setter
和 getter
方法。因此,Object.defineProperty
是有監控數組下標變化的能力的,只是vue2.x放棄了這個特性。
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
對數組和對象的表現是一致的,那麼它和 Proxy
對比存在哪些優缺點呢?
因爲 Object.defineProperty
只能對屬性進行劫持,須要遍歷對象的每一個屬性。而 Proxy
能夠直接代理對象。
因爲 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
。
完美!!!
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)
。
Proxy
做爲新標準,長遠來看,JS引擎會繼續優化 Proxy
,但 getter
和 setter
基本不會再有針對性優化。
能夠看到,Proxy
對於IE瀏覽器來講簡直是災難。
而且目前並無一個完整支持 Proxy
全部攔截方法的Polyfill方案,有一個google編寫的 proxy-polyfill 也只支持了 get,set,apply,construct 四種攔截,能夠支持到IE9+和Safari 6+。
Object.defineProperty
對數組和對象的表現一直,並不是不能監控數組下標的變化,vue2.x中沒法經過數組索引來實現響應式數據的自動更新是vue自己的設計致使的,不是 defineProperty
的鍋。
Object.defineProperty
和 Proxy
本質差異是,defineProperty
只能對屬性進行劫持,新增屬性須要手動 Observe
的問題。
Proxy
做爲新標準,瀏覽器廠商勢必會對其進行持續優化,但它的兼容性也是塊硬傷,而且目前尚未完整的polifill方案。
developer.mozilla.org/zh-CN/docs/…
es6.ruanyifeng.com/#docs/proxy
歡迎關注個人公衆號「前端小苑」,我會按期在上面更新原創文章。