前言
距離國慶假期尤大發布vue3
前瞻版本發佈已經有一個月的時間,大家都知道在vue2x
版本中的響應式數據更新是用的defineProperty
這個API。
在vue2
中,針對Object
和Array
兩種數據類型採用了兩種不同的處理方式。
對於Object
類型,通過Object.defineProperty
通過getter/setter
遞歸偵測所有對象的key
,實現深度偵測
對於Array
類型,通過攔截Array
原型上的幾個操作實現了對數組的響應式,但是存在一些問題。
總之,通過defineProperty
這種方式存在一定的性能問題
爲了解決這個問題,從很早之前vue3
就計劃將採用ES6 Proxy
代理的方式實現數據的響應式。(IE不支持這個API,所以vue3也不支持IE11了,垃圾IE)
關於Proxy
可以先查看MDN Proxy詳細用法。
這裏主要講一下基本語法
const obj = new Proxy(target,{
// 獲取對象屬性會走這裏
get(target, key, receiver){},
// 修改對象屬性會走這裏
set(target, key, value, receiver){},
// 刪除對象上的方法會走這裏
deleteProperty(target,key){}
})
嘗試使用一下Proxy
這個API,嘗試幾種用法,發現一些問題
- 代理普通對象
const obj = {
name: 'ahwgs',
age: 22,
}
const res = new Proxy(obj, {
// 獲取對象屬性會走這裏
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改對象屬性會走這裏
set(target, key, value, receiver) {
console.log('set', target[key])
target[key] = value
return true
},
// 刪除對象上的方法會走這裏
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
const n = res.name
res.age = 23
console.log(obj)
// get { name: 'ahwgs', age: 22 } name
// set 22
// { name: 'ahwgs', age: 23 }
- 代理數組
// const obj = {
// name: 'ahwgs',
// age: 22,
// }
const obj = [1, 2, 3]
const res = new Proxy(obj, {
// 獲取對象屬性會走這裏
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改對象屬性會走這裏
set(target, key, value, receiver) {
console.log('set', target[key])
target[key] = value
return true
},
// 刪除對象上的方法會走這裏
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
res.push(4)
console.log(obj)
// get [ 1, 2, 3 ] push
// get [ 1, 2, 3 ] length
// set undefined
// set 4
// [ 1, 2, 3, 4 ]
代理數組的時候發現了一個問題,get
調用的兩次,一次是push
一次是length
這兩個都是數組自身的屬性
那麼vue3中是如何解決這個問題的呢?
- 代理深層次對象
const obj = {
name: 'ahwgs',
age: 22,
arr: [1, 2, 3],
}
const res = new Proxy(obj, {
// 獲取對象屬性會走這裏
get(target, key, receiver) {
console.log('get', target, key)
return target[key]
},
// 修改對象屬性會走這裏
set(target, key, value, receiver) {
console.log('set', target, key)
target[key] = value
return true
},
// 刪除對象上的方法會走這裏
deleteProperty(target, key) {
console.log('deleteProperty', target[key])
},
})
res.arr.push(4)
console.log(obj)
// get { name: 'ahwgs', age: 22, arr: [ 1, 2, 3 ] } arr
// { name: 'ahwgs', age: 22, arr: [ 1, 2, 3, 4 ] }
發現並沒有執行set邏輯,並沒有代理到第二層級的對象,那麼vue中是如何做到深層次的代理的呢?
解決問題
上面的代碼我們遇到了兩個問題:
- 多次觸發了
get/set
- 無法代理深層級的對象
我們手寫一個簡單的vue3
嘗試解決上面這些問題,具體看下述代碼:
const toProxy = new WeakMap() // 存放的是代理後的對象
const toRaw = new WeakMap() // 存放的是代理前的對象
function isObject(target) {
// 這裏爲什麼!==null 因爲typeof null =object 這是js的一個bug
return typeof target === 'object' && target !== null;
}
// 模擬UI更新
function trigger() {
console.log('UI更新了!!');
}
// 判斷key是否是val的私有屬性
function hasOwn(val, key) {
const { hasOwnProperty } = Object.prototype
return hasOwnProperty.call(val, key)
}
// 數據代理
// target是要代理的對象,vue中data()return的那個對象
function reactive(target) {
// 先判斷如果不是對象 不需要做代理 直接返回
if (!isObject(target)) return target;
// 如果代理表中已經存在 就不需要再次代理 直接返回已存在的代理對象
const proxy = toProxy.get(target)
if (proxy) return proxy
// 如果傳入的對象被代理過
if (toRaw.has(target)) return target
const handler = {
set(tar, key, value, receiver) {
// 觸發更新
// 如果觸發的是私有屬性的話纔去更新視圖 用以解決類似於數組操作中多次set的問題
if (hasOwn(target, key)) {
trigger()
}
// 這裏使用ES6 Reflect 爲Proxy設置一些屬性
// 用於簡化自定義的一些方法
return Reflect.set(tar, key, value, receiver)
},
get(tar, key, receiver) {
const res = Reflect.get(tar, key, receiver)
// 判斷當前修改的值是否是否是對象 如果是對象的話 遞歸再次代理 解決深層級代理的問題
if (isObject(tar[key])) {
return reactive(res)
}
return res
},
deleteProperty(tar, key) {
return Reflect.deleteProperty(tar, key)
},
}
// 被代理的對象
const observed = new Proxy(target, handler)
// 將代理過的對象 放入緩存中
// 防止代理過的對象再次被代理
// WeekMap因爲的key是弱引用關係,涉及到垃圾回收機制,要比Map的效率高
toProxy.set(target, observed) // 源對象 : 代理後的結果
toRaw.set(observed, target) //
return observed
}
const data = {
name: 'ahwgs',
age: 22,
list: [1, 2, 3],
}
let user = reactive(data)
user = reactive(data)
user = reactive(data)
user.list.push(4)
針對上面的幾個問題做以下解釋:
- 多次觸發了
get/set
通過hasOwn
這個方法,判斷當前修改的屬性是否是私有屬性,如果是的話纔去更新視圖。
對於這一點,源碼中是這樣做的:
// 判斷是否有
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
判斷要set
的key
是否是存在的,如果是存在的就去更新視圖(trigger
方法),如果不是的話往視圖中新增
- 無法代理深層級的對象
通過在get
方法中判斷當前的值是否是對象,如果是對象的話再去代理一次,做一個遞歸的操作
對於源碼中是這樣的:
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
總結
- 整體是通過
ES6 Proxy
這個新特性去實現的響應式,並且還通過WeakWap
去緩存的整個代理數據的保存,提高響應式數據的性能 - 簡單版是這麼簡單處理的,但是源碼中對每一個細節處理的都很細緻,並且結構分明,具體可以查看https://github.com/vuejs/vue-next/tree/master/packages/reactivity/src
關於
- 本文首發於:實現一個簡化版的Vue3數據偵測