Почему defineProperty не может обнаружить «изменение» длины массива

внешний интерфейс JavaScript Vue.js

содержание

  • Тип свойства объекта
  • Длина массива и индекс
  • Взлом Vue методов массива

тип недвижимости

Мы знаем, что объект — это неупорядоченный набор свойств, и есть 3 способа создать объект, содержащий свойства:

  • Конструктор
  • буквальный
  • 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
  }
})

Давайте сначала поговорим о разнице, а затем рассмотрим типы атрибутов.

Типы атрибутов делятся на

  • атрибут данных
  • свойство доступа

Атрибуты, определенные в спецификации ECMA в 2 квадратных скобках, представляют собой внутренние атрибуты.

то же самое, есть

  • [[Configurable]]Буквальное понимание заключается в том, чтобы указать, может ли атрибут быть сконфигурирован — может ли атрибут быть изменен, может ли атрибут быть удален с помощью удаления, может ли атрибут быть изменен как атрибут доступа.
  • [[Enumerable]]может пройтиfor-inВернитесь к этому свойству.

разница

  • атрибут данных
    • [[Writable]]доступный для записи
    • [[Value]]стоимость имущества
  • свойство доступа
    • [[Get]]Функция значения
    • [[Set]]функция присваивания

Далее, давайте посмотрим на разницу между созданием атрибута

  • Первый и второй типы присваивания свойств одинаковы, разница заключается в способе создания объекта. В использованииobject.nameПри присвоении значения мы фактически присваиваем значение атрибуту данных.[[Value]]Присвоение, взять значение то же самое
  • Объект, созданный третьим видом, в паре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 не может определить изменение длины массива, если быть точнымпутем изменения длиныУвеличение длины не может быть отслежено.

Нам нужно понять 2 понятия, а именно длину массива и индекс массива.

массивlengthсвойство, инициализированное как

enumberable: false
configurable: false
writable: true

То есть попытка удалить и изменить (не присвоить) свойство длины не сработает.

5b0c02cfac502e0062ea9d9d

Индекс массива — это способ доступа к значению массива.Если вы сравниваете его с объектом, индекс — это ключ свойства массива, который представляет собой два разных понятия длины.

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
}

Когда мы помещаем значение в массив, мы присваиваем значение length

Связь между длиной и числовыми индексами- Существует тесная связь между свойством длины массива JavaScript и его числовым индексом. Некоторые встроенные методы работы с массивами (такие как join, slice, indexOf и т. д.) учитывают значение длины. Существуют также некоторые методы (такие как push, splice и т. д.), которые также изменяют значение длины.

Эти встроенные методы будут изменять значение длины при манипулировании массивом.Есть два случая.

  • Уменьшить значение
    • Когда мы сдвигаем массив, вы обнаружите, что он будет проходить по массиву (код проверен ниже), и значение, соответствующее индексу массива, обновляется соответствующим образом.В этом случае можно отслеживать defineProperty, потому что есть свойства ( индекс) существует.
  • Добавленная стоимость
    • При проталкивании значения длина массива будет +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

Процесс печати можно понимать как

  • Найдите место в памяти, на которое указывает тестовая переменная, в виде массива длиной 3 и распечатайте его, но не знаете, каково значение, соответствующее индексу.
  • указатель хода

Далее делаем следующее

5b0cae8efb4ffe005b06d343

  • При пуше добавлялся индекс и менялась длина, но нового индекса не наблюдалось
  • Измените значение, соответствующее новому индексу
  • Всплывающее значение, соответствующее новому индексу
  • Когда всплывает значение наблюдаемого индекса, запускается get
  • В этот момент, когда я присваиваю значение исходному индексу, я обнаруживаю, что наблюдаемый набор не срабатывает. Видно, что после удаления индекса массива он не будет наблюдаться. Являются ли свойства объекта одинаковыми ? То же самое можно увидеть на рисунке ниже
    5b0cb0772f301e0038b29fd2
  • Измените значение индекса 1, набор срабатывания
  • При несмещении значения с индексами 0 и 1 проходятся и сохраняются, а затем переназначаются

Когда мы присваиваем значение длине, мы видим, что мы не просматриваем массив для присвоения индекса.

5b0cb00f9f54540043d30ab7

резюме
Для defineProperty обработка массивов и объектов такая же, только переписывается во время инициализацииgetа такжеsetЧтобы отслеживать изменения в массивах или объектах, новые свойства необходимо повторно инициализировать вручную. Для массивов просто немного особенный.Значения push и unshift также добавят индекс.Для нового индекса можно также добавить наблюдение для достижения эффекта мониторинга;значения pop и shift удалит индекс обновить индекс и вызвать функцию defineProperty.get и set. Для массива, длина которого переназначается, новый индекс добавляться не будет, потому что непонятно, сколько новых индексов добавляется, согласноecmaСпецификация определяет, что максимальное значение индекса равно2^32 - 1, нельзя циклом присвоить индекс.

ссылка выше

Что заставило меня задуматься над этим вопросом

что мне помоглоЗнай почтиОтвет от @liuqipeng

Взлом Vue методов массива

Vue обрабатывает массив отдельно

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 не может обнаружить «изменение» длины массива