Ранним утром 5 октября был официально выпущен исходный код Vue3, из официальных новостей:
Текущая версияPre-Alpha
, Адрес склада: г.Vue-next,
в состоянии пройтиComposition APIУзнайте больше о новой версии,
Ситуация, связанная с модульным тестом текущей версииvue-next-coverage.
Схема статьи:
Одним из основных элементов Vue является реактивная система, которая управляет представлением обновлений, обнаруживая изменения в данных.
Способы реализации отзывчивого объекта
Через реагирующий объект реализуется обнаружение данных, чтобы информировать об изменениях внешних данных. Способы реализации отзывчивого объекта:
- геттеры и сеттеры
- defineProperty
- 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имеет более мощные функции,
по сравнению со старымdefineProperty
API,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
Для обнаружения изменений данных есть несколько деталей, в том числе:
- использовать
Reflect
возвращатьtrap
Поведение по умолчанию - для
set
операции, может привести к изменению свойств прокси-объекта, что приведет кset
выполнить несколько раз -
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
:
Вы можете видеть объекты после дип-прокси, все несутproxy
символы.
До сих пор мы решали, используяproxy
Ряд деталей для реализации обнаружения, хотя эти методы обработки могут решить проблему, они не кажутся достаточно элегантными, особенно рекурсияproxy
представляет опасность для производительности,
Когда объект данных относительно велик, рекурсивный прокси-сервер будет потреблять относительно большую производительность, и некоторые данные не нужно обнаруживать, нам нужно более детально контролировать обнаружение данных.
Далее давайте посмотрим, как используется Vue3.Proxy
обнаружение данных.
реактивность в Vue3
Структура проекта Vue3 принимаетlerna
Делатьmonorepo
стиль управления кодом.В настоящее время многие проекты с открытым исходным кодом перешли на режим монорепозитория.
Более примечательной особенностью является то, что будетpackages/
папка.
Vue3 хорошо разделяет функции на модули и используетTS. Находим модули для реактивных данных прямо в пакетах:
в,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)
распечатать результат:
Вы можете увидеть распечатку один раз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
, который будет искаться непосредственно при доступе, тем самым повышая производительность.
Когда мы печатаем проксируемый объект:
Видно, что внутренний слой прокси-объекта не имеет логотипа прокси, здесь только внешний прокси-объект.
вывести один из сохраненных данных проксиrawToReactive
:
для внутреннего слоя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')
Выходной результат:
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
Большое упрощение, детали, с которыми нужно иметь дело, на самом деле намного сложнее.
Более подробную информацию еще нужно проверитьисходный кодполучать.