本文簡介
點(diǎn)贊 + 關(guān)注 + 收藏 = 學(xué)會了
首先,解答一下標(biāo)題:Object.defineProperty 不能監(jiān)聽原生數(shù)組的變化。如需監(jiān)聽數(shù)組,要將數(shù)組轉(zhuǎn)成對象。
在 Vue2 時是使用了 Object.defineProperty 監(jiān)聽數(shù)據(jù)變化,但我查了下 文檔,發(fā)現(xiàn) Object.defineProperty 是用來監(jiān)聽對象指定屬性的變化。沒有看到可以監(jiān)聽個數(shù)組變化的。
但 Vue2 有的確能監(jiān)聽到數(shù)組某些方法改變了數(shù)組的值。本文的目標(biāo)就是解開這個結(jié)。
基礎(chǔ)用法
關(guān)于 Object.defineProperty() 的用法,可以看官方文檔。
基礎(chǔ)部分本文只做簡單的講解。
語法
Object.defineProperty(obj, prop, descriptor)
參數(shù)
-
obj要定義屬性的對象。 -
prop要定義或修改的屬性的名稱或Symbol。 -
descriptor要定義或修改的屬性描述符。
const data = {}
let name = '雷猴'
Object.defineProperty(data, 'name', {
get() {
console.log('get')
return name
},
set(newVal) {
console.log('set')
name = newVal
}
})
console.log(data.name)
data.name = '鯊魚辣椒'
console.log(data.name)
console.log(name)
上面的代碼會輸出
get
雷猴
set
鯊魚辣椒
鯊魚辣椒
上面的意思是,如果你需要訪問 data.name ,那就返回 name 的值。
如果你想設(shè)置 data.name ,那就會將你傳進(jìn)來的值放到變量 name 里。
此時再訪問 data.name 或者 name ,都會返回新賦予的值。
還有另一個基礎(chǔ)用法:“凍結(jié)”指定屬性
const data = {}
Object.defineProperty(data, 'name', {
value: '雷猴',
writable: false
})
data.name = '鯊魚辣椒'
delete data.name
console.log(data.name)
這個例子,把 data.name 凍結(jié)住了,不管你要修改還是要刪除都不生效了,一旦訪問 data.name 都一律返回 雷猴 。
以上就是 Object.defineProperty 的基礎(chǔ)用法。
深度監(jiān)聽
上面的例子是監(jiān)聽基礎(chǔ)的對象。但如果對象里還包含對象,這種情況就可以使用遞歸的方式。
遞歸需要創(chuàng)建一個方法,然后判斷是否需要重復(fù)調(diào)用自身。
// 觸發(fā)更新視圖
function updateView() {
console.log('視圖更新')
}
// 重新定義屬性,監(jiān)聽起來(核心)
function defineReactive(target, key, value) {
// 深度監(jiān)聽
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度監(jiān)聽
observer(newValue)
// 設(shè)置新值
// 注意,value 一直在閉包中,此處設(shè)置完之后,再 get 時也是會獲取最新的值
value = newValue
// 觸發(fā)視圖更新
updateView()
}
}
})
}
// 深度監(jiān)聽
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是對象或數(shù)組
return target
}
// 重新定義各個屬性(for in 也可以遍歷數(shù)組)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 準(zhǔn)備數(shù)據(jù)
const data = {
name: '雷猴'
}
// 開始監(jiān)聽
observer(data)
// 測試1
data.name = {
lastName: '鯊魚辣椒'
}
// 測試2
data.name.lastName = '蟑螂惡霸'
上面這個例子會輸出2次“視圖更新”。
我創(chuàng)建了一個 updateView 方法,該方法模擬更新 DOM (類似 Vue的操作),但我這里簡化成只是輸出 “視圖更新” 。因為這不是本文的重點(diǎn)。
測試1 會觸發(fā)一次 “視圖更新” ;測試2 也會觸發(fā)一次。
因為在 Object.defineProperty 的 set 里面我有調(diào)用了一次 observer(newValue) , observer 會判斷傳入的值是不是對象,如果是對象就再次調(diào)用 defineReactive 方法。
這樣可以模擬一個遞歸的狀態(tài)。
以上就是 深度監(jiān)聽 的原理,其實就是遞歸。
但遞歸有個不好的地方,就是如果對象層次很深,需要計算的量就很大,因為需要一次計算到底。
監(jiān)聽數(shù)組
數(shù)組沒有 key ,只有 下標(biāo)。所以如果需要監(jiān)聽數(shù)組的內(nèi)容變化,就需要將數(shù)組轉(zhuǎn)換成對象,并且還要模擬數(shù)組的方法。
大概的思路和編碼流程順序如下:
- 判斷要監(jiān)聽的數(shù)據(jù)是否為數(shù)組
- 是數(shù)組的情況,就將數(shù)組模擬成一個對象
- 將數(shù)組的方法名綁定到新創(chuàng)建的對象中
- 將對應(yīng)數(shù)組原型的方法賦給自定義方法
代碼如下所示
// 觸發(fā)更新視圖
function updateView() {
console.log('視圖更新')
}
// 重新定義數(shù)組原型
const oldArrayProperty = Array.prototype
// 創(chuàng)建新對象,原形指向 oldArrayProperty,再擴(kuò)展新的方法不會影響原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function() {
updateView() // 觸發(fā)視圖更新
oldArrayProperty[methodName].call(this, ...arguments)
}
})
// 重新定義屬性,監(jiān)聽起來(核心)
function defineReactive(target, key, value) {
// 深度監(jiān)聽
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度監(jiān)聽
observer(newValue)
// 設(shè)置新值
// 注意,value 一直在閉包中,此處設(shè)置完之后,再 get 時也是會獲取最新的值
value = newValue
// 觸發(fā)視圖更新
updateView()
}
}
})
}
// 監(jiān)聽對象屬性(入口)
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是對象或數(shù)組
return target
}
// 數(shù)組的情況
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定義各個屬性(for in 也可以遍歷數(shù)組)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 準(zhǔn)備數(shù)據(jù)
const data = {
nums: [10, 20, 30]
}
// 監(jiān)聽數(shù)據(jù)
observer(data)
data.nums.push(4) // 監(jiān)聽數(shù)組
上面的代碼之所以沒有直接修改數(shù)組的方法,如
Array.prototype.push = function() {
updateView()
...
}
因為這樣會污染原生 Array 的原型方法,這樣做會得不償失。
以上就是使用 Object.defineProperty 的方法。
如需監(jiān)聽更多方法,可以在數(shù)組 ['push', 'pop', 'shift', 'unshift', 'splice'] 中添加。
綜合代碼
// 深度監(jiān)聽
function updateView() {
console.log('視圖更新')
}
// 重新定義數(shù)組原型
const oldArrayProperty = Array.prototype
// 創(chuàng)建新對象,原形指向 oldArrayProperty,再擴(kuò)展新的方法不會影響原型
const arrProto = Object.create(oldArrayProperty);
// arrProto.push = function () {}
// arrProto.pop = function() {}
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function() {
updateView() // 觸發(fā)視圖更新
oldArrayProperty[methodName].call(this, ...arguments)
}
})
// 重新定義屬性,監(jiān)聽起來(核心)
function defineReactive(target, key, value) {
// 深度監(jiān)聽
observer(value)
// 核心 API
// Object.defineProperty 不具備監(jiān)聽數(shù)組的能力
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度監(jiān)聽
observer(newValue)
// 設(shè)置新值
// 注意,value 一直在閉包中,此處設(shè)置完之后,再 get 時也是會獲取最新的值
value = newValue
// 觸發(fā)視圖更新
updateView()
}
}
})
}
// 監(jiān)聽對象屬性(入口)
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是對象或數(shù)組
return target
}
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定義各個屬性(for in 也可以遍歷數(shù)組)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
總結(jié)
上面的代碼主要是模擬 Vue 2 監(jiān)聽數(shù)據(jù)變化,雖然好用,但也有缺點(diǎn)。
缺點(diǎn)
- 深度監(jiān)聽,需要遞歸到底,一次計算量大
- 無法監(jiān)聽新增屬性/刪除屬性(所以需要使用 Vue.set 和 Vue.delete)
- 無法原生監(jiān)聽數(shù)組,需要特殊處理
所以在 Vue 3 中,把 Object.defineProperty 改成 Proxy 。
但 Proxy 的缺點(diǎn)也很明顯,就是兼容性問題。所以需要根據(jù)你的項目來選擇用 Vue 2 還是 Vue 3 。
推薦閱讀
??《Vite 搭建 Vue2 項目(Vue2 + vue-router + vuex)》
點(diǎn)贊 + 關(guān)注 + 收藏 = 學(xué)會了