Самый подробный анализ принципа ответа на данные о реактивности Vue3

JavaScript Vue.js
Самый подробный анализ принципа ответа на данные о реактивности Vue3

image.png

Это почти то же самое, картинка немного клоунская, а также можно посмотреть чужие более полные картинки.@mxin

image.png

предисловие

давно нет связиVueТеперь, когда я смотрел прямую трансляцию Йоды несколько дней назад, я говорил о看源码Некоторые из представлений предназначены для лучшего начала работы.vueИли хотите изучить внутреннюю структуру мышления?

image.png

Внутренний интерфейс: интервью, интервью спросят.

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

Если вы застопоритесь и не будете следовать броску, внезапное давление сокрушит вас, и вам может быть трудно выжить в броске, даже если вы правы.

Если вам интересно, вы можете прочитатьоползни @Nuggetsнаписал боссШкала самооценки тревоги программиста.

Кажется, я слишком много наговорил не по теме, вместо того, чтобы жаловаться, лучше успокоиться и учиться вместе.ReactivityНекоторые основные принципы этого, я полагаю, что после прочтения статьи выvue 3Реагирование на данные имеет более глубокое понимание.

И почему выбираютReactivityДля модуля это связано с тем, что его степень связи низкая, и онvue3.0Один из основных модулей, стоимость исполнения очень высока.

Основы

Прежде чем начать, если вы не понимаетеES6некоторые, которые появляются高阶api,Такие как,Proxy, Reflect, WeakMap, WeakSet,Map, SetИ так далее, вы можете прочитать главу ресурса самостоятельно, и лучше всего сначала понять необходимые очки знаний и просмотреть ее снова.

Proxy

существует@vue/reactivityсередина,ProxyЭто краеугольный камень всего планирования.

пройти черезProxyпрокси-объект, который можно использовать вget, setВ методе выполняются следующие действия, например依赖收集,effect,track, triggerОперации и т. д. здесь подробно описываться не будут и будут подробно описаны позже.

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

Давайте напишем простойProxy. в этомhandleCallbackнаписано наset, getДля перехвата данных мониторинга текущего изменения значения атрибута используются два метода. Сначала код:

const user = {
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微乎其微的前端小哥。'
}

const userProxy = new Proxy(user, {
  get(target, key) {
    console.log(`userProxy: 当前获取key为${key}`)
    if (target.hasOwnProperty(key)) return target[key]
    return {
    }
  },
  set(target, key, value) {
    console.log(`userProxy: 当前设置值key为${key}, value为${value}`)
    let isWriteSuccess = false
    if (target.hasOwnProperty(key)) {
      target[key] = value
      isWriteSuccess = true
    }
    return isWriteSuccess
  }
})

console.log('myNaame', userProxy.name)

userProxy.age = 23

Когда мы назначаем, изменяем и печатаем значение, мы запускаем текущийsetа такжеgetметод.

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

image.png

Reflect

ReflectНе класс, а встроенный объект. Об этом должен знать каждый, а не прямо实例化(new)используя, его функция сравнивает иProxyизhandlesЭто немного похоже, но к этому добавлено гораздо большеObjectМетоды.

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

image.png

Следующее правильноuserнекоторые из объектов修改За примером операции вы можете обратиться к нему, который может быть использован в будущем.

const user = {
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微乎其微的前端小哥。'
}

console.log('change age before' , Reflect.get(user, 'age'))

const hasChange = Reflect.set(user, 'age', 23)
console.log('set user age is done? ', hasChange ? 'yes' : 'no')

console.log('change age after' , Reflect.get(user, 'age'))

const hasDelete = Reflect.deleteProperty(user, 'age')

console.log('delete user age is done?', hasDelete ? 'yes' : 'none')

console.log('delete age after' , Reflect.get(user, 'age'))

image.png

Принципы

Когда вы усвоите некоторые из предыдущих знаний, пора начинать@vue/reactivityГлава анализа исходного кода завершена. Далее начнем с простой идеи по реализации базовогоreactivity, когда вы поймете его основные принципы, вы@vue/reactivityиз依赖收集(track)а также触发更新(trigger),так же как副作用(effect)Какая именно работа.

reactive

reactiveдаvue3используется для создания引用类型изapi.

const user = reactive({
  name: 'wangly19',
  age: 22,
  description: '一名掉头发微乎其微的前端小哥。'
})

Затем загляните внутрь функции,reactiveЧто именно делает метод?

Внутренне выполняетtargetРешение только для чтения, если вы пройдетеtargetЕсли это прокси только для чтения, он вернется напрямую. для нормальногоreactiveзатем он возвращаетсяcreateReactiveObjectзначение метода.

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

createReactiveObject

существуетcreateReactiveObject, что делается дляtargetдобавить одинproxyиграет роль. Это его ядро,reactiveНаконец-то получилproxyпрокси, ссылкаProxyПростой пример главы может быть известенreactiveКак это работает, так что давайте посмотримcreateReactiveObjectсделал что-то.

Сначала определите токtargetТип , если он не соответствует требованиям, сразу выдается предупреждение и возвращается исходное значение.

if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

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

if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

Для этих кодов суждений их не очень сложно прочитать, обратите внимание наif ()В состоянии суда посмотрите, какое действие оно совершает. а такжеcreateReactiveObjectСамое главное, что нужно сделать, это создатьtargetизproxy, и поместите его вMapзаписано в.

И интереснее, где входящийtargetназывается разнымproxy handle. Тогда давайте посмотримhandlesЧто вы делали в .

const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy

тип ручек

В типе объекта укажитеObjectа такжеArrayа такжеMap,Set, WeakMap,WeakSetдифференцированный. они называют разныеProxy Handle.

  • baseHandlers.ts:Object & Arrayбудет вызывать под этим файломmutableHandlersобъект какProxy Handle.
  • collectionHandlers.ts:Map,Set, WeakMap,WeakSetбудет вызывать под этим файломmutableCollectionHandlersобъект какProxy Handle.
/**
 * 对象类型判断
 * @lineNumber 41
 */
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

Будет вnew Proxyвернулся в соответствии сtargetTypeсудить.

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

Из-за ограниченного места ниже приведены только примеры.mutableHandlersв качестве ориентира для анализа. когда понялmutableHandlersпосле дляcollectionHandlersПросто вопрос времени.

Proxy Handle

Как было сказано выше, по разнымTypeназывай разныеhandle, тогда давайте посмотримmutableHandlersЧто именно сделал.

В основе все мы знаемProxyможет получить объект конфигурации, который мы демонстрируемgetа такжеsetметод собственности. а такжеmutableHandlersвещи одного и того же значения, отдельно определенные внутреннеget, set, deleteProperty, has, oneKeysДождитесь нескольких параметров атрибута, если вы не знаете, что это значит, вы можете посмотреть на этоProxy Mdn. Здесь нужно понять被监听的数据пока это происходит增查删改После этого большая часть из них попадет в соответствующий канал поступления.

image.png

Здесь мы используем простойget, setдля простого примера моделирования.

function createGetter () {
    return (target, key, receiver) => {
      const result = Reflect.get(target, key, receiver)
      track(target, key)
      return result
    }
}

const get = /*#__PURE__*/ createGetter()

function createSetter () {
  
  return (target, key, value, receiver) => {
    const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)
  if (result && oldValue != value) {
    trigger(target, key)
  }
  return result
  }
}

существуетgetвыполнитtrackсбор зависимостей, в то время какsetсрабатывает, когдаtriggerспусковой механизм. существуетvue3,а такжеtriggerа такжеtrackслова все в насeffect.tsуказано в нем, то взгляните на следующий依赖收集а также响应触发Что-то сделал.

Effect

Весь модуль эффектов разделен на три части:

  • effect: Функция побочного эффекта
  • teack: коллекция зависимостей, вproxyданные проксиgetкогда звонят
  • trigger: реакция триггера, вproxyВызывается при изменении данных прокси.

effect

Давайте посмотрим на примерeffectиспользовать и понимать, что его главный аргумент — это функция. Внутри функция поможет вам выполнить регистрацию некоторых побочных эффектов и оценку функций.

effect(() => {
    proxy.user = 1
})

приди и посмотриvueизeffectЧто ты сделал?

Здесь сначала определите текущие параметрыfnэтоeffect, если да, тоrawХранится вfnсделать замену. затем сделай это сноваcreateReactiveEffectгенерировать.

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

существуетcreateReactiveEffectбудем ли мыeffectзатолкнуть вeffectStackПоместите операцию стека в , а затем используйтеactiveEffectполучить доступ к выполняемому в данный моментeffect, который будет выполняться после выполнения出栈. заменить заодноactiveEffectдля новой вершины стека.

пока вeffectбудет срабатывать во время выполненияproxy handleпотомtrackа такжеtriggerДве ключевые функции.

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

посмотреть короткую версиюeffect, помимо большей части кодового багажа, следующий код не намного понятнее.

function effect(eff) {
  try {
    effectStack.push(eff)
    activeEffect = eff
    return eff(...argsument)
    
  } finally {
    effectStack.pop()
    activeEffect = effectStack[effectStack.length  - 1]
  }
}

трек (коллекция зависимостей)

существуетtrack, будет выполнен известный сбор зависимостей, а текущийactiveEffectдобавить вdepВнутри, и говоря об этом типе отношений. Это будет отношение один-ко-многим-ко-многим.

image.png

Это также очень ясно из кода.Прежде всего, у нас будет общийtargetMapэтоWeakMap,keyдаtarget(代理的对象), valueЯвляетсяMap, назови этоdepsMap, который используется для управления текущимtargetкаждый изkeyизdepsТакже известна как зависимость от побочных эффектов, также известная какdepend. существуетvue3черезSetприходи и уходи.

Первый шаг — опираться на текущийtargetПолучатьtargetMapсерединаdepsMap, если он не существует, продолжайтеtargetMap.set(target, (depsMap = new Map()))объявление инициализации, за которым следуетdepsMapВозьмите текущийkeyизdeps, если не найдено, также используйтеdepsMap.set(key, (dep = new Set()))Сделайте объявление инициализации и, наконец, установите текущийactiveEffectзатолкнуть вdeps, для сбора зависимостей.

    1. существуетtargetMapнайти вtarget
    1. существуетdepsMapнайти вkey
    1. БудуactiveEffectСохранитьdepв.

В этом случае будет сформирован шаблон структуры «один-ко-многим-ко-многим», в котором хранятся всеproxyЗахваченные зависимости.

function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

триггер (ответ на триггер)

существуетtriggerКогда это происходит, то фактически запускает выполнение текущей зависимости ответа.

Во-первых, вам нужно получить текущийkeyпо всем каналамdeps, так что вы увидите, что естьeffectsа такжеaddФункция, которую он делает, очень проста, заключается в том, чтобы судить о текущем входящемdepsMapНужно ли добавлять недвижимостьeffectsВнутри вот такое состояниеeffectне может быть текущимactiveEffectа такжеeffect.allowRecurse, чтобы гарантировать, что текущийset keyЗависимости выполняются.

const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

Следующие хорошо известные сценарии позволяют судить о некоторых текущих входящих изменениях.triggerбудет доставлен вTriggerOpTypesповедение, а затем выполнитьaddметод, который позволит квалифицироватьeffectдобавить вeffectsиди среди, здесь@vue/reactivityСделайте много данных, чтобы изменить поведение, напримерlengthРазнообразие.

Затем по разнымTriggerOpTypesпровестиdepsMapДанные вынимаются и, наконец, вводятсяeffects. затем черезrunметод преобразует текущийeffectвыполнить, черезeffects.forEach(run)выполнить.

if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

а такжеrunЧто ты опять сделал?

Во-первых, определить текущийeffectсерединаoptionsЕсть лиscheduler, если есть, используйтеscheduleЧтобы обработать выполнение, в противном случае напрямую выполнить его напрямуюeffect().

if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }

Чтобы немного сократить, чтобы посмотреть на логику обработки, она на самом деле изtargetMapПереписка в Китаеkeyзависимость.

const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach((effect) => {
      effect()
    })
  }

Ref

Как мы все знаем,refдаvue3Объявление реактивных данных для общих типов. и получитьrefнужно передать значениеref.valueспособ получения, многие думают, чтоrefпростойreactiveНо это не так.

В исходном кодеrefв конце концов позвонитеcreateRefметод, который внутренне возвращаетRefImplпример. это сProxyразница в том,refСбор зависимостей и триггеры ответа находятся вgetter/setterСреди них это может относиться к фигуреdemoформа, адрес ссылкиgettter/setter.

export function ref<T extends object>(value: T): ToRef<T>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

image.png

как показано на рисунке,vueсуществуетgetterнейтральныйproxy中的getтот же звонокtrackСоберите зависимости, вsetterв ходе выполнения_valueВызывается после изменения значенияtriggerвызывать.

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, public readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

Тогда вы должны знать:

  • proxy handleдаreactiveпринцип, иrefПринципgetter/setter.
  • существуетgetназывается, когдаtrack,setназывается, когдаtrigger
  • effectЭто ядро ​​ответа данных.

Computed

computedКак правило, есть два распространенных способа использования: один — это передача объекта с внутреннимsetа такжеgetметод, который принадлежитComputedOptionsформа.

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

А внутри будетgetter / setterдве переменные для сохранения.

когдаgetterOrOptionsКогда это функция, она будет назначена иgetter.

когдаgetterOrOptionsКогда это объект, он будетsetа такжеgetназначить наsetter,getter.

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

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
  }
  
  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any

ТакComputedRefImplчто ты сделал?

Исходный код вычисляемого свойства, на самом деле большая его часть зависит от лицевой стороныeffectнекоторое понимание.

Во-первых, мы все знаем, чтоeffectможет пройти函数с одним对象options.

здесь будетgetterпередается как параметр функции, т.е.副作用, пока вoptionsнастроен вlazyа такжеscheduler.

lazyвыражатьeffectвыполняется не сразу иschedulerвtriggerопределит, прошли ли выscheduler, который выполняется после передачиschedulerметод.

пока вcomputed schedulerСреди них нынешний_dirtyЭтоfalse, если да, то будет_dirtyУстановить какtrueи выполнитьtriggerтриггерный ответ.

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

пока вgetter/setterЦентральная встреча_valueвыполнять разные операции.

Первый вget value, судить о текущем._dirtyЭтоtrue, если это так, выполните кешированныйeffectи сохранить возвращенный результат в_valueи выполнитьtrackВыполните сбор зависимостей.

Во-вторых, вset value, затем позвоните_setterспособ сброса значения.

get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }

ссылка на ресурс

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

Суммировать

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

@vue/reactivityСложность чтения невелика, есть много качественных туториалов, если есть определенная рабочая база и знание кода, можно шаг за шагом разобраться. Лично мне не нужно разбираться в нем досконально и понимать значение каждой строки кода, а нужно понимать его основные идеи, изучать концепции фреймворка и некоторые идеи разработчиков фреймворка по написанию кода. Это все знания, которые можно позаимствовать и усвоить как свои собственные.

для того, кто пошел вReactДля фронтенда под экосистему читайтеVueИсходный код на самом деле больше для обогащения ваших знаний в размышлениях, а не для чтения его для интервью. Так же, как ваше одобрение не для экзаменов, а для получения знаний. В нынешних условиях это делать сложно, лучше успокоиться и сконцентрироваться на понимании какого-то знания, чем заучивать несколько сутр лицом к лицу.