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