Обнаружение данных в Vue3

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

Ранним утром 5 октября был официально выпущен исходный код Vue3, из официальных новостей:

vue3.0-publish.png

Текущая версияPre-Alpha, Адрес склада: г.Vue-next, в состоянии пройтиComposition APIУзнайте больше о новой версии, Ситуация, связанная с модульным тестом текущей версииvue-next-coverage.

Схема статьи:

outline.png

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

Способы реализации отзывчивого объекта

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

  1. геттеры и сеттеры
  2. defineProperty
  3. Proxy

Мало что можно сказать об использовании первых двух API, одного аксессораgetter/setterФункциональность относительно проста, и поскольку Vue2.x реализует API реактивного объекта —defineProperty, У самого API много проблем.

В Vue2.x, чтобы реализовать отзывчивость данных, необходимоObjectа такжеArrayЭти два типа обрабатываются по-разному.Objectтип переданObject.definePropertyПреобразование свойств вgetter/setter, этот процесс должен рекурсивно обнаруживать все объектыkey, для достижения определения глубины.

восприниматьArrayИзменять,ArrayПерехватывается несколько методов на прототипе, меняющих содержимое самого массива.Хотя ответ на массив реализован, но тоже есть некоторые проблемы или неудобства. в то же время,definePropertyРеализовано рекурсивноgetter/setterЕсть также определенные проблемы с производительностью.

Лучший способ сделать это черезES6который предоставилProxy API.

Некоторые детали прокси-API

Proxy APIимеет более мощные функции, по сравнению со старымdefinePropertyAPI,ProxyМассивы могут быть проксированы, а API предоставляет несколькоtraps, который может реализовать множество функций.

Вот в основном две ловушки:get,set, и некоторые детали, которые легко упустить из виду.

Деталь 1: ловушка поведения по умолчанию

let data = { foo: 'foo' }
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value')
    target[key] = value // ?
  }
})

p.foo = 123

// set value

пройти черезproxyвозвращаемый объектpпрокси-операции с необработанными данными, когдаpКогда установлено, изменения могут быть обнаружены. Но на самом деле есть проблема с написанием таким образом, Когда объектные данные прокси представляют собой массив, будет сообщено об ошибке.

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value')
    target[key] = value
  }
})

p.push(4) // VM438:12 Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'

Измените код на:

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value')
    target[key] = value
    return true
  }
})

p.push(4)

// set value // 打印2次

Фактически, когда прокси-объект является массивом,pushОперация, а не только для работы с текущими данными,pushДействия также вызывают изменения других свойств самого массива.

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    console.log('get value:', key)
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    target[key] = value
    return true
  }
})

p.push(1)

// get value: push
// get value: length
// set value: 3 1
// set value: length 4

Первый взглядsetоперации, как видно из распечатки,pushоперации, кроме первой3Значение настройки битового индекса1, который возвращает массивуlengthзначение изменено на4.В то же время эта операция также вызывает получениеполучитьpushа такжеlengthдва свойства.

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

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    console.log('get value:', key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})

p.push(1)

// get value: push
// get value: length
// set value: 3 1
// set value: length 4

чем справиться с этим самостоятельноsetповедение по умолчанию,Reflectнамного удобнее.

Деталь 2: триггер установить/получить несколько раз

Как видно из предыдущего примера, когда прокси-объект является массивом,pushДействие сработает несколько разsetвыполнять и в то же время вызыватьgetОперация, это очень важно, vue3 очень хорошо это использует. Мы можем увидеть эту операцию из другого примера:

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    console.log('get value:', key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})

p.unshift('a')

// get value: unshift
// get value: length
// get value: 2
// set value: 3 3
// get value: 1
// set value: 2 2
// get value: 0
// set value: 1 1
// set value: 0 a
// set value: length 4

Как видите, при создании массиваunshiftПри работе он будет срабатывать несколько разgetа такжеset. Присмотревшись к выходу, нетрудно заметить, чтоgetСначала возьмите последний индекс массива и откройте новый индекс3Сохраните исходное значение последней цифры, а затем переместите исходное значение обратно в0Подстрочный индекс установлен наunshiftзначениеa, что привело ко многимsetработать.

И это дляУведомление о внешних действияхЗаведомо неблагоприятный, полагаемsetсерединаconsoleзаключается в запуске внешнего рендерингаrenderфункция, то этоunshiftоперация вызоветнеоднократно render.

О том, как решить соответствующую задачу, мы расскажем позже, продолжим.

Деталь 3: прокси может проксировать только один слой

let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] }
let p = new Proxy(data, {
  get(target, key, receiver) {
	  console.log('get value:', key)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    return Reflect.set(target, key, value, receiver)
  }
})

p.bar.key = 2

// get value: bar

Выполните код, вы можете видеть, что он не срабатываетsetВместо этого вывод срабатываетget,потому чтоsetво время визитаbarэто свойство. Отсюда видно, чтоproxyПрокси-объект может быть прокси только для первого слоя, а определение глубины внутри объекта должно быть реализовано разработчиком. То же верно и для массивов внутри объектов.

p.ary.push('c')

// get value: ary

Тоже только что ушелgetдействовать,setНе чувствую.

мы заметилиget/setЕсть еще один параметр:receiver,дляreceiver, который фактически получает прокси-объект:

let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
  get(target, key, receiver) {
    console.log(receiver)
	  const res = Reflect.get(target, key, receiver)
    return res
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

// Proxy {a: {…}}

здесьreceiverРезультатом является текущий прокси-объект. Обратите внимание, что это объект, который был проксирован.

let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    console.log(res)
    return res
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
})

// {b: {c: 1} }

когда мы пытаемся вывестиReflect.getВозвращаемое значение обнаружит, что когда прокси-объект представляет собой многоуровневую структуру,Reflect.getВозвращает внутреннюю структуру объекта.

Помните об этом, Vue3 реализует глубокий прокси, что является хорошим использованием этого.

Решить проблемы с деталями в прокси

упоминалось ранее с использованиемProxyДля обнаружения изменений данных есть несколько деталей, в том числе:

  1. использоватьReflectвозвращатьtrapПоведение по умолчанию
  2. дляsetоперации, может привести к изменению свойств прокси-объекта, что приведет кsetвыполнить несколько раз
  3. proxyОн может проксировать только один слой в объекте для работы внутри объекта.setне воспринимать, аgetбудет казнен

Далее мы будемПопробуйте решить эти проблемы самостоятельно, а затем проанализируйте, как Vue3 решает эти детали.

setTimeout разрешает повторяющиеся триггеры

function reactive(data, cb) {
  let timer = null
  return new Proxy(data, {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        cb && cb()
      }, 0);
      return Reflect.set(target, key, value, receiver)
    }
  })
}

let ary = [1, 2]
let p = reactive(ary, () => {
  console.log('trigger')
})
p.push(3)

// trigger

Выход программы один: триггер

понял здесьreactiveФункция, которая получает два параметра, первый — данные проксиdata, и функция обратного вызоваcb, Здесь мы простоcbРаспечатайте операцию триггера при моделировании уведомления об изменении внешних данных.

разрешать дубликатыcbЕсть много способов вызова, например, через флаги, чтобы решить, следует ли вызывать. А вот использование таймераsetTimeout, каждый звонокcbРаньше таймеры очищались для достижения чего-то вродеdebounceоперация, также может решить повторяющиесяcallbackвопрос.

Решите определение глубины данных

Есть еще проблема, то есть глубокое обнаружение данных, мы можем использовать рекурсивный прокси для достижения:

function reactive(data, cb) {
  let res = null
  let timer = null

  res = data instanceof Array ? []: {} 

  for (let key in data) {
    if (typeof data[key] === 'object') {
      res[key] = reactive(data[key], cb)
    } else {
      res[key] = data[key]
    }
  }

  return new Proxy(res, {
    get(target, key) {
      return Reflect.get(target, key)
    },
    set(target, key, val) {
      let res = Reflect.set(target, key, val)
      clearTimeout(timer)
      timer = setTimeout(() => {
        cb && cb()
      }, 0)
      return res
    }
  })
}

let data = { foo: 'foo', bar: [1, 2] }
let p = reactive(data, () => {
  console.log('trigger')
})
p.bar.push(3)

// trigger

Итерация по прокси-объектам для каждогоkeyсделать все это один разproxy, как это реализовано рекурсивно. В то же время в сочетании с отмеченным вышеtimerИзбегайте проблемы повторяющихся наборов.

Здесь мы можем вывести проксируемый объектp:

p.png

Вы можете видеть объекты после дип-прокси, все несутproxyсимволы.

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

Далее давайте посмотрим, как используется Vue3.Proxyобнаружение данных.

реактивность в Vue3

Структура проекта Vue3 принимаетlernaДелатьmonorepoстиль управления кодом.В настоящее время многие проекты с открытым исходным кодом перешли на режим монорепозитория. Более примечательной особенностью является то, что будетpackages/папка.

Vue3 хорошо разделяет функции на модули и используетTS. Находим модули для реактивных данных прямо в пакетах:

reactivity.png

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

Вот небольшое упрощение исходного кода:

const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()

// utils
function isObject(val) {
  return typeof val === 'object'
}

function hasOwn(val, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(val, key)
}

// traps
function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    return isObject(res) ? reactive(res) : res
  }
}

function set(target, key, val, receiver) {
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]

  val = reactiveToRaw.get(val) || val
  const result = Reflect.set(target, key, val, receiver)

  if (!hadKey) {
    console.log('trigger ...')
  } else if(val !== oldValue) {
    console.log('trigger ...')
  }

  return result
}

// handler
const mutableHandlers = {
  get: createGetter(),
  set: set,
}

// entry
function reactive(target) {
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
  )
}

function createReactiveObject(target, toProxy, toRaw, baseHandlers) {
  let observed = toProxy.get(target)
  // 原数据已经有相应的可响应数据, 返回可响应数据
  if (observed !== void 0) {
    return observed
  }
  // 原数据已经是可响应数据
  if (toRaw.has(target)) {
    return target
  }
  observed = new Proxy(target, baseHandlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

rawToReactiveа такжеreactiveToRawдве слабые ссылкиMapструктура, дваMapиспользуется для сохранения原始数据а также可响应数据, в функцииcreateReactiveObjectсередина,toProxyа такжеtoRawВходящие эти дваMap.

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

Помимо сохранения данных агента и исходных данных,createReactiveObjectФункция просто возвращаетnew ProxyПроксируемый объект. Сфокусируйся наnew ProxyПереданный параметр обработчикаbaseHandlers.

Помните о вышеупомянутомProxyДавайте углубимся в детали реализации обнаружения данных, попробуем ввести:

let data = { foo: 'foo', ary: [1, 2] }
let r = reactive(data)
r.ary.push(3)

распечатать результат:

console.png

Вы можете увидеть распечатку один разtrigger ...

Вопрос 1: Как получить подробные данные обнаружения?

Данные об обнаружении глубины получаются черезcreateGetterРеализация функции, как было сказано ранее, при работе с многоуровневыми объектами,setне чувствую этого,но получить вызовет, В то же время, используяReflect.get()Возвращенный «внутренний слой в многоуровневом объекте», а затем сделайте прокси для «данных внутреннего слоя».

function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    return isObject(res) ? reactive(res) : res
  }
}

можно увидеть здесьОпределяет, являются ли данные, возвращаемые Reflect, по-прежнему объектом, если это объект, повторите попыткуproxy,Таким образом, обнаружение внутренней части объекта получается.

И каждый разproxyданные будут храниться вMap, который будет искаться непосредственно при доступе, тем самым повышая производительность.

Когда мы печатаем проксируемый объект:

r.png

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

вывести один из сохраненных данных проксиrawToReactive:

rawToReactive.png

для внутреннего слояary: [1, 2]прокси, уже сохраненный вrawToReactiveсередина.

Это обеспечивает глубокое обнаружение данных.

Вопрос 2: Как избежать множественных триггеров?

function hasOwn(val, key) {
  const hasOwnProperty = Object.prototype.hasOwnProperty
  return hasOwnProperty.call(val, key)
}
function set(target, key, val, receiver) {
  console.log(target, key, val)
  const hadKey = hasOwn(target, key)
  const oldValue = target[key]
  
  val = reactiveToRaw.get(val) || val
  const result = Reflect.set(target, key, val, receiver)

  if (!hadKey) {
    console.log('trigger ... is a add OperationType')
  } else if(val !== oldValue) {
    console.log('trigger ... is a set OperationType')
  }

  return result
}

примерно несколько разtriggerПроблема в том, что Vue справляется с ней очень умно.

существуетsetв функцииhasOwnперед печатьюconsole.log(target, key, val).

войти:

let data = ['a', 'b']
let r = reactive(data)
r.push('c')

Выходной результат:

hasOwn.png

r.push('c')вызоветsetСделайте это дважды, один раз для самого значения'c', однаждыlengthнастройки свойств.

Настройки'c', входящий новый индексkeyдля2,targetисходный прокси-объект['a', 'c'],hasOwn(target, key)очевидно возвращаетсяfalse, которая является недавно добавленной операцией, которая может быть выполнена в это времяtrigger ... is a add OperationType.

при входящемkeyдляlengthчас,hasOwn(target, key),lengthявляется его собственным свойством, возвращаетtrue, то судитьval !== oldValue , valда3, а такжеoldValueто естьtarget['length']Слишком3, на этот раз не выполняетсяtriggerвыходное заявление.

так черезОпределите, является ли ключ собственным атрибутом цели, и равно ли заданное значение target[key]можно определитьtriggerвведите и избегайте избыточногоtrigger.

Суммировать

Фактически, эта статья в основном посвящена тому, как использовать его в Vue3.Proxyдля обнаружения данных. Прежде чем анализировать исходный код, необходимо уточнитьProxyНекоторые из своих особенностей, поэтому я сказал многоProxyпредварительное знание. При этом мы тоже решаем эти проблемы по-своему.

Наконец, мы сравнили в Vue3, как обрабатываются эти детали. Видно, что Vue3 не просто проходитProxyдля рекурсивного обнаружения данных, но черезgetоперации для реализации прокси для внутренних данных и объединенияWeakMapЧтобы сохранить данные, это значительно улучшит производительность адаптивных данных.

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

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