Object.defineProperty也能監(jiān)聽數(shù)組變化?

本文簡介

點(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ǔ)用法

Object.defineProperty() 文檔

關(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.definePropertyset 里面我有調(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ù)組的方法。

大概的思路和編碼流程順序如下:

  1. 判斷要監(jiān)聽的數(shù)據(jù)是否為數(shù)組
  2. 是數(shù)組的情況,就將數(shù)組模擬成一個對象
  3. 將數(shù)組的方法名綁定到新創(chuàng)建的對象中
  4. 將對應(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)

  1. 深度監(jiān)聽,需要遞歸到底,一次計算量大
  2. 無法監(jiān)聽新增屬性/刪除屬性(所以需要使用 Vue.set 和 Vue.delete)
  3. 無法原生監(jiān)聽數(shù)組,需要特殊處理


所以在 Vue 3 中,把 Object.defineProperty 改成 Proxy 。

Proxy 的缺點(diǎn)也很明顯,就是兼容性問題。所以需要根據(jù)你的項目來選擇用 Vue 2 還是 Vue 3 。



推薦閱讀

??《『Three.js』起飛!》

??《Vue3 過10種組件通訊方式》

??《Fabric.js 從入門到目中無人》

??《Vite 搭建 Vue2 項目(Vue2 + vue-router + vuex)》

??《console.log也能插圖!》

點(diǎn)贊 + 關(guān)注 + 收藏 = 學(xué)會了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容