Анализ исходного кода адаптивной системы Vue3 — отдельный тест

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

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

10 мая, в национальный праздник, маленький правый бог-мужчина выпустил код альфа-версии vue@3.0.0. Все равно делать мне нечего, я тоже недавно изучаю TypeScript, просто посмотри на код мужского бога и выучи его.

Из входного файла packages/vue/index он очень узкий в начале, 7 строк кода. Ищу несколько файлов, пока runtime-core вдруг просветлел. Комментарий построчно, API такой же. Забудьте, я уже не могу его редактировать, короче, код начинает становиться все больше и больше. Я чувствую, что смотреть Национальный день определенно невозможно, поэтому выберите основную реализацию принципа двусторонней привязки, который всегда задают другим во время интервью.

Всем следует знать, что Vue3 использует Proxy вместо defineProperty для реализации ответного обновления данных, как этого добиться? Откройте каталог файлов с исходным кодом, и вы сразу увидите, что ядро ​​лежит в пакетах/реактивности.

Reactivity

Нажмите на его Readme, и с помощью перевода Google мы можем понять, что это примерно означает:

Этот пакет будет встроен в средство визуализации vue (@vue/runtime-dom). Однако его также можно опубликовать отдельно и на него могут ссылаться третьи стороны (не полагаясь на vue). Тем не менее, не используйте его вслепую, если ваш рендерер открыт для пользователей фреймворка, он может уже иметь встроенный механизм ответа, который полностью состоит из двух наборов нашей реактивности, не обязательно совместимых (говоря, что это вы, react-dom) .

Что касается его API, давайте сначала посмотрим на исходный код или типы. Примечание: кромеMap , WeakMap , Set and WeakSetКроме того, некоторые встроенные объекты недоступны для наблюдения (например:Date,RegExp Ждать).

Ну, на основании одного только Readme невозможно точно знать, на что это похоже. Ведь это альфа версия. Потом еще его слушаем и прямо шлепаем исходники.

Почисти исходный код, выгляди сбитым с толку

Из входного файла реактивности выясняется, что он предоставляет API только в 6 файлах. Они есть:ref,reactive,computed,effect,lock,operations. вlock иoperationsочень простой,lockВнутри файла есть два метода для управления переменной блокировки переключателя:operationsВнутри это перечисление типов операций с данными.

Таким образом, центр реактивностиref,reactive,computed,effectЭти четыре файла, но эти четыре файла не так просты. Мне потребовалось много времени, чтобы пролистать его от начала до конца, и я обнаружил, что знаю каждую букву; я также знал каждое слово с помощью Google; в основном все выражения, мой полусырой уровень TypeScript также может понять. Однако, когда они образуют функцию, я немного смущен...refцитируетсяreactive,reactiveцитируется сноваref, плюс странная операция внутри функции, и вы запутаетесь, если дважды ее обойдете.

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

тогда что нам делать? На самом деле там другое уравнение, то есть один тест. Чтение из одиночного теста — отличный способ прочитать исходный код. Вы можете не только быстро узнать значение и использование API, но также узнать множество граничных условий. В процессе чтения я также подумаю о том, как реализовать его, если он мой, и я смогу углубить свое понимание и изучение исходного кода в будущем.

Начните с одного теста

Поскольку у меня есть немного исходного кода, я могу примерно знать порядок чтения. Конечно, исходя из количества строк кода, мы также можем оценить примерный порядок. Здесь я приведу вывод непосредственно, рекомендуемый порядок чтения: реактивный -> ссылка -> эффект -> вычисляемый -> только для чтения -> коллекции

Reactive

reactiveОтзывчивый, как следует из названия, означаетreactiveДанные — это реагирующие данные, и название предполагает, что они являются ядром этой библиотеки. Итак, давайте посмотрим, какие у него есть возможности.

Первый одиночный тест:

test('Object', () => {
  const original = { foo: 1 }
  const observed = reactive(original)
  expect(observed).not.toBe(original)
  expect(isReactive(observed)).toBe(true)
  expect(isReactive(original)).toBe(false)
  // get
  expect(observed.foo).toBe(1)
  // has
  expect('foo' in observed).toBe(true)
  // ownKeys
  expect(Object.keys(observed)).toEqual(['foo'])
})

Это ни на что не похоже, простоreactiveПередача объекта вернет новый объект.Два объекта имеют одинаковый тип, данные выглядят одинаково, но ссылки разные. Тогда мы сразу поняли, что это, должно быть, было использованоProxy! Ядро адаптивной системы vue@3.

Тогда давайте посмотримreactiveзаявление:

image.png

инструкцияreactiveПринимает только данные объекта и возвращаетUnwrapNestedRefsТип данных, но что это такое, я не знаю, я расскажу об этом позже.

Второй одиночный тест:

test('Array', () => {
  const original: any[] = [{ foo: 1 }]
  const observed = reactive(original)
  expect(observed).not.toBe(original)
  expect(isReactive(observed)).toBe(true)
  expect(isReactive(original)).toBe(false)
  expect(isReactive(observed[0])).toBe(true)
  // get
  expect(observed[0].foo).toBe(1)
  // has
  expect(0 in observed).toBe(true)
  // ownKeys
  expect(Object.keys(observed)).toEqual(['0'])
})

reactiveПосле получения массива (массив, естественно, является объектом), возвращаемый новый массив не равен исходному массиву, но данные непротиворечивы. Показатели соответствуют показателям испытуемых в Тесте 1. Однако этот единственный тест не рассматривает вложенность, я добавлю

test('Array', () => {
  const original: any[] = [{ foo: 1, a: { b: { c: 1 } }, arr: [{ d: {} }] }]
  const observed = reactive(original)
  expect(observed).not.toBe(original)
  expect(isReactive(observed)).toBe(true)
  expect(isReactive(original)).toBe(false)
  expect(isReactive(observed[0])).toBe(true)
  // observed.a.b 是reactive
  expect(isReactive(observed[0].a.b)).toBe(true)
  // observed[0].arr[0].d 是reactive
  expect(isReactive(observed[0].arr[0].d)).toBe(true)
  // get
  expect(observed[0].foo).toBe(1)
  // has
  expect(0 in observed).toBe(true)
  // ownKeys
  expect(Object.keys(observed)).toEqual(['0'])
})

Указывает, что возвращаемые новые данные, пока значение атрибута остается объектом, остаютсяisReactive.

Третий одиночный тест, о котором и говорить нечего, четвертый одиночный тест, проверка вложенных объектов, был рассмотрен в моем дополнении ко второму одиночному тесту.

Пятый одиночный тест:

test('observed value should proxy mutations to original (Object)', () => {
  const original: any = { foo: 1 }
  const observed = reactive(original)
  // set
  observed.bar = 1
  expect(observed.bar).toBe(1)
  expect(original.bar).toBe(1)
  // delete
  delete observed.foo
  expect('foo' in observed).toBe(false)
  expect('foo' in original).toBe(false)
})

**
В этом единственном тесте мы наконец видим "Отзывчивый".пройти черезreactiveЛюбые операции записи/удаления данных ответа, возвращаемых после выполнения, можно синхронно синхронизировать с исходными данными. Что, если верно обратное и исходные данные изменены напрямую?

test('observed value should proxy mutations to original (Object)', () => {
  let original: any = { foo: 1 }
  const observed = reactive(original)
  // set
  original.bar = 1
  expect(observed.bar).toBe(1)
  expect(original.bar).toBe(1)
  // delete
  delete original.foo
  expect('foo' in observed).toBe(false)
  expect('foo' in original).toBe(false)
})

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

Шестой одиночный тест

test('observed value should proxy mutations to original (Array)', () => {
  const original: any[] = [{ foo: 1 }, { bar: 2 }]
  const observed = reactive(original)
  // set
  const value = { baz: 3 }
  const reactiveValue = reactive(value)
  observed[0] = value
  expect(observed[0]).toBe(reactiveValue)
  expect(original[0]).toBe(value)
  // delete
  delete observed[0]
  expect(observed[0]).toBeUndefined()
  expect(original[0]).toBeUndefined()
  // mutating methods
  observed.push(value)
  expect(observed[2]).toBe(reactiveValue)
  expect(original[2]).toBe(value)
})

Шестое одиночное испытание оказалось пройденнымProxyОдно из больших преимуществ реализации реактивных данных: возможность перехватить все изменения данных в массиве. Помните, в vue@2 вам нужно вручную установить массив? В vue@3 нам, наконец, не нужно делать какие-то странные операции и спокойно обновлять массив.

Седьмой одиночный тест:

test('setting a property with an unobserved value should wrap with reactive', () => {
  const observed: any = reactive({})
  const raw = {}
  observed.foo = raw
  expect(observed.foo).not.toBe(raw)
  expect(isReactive(observed.foo)).toBe(true)
})

Собираюсь снова постучать по доске, это черезProxyДва больших преимущества реализации реактивных данных. В vue@2 ключ должен быть объявлен в ответных данных в начале.Если это значение свойства не существует в начале, сначала необходимо установить значение по умолчанию. Благодаря текущему техническому решению значения атрибутов отзывчивых данных vue@3 могут быть добавлены или удалены в любое время.

Восьмой и девятый одиночный тест

test('observing already observed value should return same Proxy', () => {
  const original = { foo: 1 }
  const observed = reactive(original)
  const observed2 = reactive(observed)
  expect(observed2).toBe(observed)
})

test('observing the same value multiple times should return same Proxy', () => {
  const original = { foo: 1 }
  const observed = reactive(original)
  const observed2 = reactive(original)
  expect(observed2).toBe(observed)
})

Эти два одиночных теста показывают, что для одних и тех же необработанных данных выполняется несколько выполнений.reactiveили вложенное выполнениеreactive, возвращаемые результаты — это все те же соответствующие данные. инструкцияreactiveКэш поддерживается в файле, исходные данные используются в качестве ключа, а данные ответа используются в качестве значения.Если ключ уже имеет значение, значение будет возвращено напрямую. Те, у кого есть основа OK js, должны знать, что черезWeakMapЭтот результат может быть достигнут.

Десятый одиночный тест

test('unwrap', () => {
  const original = { foo: 1 }
  const observed = reactive(original)
  expect(toRaw(observed)).toBe(original)
  expect(toRaw(original)).toBe(original)
})

Благодаря этому единственному тесту мы узналиtoRawЭто API, вы можете получить исходные данные через данные ответа. Это объясняетreactiveФайл также должен поддерживать другойWeakMapСделайте обратное отображение.

Одиннадцатый одиночный тест, не вставляйте код, в этом единственном тесте перечислены типы данных, которые нельзя использовать в качестве данных ответа, то есть пять основных типов данных JS +Symbol(После самостоятельного тестирования функция тоже не поддерживает). А для некоторых встроенных специальных типов, таких какPromise,RegExp,Date, эти три типа данных передаются вreactiveОб ошибках не сообщается, и исходные данные возвращаются напрямую.

последний одиночный тест

test('markNonReactive', () => {
  const obj = reactive({
    foo: { a: 1 },
    bar: markNonReactive({ b: 2 })
  })
  expect(isReactive(obj.foo)).toBe(true)
  expect(isReactive(obj.bar)).toBe(false)
})

Вот ссылка на api-markNonReactive, данные объекта, обернутые этим API, не станут ответными данными. Этот API следует реже использовать в реальном бизнесе, и его можно использовать для некоторых специальных оптимизаций производительности.

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

Но это... не похоже на большое дело. Но судя по производительности одиночного теста, он основан на Proxy, и была сделана некоторая обработка границ и вложенности. Тогда это приводит к очень важному вопросу: ** Как vue@3 уведомляет представление об обновлении? Другими словами, когда данные ответа изменяются, как он уведомляет своего потребителя о необходимости что-то сделать? **Это поведение должно быть инкапсулировано в различных обработчиках, таких как set/get of Proxy. Но я еще не знаю, поэтому я могу только продолжать смотреть свысока на другие одиночные тесты.

С самого начала мы знали, чтоreactiveВозвращаемое значениеUnwrapNestedRefsТип, который на первый взгляд особенныйRefТипа, тогда продолжим смотретьref. (На самом деле, этот UnwrapNestedRefs предназначен для получения общего типа вложенных ссылок. Помните, что Unwrap — это глагол, который немного сбивает с толку и будет объяснен позже, когда мы будем говорить о разборе исходного кода)

Ref

Затем посмотрите на первый модульный тест ref:

it('should hold a value', () => {
  const a = ref(1)
  expect(a.value).toBe(1)
  a.value = 2
  expect(a.value).toBe(2)
})

Тогда давайте посмотримrefОбъявление функции, передавая любые данные, может возвращатьRef данные.

image.png
image.png

иRefНеверный тип значения данныхreactiveТип возвращаемого значения функции. ТолькоreactiveДженерики должны наследоваться от объектов (в js этоreactiveПараметр должен быть объектом), иRefДанные не ограничены. Это,Refтип основан наReactiveСпециальный тип данных, который поддерживает другие типы данных в дополнение к объекту.

Вернувшись в одиночный тест, мы видим, что прохождениеrefФункция является числом и также может возвращатьRefобъект, значение которого является числовым значением, переданным в момент времени, иМодификация разрешенаэто значение.

Посмотрите на второй одиночный тест:

it('should be reactive', () => {
  const a = ref(1)
  let dummy
  effect(() => {
    dummy = a.value
  })
  expect(dummy).toBe(1)
  a.value = 2
  expect(dummy).toBe(2)
})

Этот одиночный тест более информативен, а вдруг их большеeffect концепция. Что бы это ни было, эффекту все равно передается функция, а внутри нее выполняется операция присваивания, котораяrefЗначение (a.value) результата, возвращаемого функцией, присваивается фиктивному. Затем эта функция будет выполняться один раз по умолчанию, делая фиктивную 1. Когда a.value изменяется, функция эффекта будет выполняться повторно, делая фиктивным последнее значение.

Это,Если вы передадите метод эффекту, он будет выполнен один раз и будет выполняться повторно всякий раз, когда данные ссылки, от которых он зависит, изменяются.. Это разблокирует чтение доreactiveСомнения: когда данные ответа меняются, как уведомить своих пользователей? Очевидно, через эффект. в любое времяreactiveКогда данные изменяются, запускается зависящий от них метод эффекта.

Я чувствую, что этого нетрудно добиться, поэтому, если бы это был я, это нужно было бы сделать так:

  1. Во-первых, вам нужно поддерживать двухмерную Карту эффектов;
  2. В направленииeffectФункция передает функцию ответа;
  3. Эта функция ответа будет выполнена один раз немедленно.Если на данные ответа ссылаются внутренне, поскольку эти данные были перехвачены с помощью установки/получения через прокси, зависимости этой функции могут быть соответственно собраны, и двумерная карта эффектов может быть обновлено.
  4. Когда какие-либо последующие данные ссылки изменяются (набор триггера), проверьте двухмерную карту, найдите соответствующий эффект и инициируйте их выполнение.

Но есть одна беда,refФункции также поддерживают необъектные данные, в то время как прокси поддерживает только объекты. Итак, в этой библиотекеreactivityДля необъектных данных будет осуществляться слой объектной упаковки, а затем значение будет получено через .value.

Посмотрите на третий одиночный тест:

it('should make nested properties reactive', () => {
  const a = ref({
    count: 1
  })
  let dummy
  effect(() => {
    dummy = a.value.count
  })
  expect(dummy).toBe(1)
  a.value.count = 2
  expect(dummy).toBe(2)
})

Исходные данные, переданные функции ref, становятся объектом, и операции с его прокси-данными также запускают выполнение эффекта. После прочтения сначала возникло любопытство:

  1. Что, если бы он был вложен на один уровень дальше?
  2. Поскольку исходные данные являются объектом, если я изменю исходные данные напрямую, будут ли они синхронизированы с прокси-данными?
  3. Будет ли изменение исходных данных напрямую вызывать эффект?

Поэтому я предполагаю, что 1. он может быть вложенным, 2. он будет синхронизирован и 3. он не вызовет эффекта. Тест порядка был преобразован в:

it('should make nested properties reactive', () => {
    const origin = {
      count: 1,
      b: {
        count: 1
      }
    }
    const a = ref(origin)
    // 声明两个变量,dummy跟踪a.value.count,dummyB跟踪a.value.b.count
    let dummy, dummyB
    effect(() => {
      dummy = a.value.count
    })
    effect(() => {
      dummyB = a.value.b.count
    })
    expect(dummy).toBe(1)
  	// 修改代理数据的第一层数据
    a.value.count = 2
    expect(dummy).toBe(2)

  	// 修改代理对象的嵌套数据
    expect(dummyB).toBe(1)
    a.value.b.count = 2
    expect(dummyB).toBe(2)

  	// 修改原始数据的第一层数据
    origin.count = 10
    expect(a.value.count).toBe(10)
    expect(dummy).toBe(2)
  	// 修改原始数据的嵌套数据
    origin.b.count = 10
    expect(a.value.b.count).toBe(10)
    expect(dummyB).toBe(2)
  })

Результат такой, как я ожидал (на самом деле, я сначала попробовал, просто чтобы написать статью гладко, как я ожидал):

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

Таким образом, мы можем сделать вывод: **за ****Ref****Обновление данных вызовет выполнение эффектов, которые от него зависят. **ТотReactiveКак насчет данных? Давайте продолжим чтение.

Четвертый одиночный тест

it('should work like a normal property when nested in a reactive object', () => {
  const a = ref(1)
  const obj = reactive({
    a,
    b: {
      c: a,
      d: [a]
    }
  })
  let dummy1
  let dummy2
  let dummy3
  effect(() => {
    dummy1 = obj.a
    dummy2 = obj.b.c
    dummy3 = obj.b.d[0]
  })
  expect(dummy1).toBe(1)
  expect(dummy2).toBe(1)
  expect(dummy3).toBe(1)
  a.value++
  expect(dummy1).toBe(2)
  expect(dummy2).toBe(2)
  expect(dummy3).toBe(2)
  obj.a++
  expect(dummy1).toBe(3)
  expect(dummy2).toBe(3)
  expect(dummy3).toBe(3)
})

Наконец-то представлен четвертый одиночный тестreactive. доreactiveВ одиночном тесте пройдены все простые объекты. Здесь некоторые значения свойств в переданном объектеRef данные. и после такого использования этиRefДанные больше не нужно оценивать с помощью .value, даже внутренне вложенныеRefДанные также не требуются. Используя вывод типа TS, мы можем это ясно увидеть.

image.png

В этот момент мы действительно можем понятьreactiveПочему возвращаемый типUnwrapNestedRefs<T>. Из-за дженериковTможет бытьRef<T>, так что этот тип возврата на самом деле означает: распаковать вложенныйRefдженерикиT. В частности, если ** передаетсяreactiveфункция одинRefdata, тип данных, возвращаемый после выполнения функции,RefТип данных необработанных данных данных. ** Этот человек, который не имел большого контакта с TS, не должен этого понимать, я подробнее остановлюсь на этом позже при анализе исходного кода.

Кроме того, этот единственный тест разрешил наши сомнения в предыдущем одиночном тесте,Исправлять ReactiveДанные также вызовут обновление эффекта.

Пятый одиночный тест

it('should unwrap nested values in types', () => {
  const a = {
    b: ref(0)
  }
  const c = ref(a)
  expect(typeof (c.value.b + 1)).toBe('number')
})

Пятый одиночный тест интересен, мы обнаружили, что вложенныеRefДля значения данных вам нужно использовать .value только в начале, а внутренним прокси-данным не нужно повторно вызывать .value. Объясните, что в предыдущем отдельном тесте дляreactiveВложенность передачи функцийRefДанные могут быть развернуты с помощьюreactiveФункция на самом деле не имеет значения, этоRefВозможности самих данных. На самом деле, в соответствии с типом ТС и выводом типа мы также можем видеть:

image.png
image.png

Что, если я установлю больше слоев, например этот:

const a = {
  b: ref(0),
  d: {
    b: ref(0),
    d: ref({
      b: 0,
      d: {
        b: ref(0)
      }
    })
  }
}

const c = ref(a)

В любом случае, это просто набор наборов, набор наборов и набор наборов, согласно выводу типа TS, мы обнаружили, что в этой ситуации нет проблем, пока достаточно первого .value.

image.png

Однако этой возможности не хватало в первой версии, выпущенной Xiaoyou 5 октября, и она не могла вывести данные, вложенные глубже 9 уровней. этоcommitРешив эту проблему, студенты, интересующиеся выводом типа TS, могут взглянуть.

image.png

Шестой одиночный тест

test('isRef', () => {
  expect(isRef(ref(1))).toBe(true)
  expect(isRef(computed(() => 1))).toBe(true)

  expect(isRef(0)).toBe(false)
  // an object that looks like a ref isn't necessarily a ref
  expect(isRef({ value: 0 })).toBe(false)
})

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

последний одиночный тест

test('toRefs', () => {
  const a = reactive({
    x: 1,
    y: 2
  })

  const { x, y } = toRefs(a)

  expect(isRef(x)).toBe(true)
  expect(isRef(y)).toBe(true)
  expect(x.value).toBe(1)
  expect(y.value).toBe(2)

  // source -> proxy
  a.x = 2
  a.y = 3
  expect(x.value).toBe(2)
  expect(y.value).toBe(3)

  // proxy -> source
  x.value = 3
  y.value = 4
  expect(a.x).toBe(3)
  expect(a.y).toBe(4)

  // reactivity
  let dummyX, dummyY
  effect(() => {
    dummyX = x.value
    dummyY = y.value
  })
  expect(dummyX).toBe(x.value)
  expect(dummyY).toBe(y.value)

  // mutating source should trigger effect using the proxy refs
  a.x = 4
  a.y = 5
  expect(dummyX).toBe(4)
  expect(dummyY).toBe(5)
})

Этот единственный тест предназначен дляtoRefsэтого апи. По единому измерению,toRefs иrefРазница в том,refпревратит входящие данные вRefтип, покаtoRefsТребуется, чтобы входящие данные были объектом, а затем данные первого слоя этого объекта преобразуются вRef тип. Я не знаю, для чего это можно использовать, просто знаю, какой эффект.

Пока что, прочитав единственный тест ref, вы можете примерно почувствовать, что наиболее важным назначением ref является,Реализовать захват необъектных данных. Другими словами, кажется, что другого особого применения нет. На самом деле в тестовом файле эффекта на данный момент только тестreactiveДанные запускают метод эффекта.

Тогда давайте посмотрим на тестовый файл эффекта.

Effect

effectПоведение на самом деле из приведенного выше тестового файла мы уже можем понять. Главное — следить за изменениями в ответных данных и запускать выполнение функции монитора. Описание простое, ноeffectСуществует множество отдельных измерений для , с 39 вариантами использования, более 600 строк кода и множеством соображений пограничного случая. Что касается эффектов, я не буду перечислять их по одному. Позвольте мне сначала прочитать его для вас, затем обобщить его в несколько небольших пунктов, непосредственно обобщить ключевые выводы и при необходимости вставить соответствующий тестовый код.

базовая способность

  • Метод, переданный эффекту, будет выполнен сразу один раз.. (Если второй параметр не проходит { lazy: true }, что зависит от исходного кода, одиночный тест не проходит, и заинтересованные студенты могут перейти на PR).
  • reactiveОн может наблюдать за изменениями данных в цепочке прототипов и контролироваться функцией эффекта, а также может наследовать средства доступа к свойствам (get/set) в цепочке прототипов.
it('should observe properties on the prototype chain', () => {
  let dummy
  const counter = reactive({ num: 0 })
  const parentCounter = reactive({ num: 2 })
  Object.setPrototypeOf(counter, parentCounter)
  effect(() => (dummy = counter.num))

  expect(dummy).toBe(0)
  delete counter.num
  expect(dummy).toBe(2)
  parentCounter.num = 4
  expect(dummy).toBe(4)
  counter.num = 3
  expect(dummy).toBe(3)
})
  • Любая операция чтения любых данных ответа может быть выполнена в ответ, а любая операция записи любых данных ответа может быть отслежена. пока не:
    • Ключевым значением обновленных данных является некоторое встроенное специальное значение Symbol, такое какSymbol.isConcatSpreadable(в основном не задействованы в повседневном использовании)
    • Несмотря на то, что операция записи выполнена, данные не изменились, и функция прослушивателя не будет запущена.
it('should not observe set operations without a value change', () => {
  let hasDummy, getDummy
  const obj = reactive({ prop: 'value' })

  const getSpy = jest.fn(() => (getDummy = obj.prop))
  const hasSpy = jest.fn(() => (hasDummy = 'prop' in obj))
  effect(getSpy)
  effect(hasSpy)

  expect(getDummy).toBe('value')
  expect(hasDummy).toBe(true)
  obj.prop = 'value'
  expect(getSpy).toHaveBeenCalledTimes(1)
  expect(hasSpy).toHaveBeenCalledTimes(1)
  expect(getDummy).toBe('value')
  expect(hasDummy).toBe(true)
})
  • Операции с необработанными данными данных ответа не будут запускать функцию прослушивателя.
  • Функция слушателя может вводить другую функцию слушателя.
  • Каждый раз, когда эффект выполняется, возвращается новая функция слушателя, даже если передается одна и та же функция.
it('should return a new reactive version of the function', () => {
  function greet() {
    return 'Hello World'
  }
  const effect1 = effect(greet)
  const effect2 = effect(greet)
  expect(typeof effect1).toBe('function')
  expect(typeof effect2).toBe('function')
  expect(effect1).not.toBe(greet)
  expect(effect1).not.toBe(effect2)
})
  • в состоянии пройтиstopapi, завершите функцию прослушивателя, чтобы продолжить прослушивание. (Я чувствую, что могу добавить большеstart,Заинтересованные студенты могут упомянуть PR для Сяою)
it('stop', () => {
  let dummy
  const obj = reactive({ prop: 1 })
  const runner = effect(() => {
    dummy = obj.prop
  })
  obj.prop = 2
  expect(dummy).toBe(2)
  stop(runner)
  obj.prop = 3
  expect(dummy).toBe(2)

  // stopped effect should still be manually callable
  runner()
  expect(dummy).toBe(3)
})

специальная логика

  • ** может избежать бесконечных циклов, вызванных неявной рекурсией,Если данные ответа изменены внутри функции прослушивателя или несколько функций прослушивателя влияют друг на друга. Но это не предотвратит явную рекурсию, такую ​​как вызов самого себя цикла функции слушателя.
it('should avoid implicit infinite recursive loops with itself', () => {
  const counter = reactive({ num: 0 })

  const counterSpy = jest.fn(() => counter.num++)
  effect(counterSpy)
  expect(counter.num).toBe(1)
  expect(counterSpy).toHaveBeenCalledTimes(1)
  counter.num = 4
  expect(counter.num).toBe(5)
  expect(counterSpy).toHaveBeenCalledTimes(2)
})

it('should allow explicitly recursive raw function loops', () => {
  const counter = reactive({ num: 0 })
  const numSpy = jest.fn(() => {
    counter.num++
    if (counter.num < 10) {
      numSpy()
    }
  })
  effect(numSpy)
  expect(counter.num).toEqual(10)
  expect(numSpy).toHaveBeenCalledTimes(10)
})
  • **Если зависимости внутри эффекта имеют логические ответвления, функция слушателя будет повторно обновлять зависимости после каждого выполнения. **Следующим образом: когдаobj.run заfalse час,conditionalSpyПосле повторного выполнения зависимости мониторинга обновляются, и последующиеobj.propКак изменить, функция прослушивателя больше не будет выполняться.
it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
  let dummy
  const obj = reactive({ prop: 'value', run: true })

  const conditionalSpy = jest.fn(() => {
    dummy = obj.run ? obj.prop : 'other'
  })
  effect(conditionalSpy)

  expect(dummy).toBe('value')
  expect(conditionalSpy).toHaveBeenCalledTimes(1)
  obj.run = false
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)
  obj.prop = 'value2'
  expect(dummy).toBe('other')
  expect(conditionalSpy).toHaveBeenCalledTimes(2)
})

ReactiveEffectOptions

effectтакже может принимать второй параметрReactiveEffectOptions, параметры следующие:

export interface ReactiveEffectOptions {
  lazy?: boolean
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}
  • lazy: расчет с задержкой, при значении true входящий эффект не будет выполняться немедленно.
  • computed: В одиночном тесте это не отражено, не знаю для чего он используется, может это связано с названием.computedНеважно, отпусти.
  • scheduler: функция планировщика, принятый входной параметр run - это функция, переданная эффекту, если передан планировщик, через него может быть вызвана функция слушателя.
  • onStop:пройти черезstopСобытие запускается, когда функция прослушивателя завершается.
  • onTrack:Только для отладки. Запускается во время сбора зависимостей (фаза получения).
  • onTrigger:Только для отладки. Запускается перед выполнением функции прослушивателя после запуска обновления.

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

Computed

Вычисляемые свойства. Этот одноклассник, написавший vue, должен знать, что это значит. мы смотрим наreactivityКак именно в .

первый одиночный тест

it('should return updated value', () => {
  const value = reactive<{ foo?: number }>({})
  const cValue = computed(() => value.foo)
  expect(cValue.value).toBe(undefined)
  value.foo = 1
  expect(cValue.value).toBe(1)
})

В направленииcomputedПередайте функцию получения, функция внутренне зависит отReactiveДанные после выполнения функции возвращают объект вычисления, значение которого является возвращаемым значением функции. когда это зависитReactiveПри изменении данных расчетные данные могут синхронизироваться, как если быRefАх. по фактуrefМы уже знаем в тестовом файле, что возвращаемый результат вычислений также является своего родаRef данные.

image.png

Проверьте тип TS, конечно жеComputedRefунаследовано отRef ,по сравнению сRefДобавлено свойство эффекта только для чтения, типReactiveEffect. Как вы можете догадаться, значение атрибута эффекта здесь должно быть тем, что мы передаем вcomputedРасчетная функция , а затемeffectРезультат, возвращаемый после выполнения функции. Кроме того, егоvalueдоступен только для чтения, заявивcomputedЗначение возвращаемого результата доступно только для чтения.

второй одиночный тест

it('should compute lazily', () => {
  const value = reactive<{ foo?: number }>({})
  const getter = jest.fn(() => value.foo)
  const cValue = computed(getter)

  // lazy
  expect(getter).not.toHaveBeenCalled()

  expect(cValue.value).toBe(undefined)
  expect(getter).toHaveBeenCalledTimes(1)

  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(1)

  // should not compute until needed
  value.foo = 1
  expect(getter).toHaveBeenCalledTimes(1)

  // now it should compute
  expect(cValue.value).toBe(1)
  expect(getter).toHaveBeenCalledTimes(2)

  // should not compute again
  cValue.value
  expect(getter).toHaveBeenCalledTimes(2)
})

Этот единственный тест говорит намcomputedМножество функций:

  • отличается отeffect ,В направленииcomputedпрошедшийgetterФункция не будет выполняться немедленно, она будет выполняться только тогда, когда данные будут фактически использованы.
  • Не каждый раз, когда значение нужно вызывать сноваgetterфункция, иgetterОн не будет запускаться повторно при изменении данных, от которых зависит функция, и будет запускаться только при повторном использовании данных расчета после изменения зависимых данных.getterфункция.

В первом отдельном тесте мы предполагаемComputedRefСвойство эффекта , передаетсяeffectпередача методаgetterФункция прослушивателя, сгенерированная функцией. Но когда effectВ одном тесте, как только зависимые данные изменятся, функция слушателя будет выполнена немедленно, как и здесь.computedпроизводительность непостоянна. В этом должно быть что-то хитрое!

в предыдущем разделеEffectНаконец, мы находимeffectВторой параметр функции — это элемент конфигурации, и одна из конфигураций называется вычисляемой, которая не рассматривается в отдельном тесте. Предполагается, что этот элемент конфигурации реализует здесь отложенный расчет вычисляемых данных.

третий одиночный тест

it('should trigger effect', () => {
  const value = reactive<{ foo?: number }>({})
  const cValue = computed(() => value.foo)
  let dummy
  effect(() => {
    dummy = cValue.value
  })
  expect(dummy).toBe(undefined)
  value.foo = 1
  expect(dummy).toBe(1)
})

Этот единственный тест доказывает, что мыRefГипотезы, представленные в главе 1:Если есть эффект, который зависитcomputedвозвращает данные, то при их изменении эффект тоже будет выполняться.

что еслиcomputedХотя возвращаемые данные не изменились, но изменились его зависимые данные? Это вызоветeffectЧто насчет казни? Я думаю, еслиcomputedЕсли значение не изменилось, это не приведет к повторному выполнению функции прослушивателя, поэтому измените тест порядка:

it('should trigger effect', () => {
  const value = reactive<{ foo?: number }>({})
  const cValue = computed(() => value.foo ? true : false)
  let dummy
  const reactiveEffect = jest.fn(() => {
    dummy = cValue.value
  })
  effect(reactiveEffect)
  expect(dummy).toBe(false)
  expect(reactiveEffect).toHaveBeenCalledTimes(1)
  value.foo = 1
  expect(dummy).toBe(true)
  expect(reactiveEffect).toHaveBeenCalledTimes(2)
  value.foo = 2
  expect(dummy).toBe(true)
  expect(reactiveEffect).toHaveBeenCalledTimes(2)
})

Потом я понял, что ошибался.reactiveEffectзависит отcValue,cValueзависит отvalue ,если толькоvalueизмениться, несмотря ни на чтоcValueЕсли есть какие-либо изменения, он будет запущен повторноreactiveEffect. Я чувствую, что здесь можно оптимизировать, и заинтересованные студенты могут пойти в PR.

Четвертый одиночный тест

it('should work when chained', () => {
  const value = reactive({ foo: 0 })
  const c1 = computed(() => value.foo)
  const c2 = computed(() => c1.value + 1)
  expect(c2.value).toBe(1)
  expect(c1.value).toBe(0)
  value.foo++
  expect(c2.value).toBe(2)
  expect(c1.value).toBe(1)
})

Этот единственный тест показываетcomputed изgetterФункция может зависеть от другойcomputed данные.

Пятый и шестой одиночные тесты относятся к использованию меняющихся цветов.computed. Передаваемая концепция: использованиеcomputedДанные аналогичны использованию обычных данных ответа и могут корректно инициировать выполнение функции мониторинга.

Седьмой одиночный тест

it('should no longer update when stopped', () => {
  const value = reactive<{ foo?: number }>({})
  const cValue = computed(() => value.foo)
  let dummy
  effect(() => {
    dummy = cValue.value
  })
  expect(dummy).toBe(undefined)
  value.foo = 1
  expect(dummy).toBe(1)
  stop(cValue.effect)
  value.foo = 2
  expect(dummy).toBe(1)
})

Этот единственный тест представляетstopЭтот апи черезstop(cValue.effect)Прервано обновление ответа для этих вычисляемых данных.

Два последних одиночных теста

it('should support setter', () => {
  const n = ref(1)
  const plusOne = computed({
    get: () => n.value + 1,
    set: val => {
      n.value = val - 1
    }
  })
  expect(plusOne.value).toBe(2)
  n.value++
  expect(plusOne.value).toBe(3)
  plusOne.value = 0
  expect(n.value).toBe(-1)
})
it('should trigger effect w/ setter', () => {
  const n = ref(1)
  const plusOne = computed({
    get: () => n.value + 1,
    set: val => {
      n.value = val - 1
    }
  })
  let dummy
  effect(() => {
    dummy = n.value
  })
  expect(dummy).toBe(1)
  plusOne.value = 0
  expect(dummy).toBe(-1)
})

Эти два отдельных теста более важны. прежде чем мыcomputedпросто пройтиgetterфункцию, и ееvalueДоступен только для чтения и не может напрямую изменять возвращаемое значение. Дайте нам знать здесь,computedВы также можете передать объект, содержащий методы get/set. получить этоgetterфункции, это легче понять.setterВходные параметры, полученные функцией, назначаютсяcomptuedзначение Значение данных. Таким образом, в приведенном выше случае использования
plusOne.value = 0, так чтоn.value = 0 - 1, запустить сноваdummyстановится -1.

Пока мы почти закончилиreactivityПонятие системы, остальныеreadonly иcollections.readonlyЕсть много одиночных тестовых файлов, но на самом деле концепция очень проста, т.е.reactiveВерсия только для чтения.collectionsОдиночный тест должен покрытьMap,Set,WeakMap,WeakSetответ обновлен, и это не должно быть большой проблемой, если вы пока не посмотрите его.

Суммировать

После разбора у нас должно быть четкое представление об основных внутренних API.Подведем итоги и повторим:

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

ref: это файл, который больше всего влияет на чтение кода (легко спутать связь между ним и реактивным, когда вы смотрите на код), но чтобы действительно понять его, вам нужно внимательно прочитать код. Рекомендуется оставить его в покое, прежде чем разъяснять другую логику.... когда ее не существует. Просто знайте, что самая важная роль этого файла — предоставить наборRef тип.

effect: принимает функцию и возвращает новую функцию слушателяreactiveEffect. Если функция прослушивателя внутренне полагается на реактивные данные, функция прослушивателя будет запущена при изменении данных.

computed: Вычислить данные, принять функцию получения или объект, содержащий поведение получения/установки, и вернуть реактивные данные. Он также запускает reactiveEffect, если он изменяется.

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

image.png

Но эта картинка, я не могу гарантировать, что она правильная, потому что я не закончил исходный код. На этой неделе я уделю немного времени написанию настоящего анализа исходного кода.


Автор этой статьи: Ant Insurance - Experience Technology Group - A Xiang

Адрес Наггетс:Сян Сюэчан