其實這個問題很多文章都有寫,也是面試的高頻題目,這裡僅僅是記錄下自己的理解。
Proxy
和Object.defineproperty
的區別
Object.defineProperty
只能劫持物件的屬性,對於巢狀的物件還需要進行深度的遍歷;而Proxy
是直接代理整個物件Object.defineProperty
對新增的屬性需要手動的Observe(使用$set);Proxy
可以攔截到物件新增的屬性,陣列的push
、shift
、splice
也能攔截到Proxy
具有13種攔截操作,這是defineProperty
不具有的Proxy
相容性差IE
瀏覽器不支援很多種Proxy
的方法 目前還沒有完整的polyfill
方案
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) {
// 遞迴的getter setter
defineReactive(data, key, data[key])
})
}
Proxy
的寫法:
let proxyObj = new Proxy(data, {
get(key) {
return data[key]
},
set(key, value) {
data[key] = value
}
})
當然還有其他的屬性,這裡寫最簡單的。
這兩個方法的區別讓我想到了事件代理
<ul id="ul">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
如果沒有使用事件代理,那麼它會給ul
下的每個li
繫結事件,這樣寫有個問題就是,新增的li
是沒有事件的,事件沒有一起新增進去。
如果是使用事件代理,那麼新新增的子節點也會有事件響應,因為它是通過觸發代理節點(父節點 冒泡)來觸發事件的
非常類似,這裡想要說明的是:defineProperty
是在本身自己的物件屬性上做getter/setter
, 而Proxy
返回的是一個代理物件,只有修改代理物件才會發生響應式,如果修改原來的物件屬性,並不會產生響應式更新.
Object.defineProperty
對陣列的處理
查閱vue
的官方文件 我們能看到:
Vue 不能檢測以下陣列的變動:
1、當你利用索引直接設定一個陣列項時,例如:
vm.items[indexOfItem] = newValue
2、當你修改陣列的長度時,例如:vm.items.length = newLength
對於第一點:
有一些文章直接寫
Object.defineProperty
有一個缺陷是無法監聽到陣列的變化,導致直接通過陣列的下標給陣列設定值,不能實時響應
這種說法是錯誤的,事實上Object.defineProperty
是可以監聽到陣列下標的變化,只是在Vue
的實現中,從效能/體驗的價效比考慮,放棄了這個特性.
對於陣列下的索引是可以用getter/setter
的,
但是vue為什麼沒這麼做?如果監聽索引值,通過push
或unshift
新增進來的元素的索引還沒被劫持,也不是響應式的,需要手動的進行observe
,通過pop
或shift
刪除元素,會刪除並更新索引,也能觸發響應式,但是陣列經常會被遍歷,會觸發很多次索引的getter 效能不是很好。
對於第二點:
MDN:
陣列的 length 屬性重定義是可能的,但是會受到一般的重定義限制。(length 屬性初始為 non-configurable,non-enumerable 以及 writable。對於一個內容不變的陣列,改變其 length 屬性的值或者使它變為 non-writable 是可能的。但是改變其可列舉性和可配置性或者當它是 non-writable 時嘗試改變它的值或是可寫性,這兩者都是不允許的。)然而,並不是所有的瀏覽器都允許 Array.length 的重定義。
所以對於陣列的length
,無法對它的訪問器屬性進行get
和set
,所以沒法進行響應式的更新.
這裡注意下有兩個概念:索引 和 下標
陣列有下標,但是對應的下標可能沒有索引值!
arr = [1,2]
arr.length = 5
arr[4] // empty 下標為4,值為empty,索引值不存在。 for..in 不會遍歷出索引值不存在的元素
手動賦值length
為一個更大的值,此時長度會更新,但是對應的索引不會被賦值,也就是物件的屬性沒有,defineProperty
無法處理對未知屬性的監聽,舉個例子:length = 5
的陣列,未必索引就有4,這個索引(屬性)不存在,就沒法setter
了。
陣列的索引跟物件的鍵表現其實是一致的.
vue
對陣列進行了單獨處理, 對其進行劫持重寫,
看一個陣列劫持的demo
:
const arrayProto = Array.prototype
// 以arrayProto為原型的空物件
const arrayMethods = Object.create(arrayProto)
const methodToPatch = ['push', 'splice']
methodToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
console.log('劫持hh')
return result
})
})
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
configurable: true,
writable: true
})
}
let arr = [1,2,3]
arr.__proto__ = arrayMethods
arr.push(4)
// 輸出
// 劫持hh
// 4
我們以陣列為原型建立了一個空物件arrayMethods
, 並在其上面定義了要劫持的陣列,我們這個只是簡單的列印了一句。改變arr
的原型指向(給__proto__
賦值),在arr
操作push,splice
時會走劫持的方法。 vue
的陣列劫持實際上是在劫持方法裡面新增了響應式的邏輯.
function mutator(...args) {
// cache original method
const original = arrayProto[method]
// obj key, val, enumerable
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
//eg: push(a) inserted = [a] // 為push的值新增Oberserve響應監聽
inserted = args
break
case 'splice':
// eg: splice(start,deleteCount,...items) inserted = [items] // 為新新增的值新增Oberserve響應監聽
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
$set 手動新增響應式 原理
對於物件新增屬性/陣列新增元素,無法觸發響應式,我們可以用vue $set
進行處理
vm.$set(obj,key,value)
對於陣列還能使用splice
方法:
vm.items.splice(indexOfItem, 1, newValue)
但是它們本質是一樣的!
set的實現核心就是:
- 如果是陣列,會使用
splice
對元素進行手動observe
- 如果是物件
如果是修改存在的key,直接賦值就會觸發響應式更新
如果是新增的key, 就對key進行手動observe
- 如果不是響應式的物件(響應式物件有__ob__ 屬性) 就直接賦值
set的內部實現:
export function set (target: Array<any> | Object, key: any, val: any): any {
// 如果 set 函式的第一個引數是 undefined 或 null 或者是原始型別值,那麼在非生產環境下會列印警告資訊
// 這個api本來就是給物件與陣列使用的
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 類似$vm.set(vm.$data.arr, 0, 3)
// 修改陣列的長度, 避免索引>陣列長度導致splcie()執行有誤
target.length = Math.max(target.length, key)
// 利用陣列的splice變異方法觸發響應式, 這個前面講過
target.splice(key, 1, val)
return val
}
// target為物件, key在target或者target.prototype上。
// 同時必須不能在 Object.prototype 上
// 直接修改即可, 有興趣可以看issue: https://github.com/vuejs/vue/issues/6845
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 以上都不成立, 即開始給target建立一個全新的屬性
// 獲取Observer例項
const ob = (target: any).__ob__
// Vue 例項物件擁有 _isVue 屬性, 即不允許給Vue 例項物件新增屬性
// 也不允許Vue.set/$set 函式為根資料物件(vm.$data)新增屬性
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// target本身就不是響應式資料, 直接賦值
if (!ob) {
target[key] = val
return val
}
// ---->進行響應式處理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
參考:
https://www.zhihu.com/questio...
https://www.javascriptc.com/3...
https://juejin.cn/post/684490...