Мутация массива парсинга исходного кода Vue

внешний интерфейс исходный код JavaScript Vue.js

объект вне досягаемости

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

Какова причина?

причина в следующем:VueРеагирующая система основана наObject.definePropertyЭтот метод может отслеживать приобретение или изменение элемента в объекте, а данные, обрабатываемые этим методом, называются реагирующими данными. Однако у этого метода есть большой недостаток: добавление или удаление атрибутов не приведет к запуску мониторинга, например:

var vm = new Vue({
    data () {
        return {
            obj: {
                a: 1
            }
        }
    }
})
// `vm.obj.a` 现在是响应式的

vm.obj.b = 2
// `vm.obj.b` 不是响应式的

Причина в том, что вVueПри инициализацииVueвнутреннее собраниеdataВозвращаемое значение метода глубоко реагирует, чтобы сделать его чувствительными данными, поэтомуvm.obj.aотзывчив. Однако после установкиvm.obj.bне прошелVueОтзывчивое крещение при инициализации, поэтому не должно быть отзывчивым.

Так,vm.obj.bМожно ли сделать его отзывчивым? Конечно, поvm.$setМетод вполне соответствует требованиям, я не буду здесь повторять соответствующие принципы, я должен написать статью об этом позже.vm.$setОбоснование позади.

более убогий массив

Сказав так много выше, главный герой этой статьи, массив, еще не был упомянут, теперь пришло время появиться главному герою.

По сравнению с объектами ситуация с массивами еще более плачевна, взгляните на официальную документацию:

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

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

Возможно, официальная документация не очень понятна, так что продолжим давать каштан:

var vm = new Vue({
    data () {
        return {
            items: ['a', 'b', 'c']
        }
    }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

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

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

Итак, есть ли способ сломать его?

Конечно, официально предусмотрены методы массива 7. С помощью этих методов массива 7 вы можете с радостью вызвать ответ массива.7 методов массива:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

Можно обнаружить, что эти 7 методов массива кажутся собственными методами массива.Почему эти 7 методов массива могут запускать приложение и запускать обновление представления?

Вы думаете: методы массива великолепны, методы массива могут делать все, что захотят?

Сури, эти 7 методов массива действительно могут делать все, что захотят.

Потому что это методы мутированного массива.

Идея мутации массива

Что такое метод мутирующего массива?

Метод мутирующего массива заключается в расширении функции метода массива при условии, что исходная функция метода массива остается неизменной.VueТак называемое расширение функций добавляет адаптивные функции.

Метод превращения обычного массива в мутированный массив состоит из двух шагов:

  1. Расширение функций
  2. захват массива

Расширение функций

Сначала мысленный вопрос:

Существует такое требование, чтобы «HelloWorld» можно было печатать в консоли каждый раз, когда вызывается функция, без изменения исходной функции и метода вызова.

На самом деле идея очень проста, разделена на три шага:

  1. Использовать новую оригинальную функцию кеша переменных
  2. переопределить исходную функцию
  3. Вызов исходной функции во вновь определенной функции

Взгляните на конкретную реализацию кода:

function A () {
    console.log('调用了函数A')
}

const nativeA = A
A = function () {
    console.log('HelloWorld')
    nativeA()
}

Как видите, таким образом мы гарантируем, чтоAИсходя из поведения функции, ее функция была расширена.

Далее мы используем этот метод для расширения функции исходного метода массива:

// 变异方法名称
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

const arrayProto = Array.prototype
// 继承原有数组的方法
const arrayMethods = Object.create(arrayProto)

mutationMethods.forEach(method => {
    // 缓存原生数组方法
    const original = arrayProto[method]
    arrayMethods[method] = function (...args) {
        const result = original.apply(this, args)
        
        console.log('执行响应式功能')
        
        return result
    }
})

Как видно из кода, мы вызываемarrayMethodsМетоды этого объекта имеют два случая:

  1. Метод расширения функции вызова: прямой вызовarrayMethodsметод в
  2. Вызов собственного метода: в этом случае найдите собственный метод, определенный в прототипе массива, через цепочку прототипов.

С помощью описанного выше метода мы реализовали расширение функции исходного метода массива, но перед нами стоит огромная проблема: как заставить экземпляр массива вызывать метод массива, расширенный функцией?

Решение этой проблемы: захват массива.

захват массива

Перехват массива по определению заключается в наследовании исходных методов экземпляра массива после замены метода расширения нашей функции.

Вдумайтесь, мы реализовали перед собой расширенный массивarrayMethods, этот пользовательский массив наследуется от объекта массива, нам нужно только связать его с обычным экземпляром массива и позволить обычному массиву наследоваться от него.

Чтобы выполнить вышеуказанную операцию, она осуществляется через цепочку прототипов.

Метод реализации показан в следующем коде:

let arr = []
// 通过隐式原型继承arrayMethods
arr.__proto__ = arrayMethods

// 执行变异后方法
arr.push(1)

Путем расширения функций и перехвата массива мы, наконец, реализовали массив мутаций, давайте посмотримVueКак исходный код реализует массив мутаций.

Анализ исходного кода

мы приходимsrc/core/observer/index.jsв серединеObserverв классеconstructorфункция:

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 检测是否是数组
    if (Array.isArray(value)) {
        // 能力检测
        const augment = hasProto
        ? protoAugment
        : copyAugment
        // 通过能力检测的结果选择不同方式进行数组劫持
        augment(value, arrayMethods, arrayKeys)
        // 对数组的响应式处理
        this.observeArray(value)
    } else {
        this.walk(value)
    }
}

ObserverЭтот классVueОсновной компонент отзывчивой системы, основная функция на этапе инициализации — сделать целевой объект отзывчивым. Здесь мы в основном сосредоточимся на обработке массивов.

Обработка массива в основном представляет собой следующий код

// 能力检测
const augment = hasProto
? protoAugment
: copyAugment
// 通过能力检测的结果选择不同方式进行数组劫持
augment(value, arrayMethods, arrayKeys)
// 对数组的响应式处理,很本文关系不大,略过
this.observeArray(value)

Сначала определитеaugmentконстанта, значение которой определяется выражениемhasProtoПринимать решение.

Давайте посмотримhasProto:

export const hasProto = '__proto__' in {}

Его можно найти,hasProtoНа самом деле это логическая константа, используемая для указания, поддерживает ли браузер прямое использование__proto__(Неявный прототип).

Итак, первый фрагмент кода прост для понимания: выберите различные методы захвата массива в соответствии с результатом обнаружения возможности, если браузер поддерживает неявный прототип, затем вызовитеprotoAugmentиспользовать как метод захвата массива, в противном случае используйтеcopyAugment.

Различные методы захвата массива

Теперь давайте посмотримprotoAugmentтак же какcopyAugment.

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

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

Далее мы смотрим наcopyAugment:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    // Object.defineProperty的封装
    def(target, key, src[key])
  }
}

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

// 获取变异数组中所有自身属性的属性名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

arrayKeysОн определен в начале файла, то есть имена атрибутов всех собственных атрибутов в массиве мутаций, который является массивом.

оглядыватьсяcopyAugmentФункция очень понятна: определение всех методов в массиве мутаций непосредственно в самом экземпляре массива эквивалентно замаскированному захвату массива.

После реализации перехвата массива давайте посмотримVueКак реализовать функцию расширения массива.

Расширение функций

Код расширения функции массива находится вsrc/core/observer/array.js, код показан ниже:

import { def } from '../util/index'

// 缓存数组原型
const arrayProto = Array.prototype
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto)

// 需要进行功能拓展的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  // 缓存原生数组方法
  const original = arrayProto[method]
  // 在变异数组中定义功能拓展方法
  def(arrayMethods, method, function mutator (...args) {
    // 执行并缓存原生数组方法的执行结果
    const result = original.apply(this, args)
    // 响应式处理
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    // 返回原生数组方法的执行结果
    return result
  })
})

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

Суммировать

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