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

Vue.js

предисловие

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

Если мы читали предыдущие статьи, мы уже должны понимать, как украсть данные. Но есть еще две большие проблемы, которые не решены, а именно, как собирать зависимости и как запускать функцию слушателя. Из предыдущей статьи мы можем примерно догадаться, что:effectВ функцию передается примитивная функция, будет создана функция слушателя, которая сразу же будет выполнена один раз. И при первом выполнении он может пройтиtrackЗависимости собираются, и при написании операций проходятtriggerснова запустить эту функцию прослушивателя. А внутренняя логика этих основных методов находится в файле эффекта.

Effect

внешний импорт

import { OperationTypes } from './operations'
import { Dep, targetMap } from './reactive'
import { EMPTY_OBJ, extend } from '@vue/shared'

Не так много внешних представлений файлов эффектов,EMPTY_OBJотносится к пустому объекту{},extendэто метод, расширяющий объект, аналогичный тому, что используется в lodash._.extend. а такжеDepа такжеtargetMapименно то, что нам нужно вeffectИсследовано в.

Типы и константы

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

// 迭代行为标识符
export const ITERATE_KEY = Symbol('iterate')

// 监听函数的配置项
export interface ReactiveEffectOptions {
  // 延迟计算,为true时候,传入的effect不会立即执行。
  lazy?: boolean
  // 是否是computed数据依赖的监听函数
  computed?: boolean
  // 调度器函数,接受的入参run即是传给effect的函数,如果传了scheduler,则可通过其调用监听函数。
  scheduler?: (run: Function) => void
  // **仅供调试使用**。在收集依赖(get阶段)的过程中触发。
  onTrack?: (event: DebuggerEvent) => void
  // **仅供调试使用**。在触发更新后执行监听函数之前触发。
  onTrigger?: (event: DebuggerEvent) => void
  //通过 `stop` 终止监听函数时触发的事件。
  onStop?: () => void
}

// 监听函数的接口
export interface ReactiveEffect<T = any> {
  // 代表这是一个函数类型,不接受入参,返回结果类型为泛型T
  // T也即是原始函数的返回结果类型
  (): T
  [effectSymbol]: true
  // 暂时未知,猜测是某种开关
  active: boolean
  // 监听函数的原始函数
  raw: () => T
  // 暂时未知,根据名字来看是存一些依赖
  // 根据类型来看,存放是二维集合数据,一维是数组,二维是ReactiveEffect的Set集合
  deps: Array<Dep> // === Array<Set<ReactiveEffect>>
  // 以下同上述ReactiveEffectOptions
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

// debugger事件,这个基本不需要解释
export type DebuggerEvent = {
  effect: ReactiveEffect
  target: object
  type: OperationTypes
  key: any
} & DebuggerEventExtraInfo

// debugger拓展信息
export interface DebuggerEventExtraInfo {
  newValue?: any
  oldValue?: any
  oldTarget?: Map<any, any> | Set<any>
}

// 存放监听函数的数组
export const effectStack: ReactiveEffect[] = []

основной скелет

// 是否是监听函数
export function isEffect(fn: any): fn is ReactiveEffect {
  return fn != null && fn._isEffect === true
}
// 生成监听函数的effect方法
export function effect<T = any>(
  // 原始函数
  fn: () => T,
  // 配置项
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果该函数已经是监听函数了,那赋值fn为该函数的原始函数
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 创建一个监听函数
  const effect = createReactiveEffect(fn, options)
  // 如果不是延迟执行的话,立即执行一次
  if (!options.lazy) {
    effect()
  }
  // 返回该监听函数
  return effect
}
// 创建监听函数的方法
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 创建监听函数,通过run来包裹原始函数,做额外操作
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  // 监听函数标识符
  effect._isEffect = true
  // 依旧不知道做什么用的开关
  effect.active = true
  // 原始函数
  effect.raw = fn
  // 应该是存什么依赖的数组
  effect.deps = []
  // 获取配置数据
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  return effect
}

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

// 如果该函数已经是监听函数了,那赋值fn为该函数的原始函数
if (isEffect(fn)) {
  fn = fn.raw
}

Эта логика означает, что если переданная функция уже является функцией слушателя, она не вернет старую функцию слушателя напрямую, а создаст новую функцию слушателя с ее исходной функцией, что немного отражено в нашей единственной тестовой статье.effectМетод всегда возвращает новую функцию, но я не знаю, почему он так устроен.

Продолжай читатьrunметод.

// 监听函数执行器
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  // 如果这个active开关是关上的,那就执行原始方法,并返回
  if (!effect.active) {
    return fn(...args)
  }
  // 如果监听函数栈中并没有此监听函数,则:
  if (!effectStack.includes(effect)) {
    // 还不知道具体做什么用的清除行为
    cleanup(effect)
    try {
      // 将本effect推到effect栈中
      effectStack.push(effect)
      // 执行原始函数并返回
      return fn(...args)
    } finally {
      // 执行完以后将effect从栈中推出
      effectStack.pop()
    }
  }
}

// 传递一个监听函数,做某种清除操作
function cleanup(effect: ReactiveEffect) {
  // 获取本effect的deps,然后循环清除存储了自身effect的引用
  // 最后将deps置为空
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

этоrunметод, с момента введенияcleanupа такжеeffectStack, Есть еще несколько мнений, и я немного не понимаю.

Вопрос 1:effect.activeКакова логика?

Мы ищем и модифицируемeffect.activeметод, только одно место:

export function stop(effect: ReactiveEffect) {
  // 如果active为true,则触发effect.onStop,并且把active置为false。
  if (effect.active) {
    cleanup(effect)
    if (effect.onStop) {
      effect.onStop()
    }
    effect.active = false
  }
}

Если вы читали статью об одном тесте, вы, возможно, помните, что естьstopЭтот API, передавая ему параметры функции слушателя, может привести к тому, что функция слушателя потеряет свою отзывчивую логику. ЭтоactiveЛогика ясна.

Вопрос 2: С момента предварительного исполненияeffectStack.push(effect), после выполненияeffectStack.pop(). Тогда почему он все еще существуетeffectStack.includes(effect)Как насчет этой ситуации?

Не паникуйте, когда у вас возникнут проблемы, помните, у нас есть единый тест Дафа! Мы убираем эту логику, а затем запускаем одиночный тест эффекта, и тогда мы обнаружим, что два одиночных теста выброшены неправильно.

✕ следует избегать неявных бесконечных рекурсивных циклов с самим собой (26 мс)

✕ должен разрешать явно рекурсивные циклы необработанных функций (12 мс)

Поищите один тестовый код еще раз, и мы узнаем, что это следует избегать рекурсивных петель. Например, в функции монитора зависимые данные изменяются, а функция монитора будет продолжена непрерывно в соответствии с нормальной логикой. но сквозьeffectStack.includes(effect)Такая логика суждения естественным образом избегает рекурсивных циклов.

Тогда есть еще более загадочноеcleanup. Основная причина в том, что я этого не знаюdeps: Array<Set<ReactiveEffect>>как это написано. Почему внутри функции прослушивателя находится куча коллекций функций прослушивателя? Вот зачем опять удалять. Мы пока оставляем вопросы и отвечаем на них позже.

Кроме того, изeffectфункционировать, чтобыrunфункции, мы можем найти очевидную неразумность:

// ...监听函数类型
interface ReactiveEffect<T = any> {
  (): T
  // ...
}
const effect = function reactiveEffect(...args: unknown[]): unknown {
  return run(effect, fn, args)
} as ReactiveEffect
// ...
fn(...args)

Как видите, недавно созданная функция слушателяreactiveEffect, оказывается, параметры есть, а тип интерфейса исходной функции и функции слушателя() => T, параметры не передаются... Тут нестыковка. По логике вещей, поскольку основная процедура функции мониторинга по-прежнему запускается автоматически, никаких параметров быть не должно. так вотargsНа самом деле это не имеет смысла. Если вы действительно хотите его передать, в среде ts он также сообщит об ошибке из-за несоответствия типа.

Если вы действительно хотите поддерживать передачу параметров, чтобы сохранить исходный тип функции (в настоящее время неизвестный), вам нужно написать много вывода типа. И должно быть ограничение, что функция ввода параметра должна быть необязательной, потому что переданная функция будет выполняться один раз... так что не передавайте параметр... В любом случае, кажется, что здесь можно оптимизировать.

Вернись, теперь наша самая большая проблема, на самом деле, этоeffectПочему он снова существуетdeps, и как он включается. Логика этого явно видна все время доtrackа такжеtriggerсередина.

track

Перед просмотром трека я должен его просмотретьtargetMap, прежде чем мы примерно узнаем, что это такая структура:

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()

Посмотрите, это так:WeakMap<Target, Map<string | symbol, Set<ReactiveEffect>>>.

Это трехмерная структура данных.TargetМы знали это раньше, необработанные данные были украдены. С нашими текущими знаниями (и моим предварительным уведомлением) мы можем знать. двухмерныйKeyToDepMapизkey, который является ключом свойства исходного объекта. а такжеDepхранится监听函数effectколлекция. Тогда посмотри еще разtrackКод:

// 收集依赖的函数
export function track(
  // 原始数据
  target: object,
  // 操作行为
  type: OperationTypes,
  key?: string | symbol
) {
  // 如果shouldTrack开关关闭,或effectStack中不存在监听函数,则无需要收集
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  // 获取effect栈最后一个effect
  const effect = effectStack[effectStack.length - 1]
  // 是迭代操作的话,重新赋值一下key
  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }
  // 获取二维map,不存在的话,则初始化
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取effect集合,无则初始化
  let dep = depsMap.get(key!)
  if (dep === void 0) {
    depsMap.set(key!, (dep = new Set()))
  }
  // 如果集合中,没有刚刚获取的最后一个effect,则将其add到集合dep中
  // 并在effect的deps中也push这个effects集合dep
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
    // 开发环境下时,触发track钩子函数
    if (__DEV__ && effect.onTrack) {
      effect.onTrack({
        effect,
        target,
        type,
        key
      })
    }
  }
}

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

Вопрос 1: Почему изeffectStackполученный в концеeffectзависит отtargetфункция слушателя.

Это из-за этой логики:

try {
  // 将本effect推到effect栈中
  effectStack.push(effect)
  // 执行原始函数并返回
  return fn(...args)
} finally {
  // 执行完以后将effect从栈中推出
  effectStack.pop()
}

fnЗависимые данные ссылаются внутри, выполняютfnзапускать эти данныеget, а затем отправился вtrack, а на этот разeffectStackХвост стека — это именноeffect. Но здесь есть скрытое ограничение,fn, то есть перейти кeffectИсходная функция, внутренняя логика зависимостей должна быть синхронизирована. Например, это не работает:

let dummy
const obj = reactive({ prop: 1 })
effect(() => {
  setTimeout(() => {
    dummy = obj.prop
  }, 1000)
})
obj.prop = 2

obj.propизменения не приведут к повторному выполнению функции прослушивателя.fnне может быть однимasyncфункция.

Однако в vue3watchфункция поддерживаетсяasync. Здесь это не обсуждается, в основном я не исследовал...

Вопрос 2:targetMapа такжеeffectКак именно выглядит сопоставление зависимостей.

targetMapизdepsMapсохраненeffectколлекцияdep,а такжеeffectсохранил это сноваdep... немного запутанно на первый взгляд, и почему вы хотите хранить в обоих направлениях?

На самом деле, мы только что увидели часть причины, т.runвыполняется в методеcleanup. Перед каждым запуском будет выполняться:

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

читай внимательноtrackметод, мы можем примерноtargetMap,effect.depsКакие именно данные в нем хранятся. Объясните в два шага:

  1. Для отзывчивых данных он находится вtargetMapесть одинMapданные (я называю это «отображение зависимостей ответов»). Этот ответ зависит от отображенияkeyзначение атрибута ответных данных,valueвсе функции прослушивателя, которые используют значение этого атрибута данных ответа, то естьSetсобиратьdep.
  2. А для функции слушателя он будет хранить всеdep.

Вот вопрос,effectЗачем хранить такие рекурсивные данные? Это потому, что пройтиcleanupметод удаляет себя из карты зависимостей ответа перед выполнением. затем выполняет свою собственную исходную функциюfn, а затем активировать данныеget, затем срабатываетtrack, а затем поместите этоeffectдобавлено в соответствующийSet<ReactiveEffect>середина. Это немного волшебно: перед каждым выполнением я удаляю себя из карты зависимостей, а во время выполнения добавляю себя обратно.

Для такой необъяснимой логики пришло время снова использовать метод одной меры. я кладуcleanupАннотируйте логику , а затем запустите модульный тест на некоторое время, и тогда вы обнаружите, что следующий модульный тест терпит неудачу:

✕ не должен срабатывать при изменении свойства, которое используется в неактивной ветке (3 мс)

Логика модульного теста такова:

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)
})

О~~~ Теперь мы понимаем, что это за такая ситуация с обработкой ветвей. Поскольку в функции мониторинга зависимые данные могут отличаться из-за утверждений условного суждения, например, если. Поэтому каждый раз, когда функция выполняется, зависимости должны обновляться снова. Так что естьcleanupэта логика.

Таким образом, мы в основном понимаемtrackрутинаtragetMapлогика, тогда изучайtrigger.

trigger

Давайте сначала кратко рассмотрим эту функцию.

// 触发监听函数的方法
export function trigger(
  target: object, // 原始数据
  type: OperationTypes, // 写操作类型
  key?: unknown, // 属性key
  extraInfo?: DebuggerEventExtraInfo // 拓展信息
) {
  // 获取原始数据的响应依赖映射,没有的话,说明没被监听,直接返回
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  // 声明一个effect集合
  const effects = new Set<ReactiveEffect>()
  // 声明一个计算属性集合
  const computedRunners = new Set<ReactiveEffect>()
  // OperationTypes.CLEAR 代表是集合数据的清除方法,会清除集合数据的所有项
  // 如果是清除操作,那就要执行依赖原始数据的所有监听方法。因为所有项都被清除了。
  // addRunners并未执行监听函数,而是将其推到一个执行队列中,待后续执行
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // key不为void 0,则说明肯定是SET | ADD | DELETE这三种操作
    // 然后将依赖这个key的所有监听函数推到相应队列中
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // 如果是增加或者删除数据的行为,还要再往相应队列中增加监听函数
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      // 如果原始数据是数组,则key为length,否则为迭代行为标识符
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  // 声明一个run方法
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // 大致翻译一下:计算属性的getter函数必须先执行,因为正常的监听函数,可能会依赖于计算属性数据

  // 运行所有计算数据的监听方法
  computedRunners.forEach(run)
  // 运行所有寻常的监听函数
  effects.forEach(run)
}

КромеaddRunnersа такжеscheduleRunЗа пределами черного ящика в целом понятна другая логика. Только здесь:

if (key !== void 0) {
  addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
  const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
  addRunners(effects, computedRunners, depsMap.get(iterationKey))
}

Каждый раз, когда речь идет о массивах/коллекциях и итеративном поведении, это всегда трудно понять, суть в том, что мы меньше понимаем базовые данные. Здесь мы не понимаем, при каких обстоятельствах мы перейдем ко второй логике, и эта ситуация обязательно повторится.addRunners, это имеет значение? Ничего страшного, просто запускайте одиночный тест, если не понимаете, второй если комментируем, а потом запускаемeffectОдиночный тест:

✕ следует соблюдать итерацию (5 мс)

✕ следует наблюдать неявные изменения длины массива (1 мс)

✕ следует соблюдать перечисление (1 мс)

Обнаружено, что, как показано в комментариях, модульный тест, связанный с итератором, неверен, и в некоторых случаях функция прослушивателя не запускается. Если вы прочиталиreactvieизhandlers, мы можем примерно предположить, почему. Конкретный пример, аналогичный случаю в одиночном тесте (немного модифицированный одиночный тест, так проще понять):

it('should observe iteration', () => {
  let dummy
  const list = reactive<string[]>([])
  effect(() => (dummy = list.join(' ')))

  expect(dummy).toBe('')
  list.push('Hello')
  expect(dummy).toBe('Hello')
})

здесьeffectне использует определенный индекс массива,handlersсерединаtrackНа самом деле, он угнал массивlengthсвойства (на самом делеjoin方法, но здесь бесполезен), и отслеживать его изменения. при этих обстоятельствах,depsMapФактическиlengthа такжеeffectsкартографические отношения. (ЗачемtrackприбытьlengthОтвет в предыдущей статье)

А в предыдущей статьеreactiveВ статье мы знаем. множествоpushИзменение длины, вызванное поведением, больше не будет вызваноtrigger... поэтому в этом единственном тесте он будет запущен только один разkeyдля0,valueдляHelloизtrigger.

В этом случае, потому чтоkeyдля0,такif(key !== void 0)действительно верно, ноdepsMap.get(0)на самом деле пусто. а такжеdepsMap.get('length')действительно актуальноeffect, поэтому должна быть вторая логика, чтобы дополнить ее.

Тогда снова возникает вопрос... Что делать с такой операцией?

it('should observe iteration', () => {
  let dummy
  const list = reactive<number[]>([])
  effect(() => {
    dummy = list.length + list[0] || 0
  })

  expect(dummy).toBe(0)
  list.push(1)
  expect(dummy).toBe(2)
})

В этом случае дваifлогика будет работать, иdepsMap.get(key)а такжеdepsMap.get(iterationKey)иметь ценность. Будет ли эффект выполняться дважды? Вообще-то нет. мы продолжаем видетьaddRunnersа такжеscheduleRun.

// 将effect添加到执行队列中
function addRunners(
  effects: Set<ReactiveEffect>, // 监听函数集合
  computedRunners: Set<ReactiveEffect>, // 计算函数集合
  effectsToAdd: Set<ReactiveEffect> | undefined // 待添加的监听函数或计算函数集合
) {
  // 如果effectsToAdd不存在,啥也不干
  if (effectsToAdd !== void 0) {
    // 遍历effectsToAdd
    // 如果是计算函数,则推到computedRunners,否则推到effects
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

function scheduleRun(
  effect: ReactiveEffect,
  target: object,
  type: OperationTypes,
  key: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  // 开发环境,并且配置了onTrigger,则触发该函数,传入相应数据
  if (__DEV__ && effect.onTrigger) {
    effect.onTrigger(
      extend(
        {
          effect,
          target,
          key,
          type
        },
        extraInfo
      )
    )
  }
  // 如果配置了自定义的执行器方法,则执行该方法
  // 否则执行effect
  if (effect.scheduler !== void 0) {
    effect.scheduler(effect)
  } else {
    effect()
  }
}

Эти два метода выглядят очень мощными, но на самом деле они очень просты, т.effectsдобавлено в соответствующийSetзадавать. еслиcomputedМетод расчета перенесен наcomputedRunners, иначе нажмите обычныйeffectsв коллекции. Поскольку обаSetсобирать.

const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()

Итак, если добавляется неоднократно, он автоматически будет тяжелым. Так два вышеifЕсли в логике будет получена одна и та же функция слушателя, она будет автоматически дедуплицирована и не будет выполняться несколько раз. весьeffectЭто так просто. Не слишком много наворотов и свистков,runВот и все.

Кроме того, прочитав, мы также можем узнать,computedМетод — это специальный класс с возвращаемым значением.effect. Тогда давайте посмотрим.

computed

оcomputedНе буду вдаваться в подробности, в принципе всем все понятно, а сразу расставлю основные моменты.

// 函数重载
// 入参为getter函数
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
// 入参为配置项
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
// 真正的函数实现
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      dirty = true
    }
  })
  return {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      // When computed effects are accessed in a parent effect, the parent
      // should track all the dependencies the computed property has tracked.
      // This should also apply for chained computed properties.
      trackChildRun(runner)
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  }
}

Как видите, логика довольно проста. первый,computedВозвращаемое значение представляет собойRefтип данных. каждый разgetзначение, если функция слушателя не была выполнена, т.е.dirty === true, выполните функцию слушателя один раз. Чтобы избежать повторного получения, повторите выполнение.

Вообще говоря, кcomputedпрошло этоfunctionТип, просто получить данные класса расчета, возвращаемые данные не могут быть изменены. Но есть исключения, если элемент конфигурации передается, указанныйgetterа такжеsetterметод, который также позволяет вносить ручные измененияcomputedданные.

Общая логика относительно проста, есть только однаtrackChildRunНужно понять больше:

function trackChildRun(childRunner: ReactiveEffect) {
  if (effectStack.length === 0) {
    return
  }
  // 获取父级effect
  const parentRunner = effectStack[effectStack.length - 1]
  // 遍历子级,也即是本effect,的deps
  for (let i = 0; i < childRunner.deps.length; i++) {
    const dep = childRunner.deps[i]
    // 如果子级的某dep中没有父级effect,则将父级effect添加本dep中,然后更新父级effect的deps
    if (!dep.has(parentRunner)) {
      dep.add(parentRunner)
      parentRunner.deps.push(dep)
    }
  }
}

Просто смотреть на код, пытаясь понять смысл, на самом деле довольно сложно. давайте разбиратьсяtrackChildRunДля чего это. Точно так же с помощью одного теста съедается один трюк по всему небу. Закомментируйте его и запустите одиночный тест:

✕ должен вызвать эффект (3 мс)

✕ должен работать в цепочке (2 мс)

✕ должен вызвать эффект при цепочке (1 мс)

✕ должен вызывать эффект при цепочке (смешанные вызовы) (1 мс)

✕ больше не должно обновляться при остановке (1 мс)

Потом найти соответствующий юнит-тест, будем разбираться в его полезности, то есть делать зависимостиcomputedиз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)
})

если мы неtrackChildRunлогика, когдаvalueПри изменении,cValueФункция расчета действительно выполнима. ноcValueЧитать и писать безtrackа такжеtriggerлогика, когдаcValueПри изменении, естественно, функция слушателя не может быть запущена. Для решения этой задачи у нас естьtrackChildRun.

Функция слушателя, то есть в одиночном тесте() => { dummy = cValue.value }, когда он выполняется в первый раз, так какcValue, вызывает функцию расчета, а затем переходит кtrackChildRun.

В это время функция слушателя() => { dummy = cValue.value }еще не закончил выполнение, так что все ещеeffectStackконец очереди. взять его с конца, это называетсяcomputedродительeffect.

И сама функция вычисления тожеeffect, как мы уже говорили, егоdepsсохранить все, что держит егоdep. и этоdepуказать сноваtargetMapсоответствующие данные в . Поскольку все они являются справочными данными, до тех пор, пока родительскийeffectдобавлен кcomputed.deps, что эквивалентно выполнению родительскогоeffectзависит отcomputedДанные ответа, на которые функция опирается внутри.

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

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