предисловие
Как мы все знаем, отзывчивость в Vue2 на самом деле немного похожа на полузавершенное тело.Он ничего не может сделать с недавно добавленными свойствами объекта.Для массива ему нужно перехватить свой метод-прототип, чтобы реализовать отзывчивость .
Например:
let vm = new Vue({
data() {
return {
a: 1
}
}
})
// ❌ oops,没反应!
vm.b = 2
let vm = new Vue({
data() {
return {
a: 1
}
},
watch: {
b() {
console.log('change !!')
}
}
})
// ❌ oops,没反应!
vm.b = 2
В это время Vue предоставляет API:this.$set
Для добавления таких свойств также адаптируют эффект.
Но для многих новичков часто необходимо тщательно оценить, какие ситуации нужно использовать.$set
, когда вы можете вызвать реакцию напрямую.
Короче говоря, в Vue3 это останется в прошлом. В этой статье вы подробно объясните, какие удобные прокси-серверы привнесут в Vue3. И он расскажет вам, как они реализованы на уровне исходного кода.
Отзывчивый склад
Vue3 отличается от Vue2 структурой исходного кода, Vue3 распределяет пакеты с низкой связанностью вpackages
Публикуется отдельно в каталоге какnpm
Мешок. Это также очень популярный способ управления крупномасштабными проектами.Monorepo
.
Репозиторий, отвечающий за реактивную часть,@vue/reactivity, он не включает никакую другую часть Vue и является очень, очень «ортогональной» реализацией.
может дажеПростая интеграция в React.
Это также делает анализ этой статьи более сосредоточенным на этом складе, исключая другие нерелевантные части.
разница
Использование Proxy и Object.defineProperty кажется очень похожим, на самом деле Proxy перехватывает изменение свойства в «высшем измерении» Как вы это понимаете?
В Vue2 для заданных данных, таких как{ count: 1 }
, он должен быть основан на конкретном ключе, которыйcount
, чтобы перехватить "modify data.count" и "read data.count", то есть
Object.defineProperty(data, 'count', {
get() {},
set() {},
})
Вы должны заранее знать, какой ключ нужно перехватить, поэтому Vue2 ничего не может сделать с новыми свойствами объекта.
Прокси, используемый Vue3, перехватывается следующим образом:
new Proxy(data, {
get(key) { },
set(key, value) { },
})
Видно, что нет необходимости заботиться о конкретном ключе вообще, он перехватывает «изменение любого ключа в данных» и «чтение любого ключа в данных».
Поэтому, будь то существующий ключ или новый ключ, он не может ускользнуть из его когтей.
Но что делает Proxy более мощным, так это то, что Proxy может перехватывать больше операторов, чем get и set.
Простой пример 🌰
Сначала напишите минимальный пример отзывчивости Vue3, соответствующие случаи в этой статье будут использовать толькоreactive
а такжеeffect
эти два API. Если вы знаете о ReactuseEffect
, я думаю, вы поймете эту концепцию за считанные секунды, Vue3effect
Однако «эволюционная версия» ручного объявления зависимостей удалена.useEffect
.
Ручное объявление в React[data.count]
Этот зависимый шаг выполняется непосредственно Vue3, вeffect
читать внутри функцииdata.count
, он уже собран как зависимость.
vue3:
// 响应式数据
const data = reactive({
count: 1
})
// 观测变化
effect(() => console.log('count changed', data.count))
// 触发 console.log('count changed', data.count) 重新执行
data.count = 2
Реагировать:
// 数据
const [data, setData] = useState({
count: 1
})
// 观测变化 需要手动声明依赖
useEffect(() => {
console.log('count changed', data.count)
}, [data.count])
// 触发 console.log('count changed', data.count) 重新执行
setData({
count: 2
})
На самом деле, видя это дело, можно и умничатьeffect
Функции обратного вызова связаны с повторным рендерингом представления, функциями обратного вызова просмотра и т. д. Они также основаны на этом реактивном механизме.
Основная цель этой статьи — изучить, насколько мощным может быть этот реактивный API на основе прокси и насколько мощным он может отслеживать пользовательские модификации.
Поговорим о принципе
Сначала объясню принцип отзывчивости минимально, по сути это второй параметр Proxy.handler
то естьоператор ловушки, перехватывать различные операции получения и присвоения значений, опираясь наtrack
а такжеtrigger
Две функции выполняют сбор зависимостей и отправку обновлений.
track
Используется для сбора зависимостей при чтении.
trigger
Используется для запуска зависимостей при обновлении.
track
function track(target: object, type: TrackOpTypes, key: unknown) {
const depsMap = targetMap.get(target);
// 收集依赖时 通过 key 建立一个 set
let dep = new Set()
targetMap.set(ITERATE_KEY, dep)
// 这个 effect 可以先理解为更新函数 存放在 dep 里
dep.add(effect)
}
target
является исходным объектом.
type
Это тип этой коллекции, то есть, какой тип операции используется для определения, когда зависимость собрана.Например, тип в зависимости вышеget
, что будет подробно объяснено позже.
key
Это относится к тому, к какому ключу в данных осуществляется доступ на этот раз. Например, ключ для сбора зависимостей в приведенном выше примере —count
Во-первых, это будет глобальныйtargetMap
, который используется для создания数据 -> 依赖
map, которая представляет собой структуру данных WeakMap.
а такжеtargetMap
через данныеtarget
, вы можете получитьdepsMap
, который используется для хранения всех реактивных зависимостей, соответствующих этим данным.
depsMap
Каждый элемент представляет собой структуру данных Set, и этот Set хранит функцию обновления соответствующего ключа.
Он немного круглый? Возьмем конкретный пример.
const target = { count: 1}
const data = reactive(target)
const effection = effect(() => {
console.log(data.count)
})
Для зависимостей этого примера
- Глобальный
targetMap
Да:
targetMap: {
{ count: 1 }: dep
}
- деп это
dep: {
count: Set { effection }
}
Таким образом, слой за слоем можно пройтиtarget
оказатьсяcount
Соответствующая функция обновленияeffection
.
trigger
Вот минимальная реализация, просто для удобства понимания принципа, на самом деле все гораздо сложнее,
фактическиtype
Роль очень важна, запомните сначала, а я расскажу о ней подробно позже.
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
) {
// 简化来说 就是通过 key 找到所有更新函数 依次执行
const dep = targetMap.get(target)
dep.get(key).forEach(effect => effect())
}
Добавить свойство
Это было упомянуто выше, поскольку прокси вообще не заботится о конкретном ключе, так что это не проблема.
// 响应式数据
const data = reactive({
count: 1
})
// 观测变化
effect(() => console.log('newCount changed', data.newCount))
// ✅ 触发响应
data.newCount = 2
Новый индекс массива:
// 响应式数据
const data = reactive([])
// 观测变化
effect(() => console.log('data[1] changed', data[1]))
// ✅ 触发响应
data[1] = 5
Массивы вызывают нативные методы:
const data = reactive([])
effect(() => console.log('c', data[1]))
// 没反应
data.push(1)
// ✅ 触发响应 因为修改了下标为 1 的值
data.push(2)
На самом деле этот случай более интересен, мы просто вызываем push, но при проталкивании второго элемента массива мы обратили внимание наdata[1]
Также выполняется функция обратного вызова для зависимости, по какому принципу? Напишите простой прокси, чтобы знать.
const raw = []
const arr = new Proxy(raw, {
get(target, key) {
console.log('get', key)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key)
return Reflect.set(target, key, value)
}
})
arr.push(1)
В этом случае мы просто распечатываем дляraw
Все получают, устанавливают операции над этим массивом и вызываютReflectЭтот API обрабатывает операции получения и назначения как есть и возвращает. посмотриarr.push(1)
Что консоль напечатала?
get push
get length
set 0
set length
У него был маленький толчок, и вызвало набор двух пар, давайте представим процесс:
- прочитать метод push
- Прочитайте исходное свойство длины обрр
- Присвоить значение элементу 0 массива
- Присвоение свойству length
Основное внимание здесь уделяется третьему шагу.Для назначения элемента индекса нажмите в следующий раз, и можно запустить операцию установки для первого элемента.
пока читаем в примереdata[1]
, связан с1
Собираются зависимости этого индекса, что ясно объясняет, почему выполнение реактивных зависимостей может быть точно запущено при выполнении push.
Кстати, очень важно помнить, что эта операция установки длины будет использоваться позже.
Добавлено после обхода
// 响应式数据
const data = reactive([])
// 观测变化
effect(() => console.log('data map +1', data.map(item => item + 1))
// ✅ 触发响应 打印出 [2]
data.push(1)
Этот перехват изумителен, но и очень разумен. Превращая его в пример на деле,
Предположим, нам нужна коллекция на основе студенческого билета.ids
, чтобы запросить данные студента, то просто нужно написать:
const state = reactive({})
const ids = reactive([1])
effect(async () => {
state.students = await axios.get('students/batch', ids.map(id => ({ id })))
})
// ✅ 触发响应
ids.push(2)
Таким образом, каждый раз, когда вы вызываете различные API для изменения массива ids, запрос будет повторно отправлен, чтобы получить последний список студентов.
Если я вызову map, forEach и т. д. API в функции прослушивателя,
Это означает, что меня волнует изменение длины этого массива, поэтому совершенно правильно запускать ответ при нажатии.
Но как это достигается? Это кажется сложным.
Потому что, когда эффект выполняется в первый раз,data
Это все еще пустой массив, как он может запускать обновление при нажатии?
Или используйте небольшой тест прямо сейчас, чтобы увидеть, что происходит, когда вы составляете карту.
const raw = [1, 2]
const arr = new Proxy(raw, {
get(target, key) {
console.log('get', key)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key)
return Reflect.set(target, key, value)
}
})
arr.map(v => v + 1)
get map
get length
get constructor
get 0
get 1
Что такое же, как нажимная часть? Найдите подсказку, мы сработаем, когда найдем картуget length
, и когда обновление запускается, Vue3 выполнит специальную обработку операции «новый ключ», вот новый0
Значение этого индекса перейдет кtrigger
В такой логике:
// 简化版
if (isAddOrDelete) {
add(depsMap.get('length'))
}
Получите зависимости, собранные при чтении длины ранее, а затем запустите функцию.
Очевидно, что мыeffect
Операция карты считывает длину и собирает зависимости длины.
При добавлении ключа активируйте зависимости, собранные по длине, и активируйте функцию обратного вызова.
Правильно, дляfor of
Также возможна операция:
// 响应式数据
const data = reactive([])
// 观测变化
effect(() => {
for (const val of data) {
console.log('val', val)
}
})
// ✅ 触发响应 打印出 val 1
data.push(1)
Вы можете самостоятельно запустить перехват по нашему небольшому тесту,for of
также вызоветlength
читать.
length
Такой хороший товарищ... очень помог.
Удалить или очистить после обхода
Обратите внимание, что условие оценки в приведенном выше исходном кодеisAddOrDelete
, то же самое и при удалении, с помощьюlength
Зависимости собраны выше.
// 简化版
if (isAddOrDelete) {
add(depsMap.get('length'))
}
const arr = reactive([1])
effect(() => {
console.log('arr', arr.map(v => v))
})
// ✅ 触发响应
arr.length = 0
// ✅ 触发响应
arr.splice(0, 1)
Он действительно может реагировать на любую операцию, мне это нравится.
получить ключи
const obj = reactive({ a: 1 })
effect(() => {
console.log('keys', Reflect.ownKeys(obj))
})
effect(() => {
console.log('keys', Object.keys(obj))
})
effect(() => {
for (let key in obj) {
console.log(key)
}
})
// ✅ 触发所有响应
obj.b = 2
Эти методы получения ключей могут быть успешно перехвачены, ведь Vue перехватывается изнутри.ownKeys
оператор.
const ITERATE_KEY = Symbol( 'iterate' );
function ownKeys(target) {
track(target, "iterate", ITERATE_KEY);
return Reflect.ownKeys(target);
}
ITERATE_KEY
Просто как специальный идентификатор, указывающий, что это зависимость, собранная при чтении ключа. Он будет использоваться как ключ для сбора зависимостей.
Затем, когда запускается обновление, оно фактически соответствует этому исходному коду:
if (isAddOrDelete) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY));
}
На самом деле, когда мы говорим о массивах, часть кода упрощается. Если это не массив, он сработаетITERATE_KEY
соответствующие зависимости.
Пасхальные яйца:
Reflect.ownKeys
,Object.keys
а такжеfor in
На самом деле поведение другое.
Reflect.ownKeys
можно собратьSymbol
Тип ключа, неперечисляемый ключ.
Например:
var a = {
[Symbol(2)]: 2,
}
Object.defineProperty(a, 'b', {
enumerable: false,
})
Reflect.ownKeys(a) // [Symbol(2), 'b']
Object.keys(a) // []
Оглядываясь на то, что только что было упомянутоownKeys
перехват,
function ownKeys(target) {
track(target, "iterate", ITERATE_KEY);
// 这里直接返回 Reflect.ownKeys(target)
return Reflect.ownKeys(target);
}
Внутренне возвращается непосредственно междуReflect.ownKeys(target)
, понятно, что на этот разObject.keys
После этого перехвата операция также будет следоватьReflect.ownKeys
поведение для возврата значения.
Однако окончательный результат вернулсяObject.keys
Результат, который довольно чудесен.
удалить свойство объекта
С вышеуказаннымownKeys
Основы, давайте посмотрим на этот пример.
const obj = reactive({ a: 1, b: 2})
effect(() => {
console.log(Object.keys(obj))
})
// ✅ 触发响应
delete obj['b']
Это тоже магическая операция, принцип в том, что дляdeleteProperty
Перехват оператором:
function deleteProperty(target: object, key: string | symbol): boolean {
const result = Reflect.deleteProperty(target, key)
trigger(target, TriggerOpTypes.DELETE, key)
return result
}
используется снова здесьTriggerOpTypes.DELETE
Тип , основанный на опыте выше, должен иметь для него специальную обработку.
На самом деле ещеtrigger
Логика в:
const isAddOrDelete = type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE
if (isAddOrDelete) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
Цель здесь не является массивом, поэтому она все равно сработает.ITERATE_KEY
Собранные зависимости — это зависимости, собранные для чтения ключей, только что упомянутого в приведенном выше примере.
Проверить, существует ли свойство
const obj = reactive({})
effect(() => {
console.log('has', Reflect.has(obj, 'a'))
})
effect(() => {
console.log('has', 'a' in obj)
})
// ✅ 触发两次响应
obj.a = 1
Это очень просто, просто используйтеhas
Перехват оператором.
function has(target, key) {
const result = Reflect.has(target, key);
track(target, "has", key);
return result;
}
представление
- Во-первых, Proxy, как новый стандарт для браузеров, обязательно будет оптимизирован производителями с точки зрения производительности, поживем-увидим.
- Для чувствительных данных Vue3 больше не рекурсивно определяет все подданные в ответ, как в Vue2, а использует их при получении глубоких данных.
reactive
Для дальнейшего определения скорости отклика это может быть очень полезно для сценариев инициализации с большими объемами данных.
Например, для
const obj = reactive({
foo: {
bar: 1
}
})
определение инициализацииreactive
когда только правильноobj
Поверхностное определение отзывчивости, но действительно читаемоеobj.foo
будет правильно, когдаfoo
Этот слой объектов определяет отзывчивость, а упрощенный исходный код выглядит следующим образом:
function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver)
// 这段就是惰性定义
return isObject(res)
? reactive(res)
: res
}
Рекомендуемое чтение
На самом деле Vue3 дляMap
а такжеSet
Эти два типа данных также полностью поддерживают отзывчивость, а их методы-прототипы также полностью перехвачены, поэтому из-за нехватки места в этой статье мы их повторять не будем.
Честно говоря, отзывчивая часть ветки логики кода Vue3 все еще слишком велика, и она не очень удобна для понимания кода, потому что она также будет включатьreadonly
Для операций только для чтения, если вы очень заинтересовались принципом отзывчивости Vue3 после прочтения этой статьи, рекомендуется прочитать исходный код из упрощенной версии библиотеки.
я рекомендую здесьobserver-util, я прочитал исходный код этой библиотеки, и принцип реализации Vue3 в основном такой же! Но гораздо проще. Небольшой, но полный. Примечания внутри также очень полны.
Конечно, если вы не очень хорошо владеете английским языком, вы также можете увидеть мои тщательно использованные комментарии TypeScript + китайский язык, основанные наobserver-util
Переписанный код:typescript-proxy-reactive
Для интерпретации этой библиотеки вы можете посмотреть две мои предыдущие статьи:
Во второй статье вы также можете получить представление на уровне исходного кода о том, что могут делать операции перехвата Map и Set.
Суммировать
Прокси-сервер Vue3 действительно мощный, и он решает часть Vue2, которая, как мне кажется, является большой умственной нагрузкой. (Когда я впервые запустил Vue, я действительно не знал, когда использовать$set
),этоcomposition-api
можно идеально выровнятьReact Hook
, и благодаря мощи отзывчивой системы он в некоторых отношениях превосходит ее.Интенсивное чтение "API функций Vue3.0"
Я надеюсь, что в этой статье можно будет использовать некоторые новые функции, знакомые с Vue3, до того, как Vue3 официально появится.
Расширенное чтение
В перехватчике Proxy есть параметр Receiver, который для простоты в статье не отражен, для чего он нужен? Эта информация редко встречается на отечественных сайтах:
new Proxy(raw, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
}
})
См. вопросы и ответы на StackOverflow:what-is-a-receiver-in-javascript
Также см. мое резюмеЧто такое получатели в Proxy и Reflect?
коммерческое время
Отличный автор брошюры Сюян выпустил брошюру по алгоритмам для тех, кто хочет изучить алгоритмы на начальном этапе. Он поможет вам освоить некоторые основные идеи алгоритмов или простые алгоритмические задачи. Эта брошюра Я участвовал в процессе внутреннего тестирования, а также дал много мнений Xiuyan. Его цель состоит в том, чтобы предоставить «услуги няни» для клиентской группы с помощью алгоритмов, основанных на нуле, что очень заботливо~
попросить лайк
Если эта статья была вам полезна, пожалуйста, поставьте лайк и поддержите ее. Ваш "лайк" - это движущая сила для меня, чтобы продолжать творить. Дайте мне знать, что вам нравится читать мою статью~
❤️Спасибо всем
Подпишитесь на официальный аккаунт «Front-end from Advanced to Hospital» и вы сможете добавить меня в друзья, и я втяну вас в «Front-end Advanced Exchange Group», где все смогут общаться и прогрессировать вместе.