Как отслеживать изменения массива?

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

Происхождение: в привязке данных Vue отслеживаются изменения свойств объекта, а соответствующие обновления представления выполняются посредством сбора зависимостей и т. д.

Вопрос: Можно ли контролировать все типы изменения свойств объекта?

использовался раньшеObject.definePropertyчерез объектgetter/setterпросто достичьОтслеживание изменений свойств объектаи просмотрите зависимости, чтобы выполнить соответствующую обработку зависимостей.

Однако это проблематично, особенно когда значением свойства в объекте является массив. Как сказано в документации Vue:

Из-за ограничений JavaScript Vue не может обнаружить следующие изменения массива:

  1. Когда вы устанавливаете элемент напрямую с помощью индекса, например.vm.items[indexOfItem] = newValue
  2. Когда вы изменяете длину массива, например.vm.items.length = newLength

отИсходный код VueВы также можете видеть, что массив действительно обрабатывается особым образом. Причина в том,Версии ES5 и ниже не могут обеспечить идеальное наследование массивов..

Эксперимент?

написано раньшеobserveБыл проведен простой эксперимент:

import { observe } from './mvvm'
const data = {
  name: 'Jiang',
  userInfo: {
    gender: 0
  },
  list: []
}
// 此处直接使用了前面写好的 getter/setter
observe(data)
data.name = 'Solo'
data.userInfo.gender = 1
data.list.push(1)
console.log(data)

Результат таков:

По результатам видно проблемуdataЗначения атрибутов name, userInfo и list изменились, но изменения в списке массивов не произошли.observeконтролируется. какова причина? Проще говоря, метод манипулирования массивом, то естьArray.prototypeМетод, установленный выше, не запускает установщик свойства, потому что свойство не имеет операции присваивания.

Как решить эту проблему?

Способ решения этой проблемы во Vue — переписать общие методы массивов, а методы массивов после упаковки можно будет отслеживать при их вызове.

Здесь метод, который, как я думаю, похож на него, вероятно, состоит в том, чтобы перехватить работу массива через цепочку прототипов, чтобы реализовать мониторинг поведения при работе с массивом.

Реализация выглядит следующим образом:

// 让 arrExtend 先继承 Array 本身的所有属性
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
/**
 * arrExtend 作为一个拦截对象, 对其中的方法进行重写
 */
arrMethods.forEach(method => {
  const oldMethod = Array.prototype[method]
  const newMethod = function(...args) {
    oldMethod.apply(this, args)
    console.log(`${method}方法被执行了`)
  }
  arrExtend[method] = newMethod
})

export default {
  arrExtend
}

нуждаться вdefineReactiveКод, добавленный в функцию:

if (Array.isArray(value)) {
    value.__proto__ = arrExtend
 }

есть тест:data.list.push(1)

Посмотрим на результаты:

Логика приведенного выше кода понятна с первого взгляда, и это также упрощение идей реализации в Vue. БудуarrExtendЭтот объект действует как перехватчик. Сначала пусть этот объект наследуетArrayВсе атрибуты самого себя, чтобы это не повлияло на использование других атрибутов самого массива, а затем переписать соответствующую функцию, то есть после вызова исходного метода, чтобы уведомить другие связанные зависимости, что этот атрибут изменился, что такой же какObject.definePropertyсерединаsetterОн делает почти то же самое, разница лишь в том, что его можно уточнить, какую именно операцию выполняет пользователь, меняется ли длина массива и т. д.

Есть ли другой способ?

В ES6 мы увидели освежающее свойство —Proxy. Сначала рассмотрим понятия:

Вызывая new Proxy(), вы можете создать прокси для замены другого объекта (называемого целевым), прокси виртуализирует целевой объект, поэтому прокси и целевой объект могут казаться одним и тем же объектом для обработки.

Прокси позволяют перехватывать низкоуровневые операции над целевым объектом, что является внутренней возможностью движка JS. Поведение перехвата использует функцию (называемую ловушкой), которая реагирует на определенное действие.

ProxyКак следует из названия, это означает прокси, то есть функцию, которая позволяет нам играть с объектами по желанию. когда мы проходимProxyПосле проксирования объекта мы получим объект, практически идентичный проксируемому объекту, и сможем полностью мониторить этот объект.

Что такое полный мониторинг?ProxyЭто приводит к перехвату основной операции. Ранее мы использовали его, когда реализовывали объектный мониторинг.Object.defineProperty, на самом деле это высокоуровневая операция, предоставляемая JS, то есть метод, предоставляемый после базовой инкапсуляции.ProxyСтрого говоря, мы можем напрямую перехватить базовую операцию объекта агента. Это эквивалентно достижению контроля над ним из базовой операции объекта.

Улучшить наш код?

const createProxy = data => {
  if (typeof data === 'object' && data.toString() === '[object Object]') {
    for (let k in data) {
      if (typeof data[k] === 'object') {
        defineObjectReactive(data, k, data[k])
      } else {
        defineBasicReactive(data, k, data[k])
      }
    }
  }
}

function defineObjectReactive(obj, key, value) {
  // 递归
  createProxy(value)
  obj[key] = new Proxy(value, {
    set(target, property, val, receiver) {
      if (property !== 'length') {
        console.log('Set %s to %o', property, val)
      }
      return Reflect.set(target, property, val, receiver)
    }
  })
}

function defineBasicReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: false,
    get() {
      return value
    },
    set(newValue) {
      if (value === newValue) return
      console.log(`发现 ${key} 属性 ${value} -> ${newValue}`)
      value = newValue
    }
  })
}

export default {
  createProxy
}

Для свойств базового типа в объекте мы по-прежнему передаемObject.definePropertyРеализовать отзывчивые свойства, потому что здесь нет болевой точки, а в реализации правильногоObjectПри мониторинге свойств типа я использовал создание прокси, потому что наша предыдущая болевая точка заключалась в том, что мы не могли эффективно отслеживать изменения массива. Когда мы используем этот улучшенный метод, нам больше не нужно реализовывать мониторинг операций массива, переопределяя метод массива ранее, потому что в предыдущем методе много ограничений, мы не можем охватить все операции массива, и в то же время мы также не могу ответить на что-то вродеdata.array.length = 0эта операция. После реализации через прокси все по другому. Мы можем отслеживать изменения в массиве снизу вверх. может дажеwatchК изменению длины массива и всяким более подробным вещам. Это определенно решает большую проблему.

Давайте вызовем метод прямо сейчас, попробуйте?

let data = {
  name: 'Jiang',
  userInfo: {
    gender: 0,
    movies: []
  },
  list: []
}
createProxy(data)

data.name = 'Solo'
data.userInfo.gender = 0
data.userInfo.movies.push('星际穿越')
data.list.push(1)

Результат:

Результат отличный ~ мы реализовали мониторинг всех изменений свойств объектаProxyЕсть много других операций, таких как размещение прокси в качестве прототипа в цепочке прототипов, так что вы можете отслеживать только те свойства, которые не содержит подкласс, что очень мощно.ProxyЕго можно применять более широко, и существует множество сценариев. Тоже первый раз использую, надо закрепить ( ;´Д`)