爲何defineProperty不能檢測到數組長度的「變化」

目錄

  • 對象的屬性類型
  • 數組長度與索引
  • vue對數組方法的hack

屬性類型

咱們知道對象是一個無序屬性集合,建立一個包含屬性的對象有3種方式:vue

  • 構造函數
  • 字面量
  • defineProperty
var object1 = new Object()
object1.name = 'a'

var object2 = {}
object2.name = 'b'

var object3 = {}
Object.defineProperty(object3, 'name', {
  enumerable: true,
  configurable: true,
  get() {
    return 'c'
  },
  set() {
    // do
  }
})
複製代碼

區別咱們先講完屬性類型後再來看。git

屬性類型分爲github

  • 數據屬性
  • 訪問器屬性

ECMA規範中定義放在2對方括號中的屬性表示內部屬性數組

相同點,都有bash

  • [[Configurable]] 字面理解是表示屬性是否可配置——可否修改屬性;可否經過delete刪除屬性;可否把屬性修改成訪問器屬性。
  • [[Enumerable]]可否經過for-in循環返回該屬性。

區別閉包

  • 數據屬性
    • [[Writable]]是否可寫
    • [[Value]] 屬性的值
  • 訪問器屬性
    • [[Get]]取值函數
    • [[Set]]賦值函數

接着來看屬性建立的區別app

  • 第一、第2種對於屬性的賦值是同樣的,不一樣的是建立對象的方式。在使用object.name賦值的時候,咱們實際上是對數據屬性[[Value]]賦值,取值也是同樣
  • 經過第3種建立的對象,在對object.name取值賦值時,是經過訪問器屬性的[[Get]][[Set]]函數

使用defineProperty注意點函數

// 假設咱們想修改a的值爲123
var object = { a: 1 }
Object.defineProperty(object, 'a', {
  enumerable: true,
  configurable: true,
  get() {
    // 不能在函數中引用屬性a,不然會形成循環引用
    // 錯誤
    return this.a + '23'
    // 正確
    return val + '23'
  },
  set(newVal) {
    // 爲了在原屬性值的基礎上修改屬性,咱們能夠利用閉包的特性
    // 在初始化對象的時候會調用set函數,此時將屬性(例如a)的值用閉包保存起來
    // 接着取值的時候,就利用閉包中變量的值修改便可
    val = newVal
  }
})
// 其實也就是一個先賦值再取值修改的過程
複製代碼

以上有感於vue早期源碼學習系列之一:如何監聽一個對象的變化學習

數組長度與索引

咱們知道vue對於監測數組的變化重寫了數組的原型以達到目的,緣由是defineProperty不能檢測到數組長度的變化,準確的說是經過改變length而增長的長度不能監測到。ui

咱們須要理解2個概念,即數組長度與數組索引

數組的length屬性,被初始化爲

enumberable: false
configurable: false
writable: true
複製代碼

也就是說,試圖去刪除和修改(並不是賦值)length屬性是行不通的。

5b0c02cfac502e0062ea9d9d

數組索引是訪問數組值的一種方式,若是拿它和對象來比較,索引就是數組的屬性key,它與length是2個不一樣的概念。

var a = [a, b, c]
a.length = 10
// 只是顯示的給length賦值,索引3-9的對應的value也會賦值undefined
// 可是索引3-9的key都是沒有值的
// 咱們能夠用for-in打印,只會打印0,1,2
for (var key in a) {
  console.log(key) // 0,1,2
}
複製代碼

當咱們給數組push值後,會給length賦值

length 和數字下標之間的關係 —— JavaScript 數組的 length 屬性和其數字下標之間有着緊密的聯繫。數組內置的幾個方法(例如 join、slice、indexOf 等)都會考慮 length 的值。另外還有一些方法(例如 push、splice 等)還會改變 length 的值。

這幾個內置的方法在操做數組時,都會改變length的值,分2種狀況

  • 減小值
    • 當咱們shift一個數組時,你會發現它會遍歷數組(下面有代碼印證),此時數組的索引對應的值獲得了相應的更新,這種狀況下defineProperty是能夠監測到的,由於有屬性(索引)存在。
  • 增長值
    • push值時,此時數組的長度會+1,索引也會+1,可是此時的索引是新增的,雖然defineProperty不能監測到新增的屬性,可是在vue中,新增的對象屬性能夠顯示的調用vm.$set來添加監聽
    • 手動賦值length爲一個更大的值,此時長度會更新,可是對應的索引不會被賦值,也就是對象的屬性沒有,defineProperty再牛逼也沒辦法處理對未知屬性的監聽。

驗證數組的幾個內部方法對索引的影響

// 仍是老套路,定義一個observe方法
function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} val: ${val}`)
      return val
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} val: ${newVal}`)
      // 還記得咱們上面討論的閉包麼
      // 此處將新的值賦給val,保存在內存中,從而達到賦值的效果
      val = newVal
    }
  })
}
function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key])
  })
}

let test = [1, 2, 3]
// 初始化
observe(test)
複製代碼

console.log時,你會發如今打印的過程當中是遍歷這個數組的

5b0cac2cd50eee008930bc0d

打印的過程能夠理解爲

  • 找到test變量指向的內存位置爲一個數組,長度爲3並打印,但並不知道索引對應的值是多少
  • 遍歷索引

接下來咱們作以下操做

5b0cae8efb4ffe005b06d343

  • push時,新增了索引並改變了長度,但新的索引未被observe
  • 修改新的索引對應的值
  • 彈出新的索引對應的值
  • 彈出索引被observe的值時,觸發了get
  • 此時再去給原索引賦值時,發現並無觸發被observe的set,因而可知數組索引被刪除後就不會被observe到了,那對象的屬性是否也是同樣的呢?以下圖可見也是同樣的
    5b0cb0772f301e0038b29fd2
  • 修改索引爲1的值,觸發了set
  • unshift時,會將索引爲0和1的值遍歷出來存放,而後從新賦值

當咱們給length賦值時,能夠看見並不會遍歷數組去賦值索引。

5b0cb00f9f54540043d30ab7

小結
對於defineProperty來講,處理數組與對象是一視同仁的,只是在初始化時去改寫getset達到監測數組或對象的變化,對於新增的屬性,須要手動再初始化。對於數組來講,只不過特別了點,push、unshift值也會新增索引,對於新增的索引也是能夠添加observe從而達到監聽的效果;pop、shift值會刪除更新索引,也會觸發defineProperty的get和set。對於從新賦值length的數組,不會新增索引,由於不清楚新增的索引有多少,根據ecma規範定義,索引的最大值爲2^32 - 1,不可能循環去賦值索引的。

以上參考

引起我對這個問題的思考是

對我有所幫助是知乎@liuqipeng的回答

vue對數組方法的hack

vue對數組的observe單獨作了處理

if (Array.isArray(value)) {
  const augment = hasProto
    ? protoAugment
    : copyAugment
  // 判斷數組實例是否有__proto__屬性,有就用protoAugment
  // 而protoAugment司機就是重寫實例的__proto__
  // target.__proto__ = src
  // 將新的arrayMethods重寫到value上
  augment(value, arrayMethods, arrayKeys)
  // 而後初始化observe已存在索引的值
  this.observeArray(value)
} else {
  this.walk(value)
}
複製代碼

再來看如何重寫的arrayMethods,在array.js中,咱們能夠看到

const arrayProto = Array.prototype
// 複製了數組構造函數的原型
// 這裏須要注意的是數組構造函數的原型也是個數組
// 實例中指向原型的指針__proto__也是個數組
// 數組並無索引,由於length = 0
// 相反的擁有屬性,屬性名爲數組方法,值爲對應的函數
export const arrayMethods = Object.create(arrayProto)

// 對如下方法重寫
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
複製代碼

以下圖,當我給__proto__索引爲0賦值時,是正常的,可是其他的屬性依舊在後面。咱們能夠這樣認爲,數組的構造函數的原型是個空數組,可是默認給你內置了幾個方法。

5b0cfd0367f356003b7ae87c

咱們再來看爲何只對這些方法重寫?

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // 這裏的def很重要,其實也就是用object.defineProperty從新定義屬性
  // 但這裏的arrayMethods是個數組,這就是爲何上面咱們解釋
  // 數組構造函數原型是個空數組可是默認了屬性方法
  // 因此這裏的定義是很巧妙的
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    // ob就是observe實例
    const ob = this.__ob__
    let inserted
    switch (method) {
      // 爲何對push和unshift單獨處理?
      // 咱們在上看解釋過,這2中方法會增長數組的索引,可是新增的索引位須要手動observe的
      case 'push':
      case 'unshift':
        inserted = args
        break
      // 同理,splice的第三個參數,爲新增的值,也須要手動observe
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 其他的方法都是在原有的索引上更新,初始化的時候已經observe過了
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 而後通知全部的訂閱者觸發回調
    ob.dep.notify()
    return result
  })
})
複製代碼

最後,仍是貼一波博客地址爲何defineProperty不能檢測到數組長度的「變化」

相關文章
相關標籤/搜索