Чем отличается отзывчивость Vue3 от предыдущего, Proxy непобедим?

Vue.js опрос

предисловие

Как мы все знаем, отзывчивость в 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)
})

Для зависимостей этого примера

  1. ГлобальныйtargetMapДа:
targetMap: {
  { count: 1 }: dep    
}
  1. деп это
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

У него был маленький толчок, и вызвало набор двух пар, давайте представим процесс:

  1. прочитать метод push
  2. Прочитайте исходное свойство длины обрр
  3. Присвоить значение элементу 0 массива
  4. Присвоение свойству 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;
}

представление

  1. Во-первых, Proxy, как новый стандарт для браузеров, обязательно будет оптимизирован производителями с точки зрения производительности, поживем-увидим.
  2. Для чувствительных данных 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

Для интерпретации этой библиотеки вы можете посмотреть две мои предыдущие статьи:

Помогите вам досконально понять принцип ответа прокси Vue3! TypeScript реализует реактивную библиотеку на основе прокси с нуля.

Помогите вам досконально понять принцип ответа прокси Vue3! Реактивная реализация Map и Set на основе перехвата функций

Во второй статье вы также можете получить представление на уровне исходного кода о том, что могут делать операции перехвата 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», где все смогут общаться и прогрессировать вместе.

Категории