Объясните оптимизацию Vue.js 3.2 в адаптивной части.

внешний интерфейс оптимизация производительности Vue.js
Объясните оптимизацию Vue.js 3.2 в адаптивной части.

задний план

Прошел почти год с момента официального выпуска Vue 3. Я считаю, что многие мелкие партнеры уже использовали Vue 3 в производственной среде. Сегодня Vue.js 3.2 имеетофициальный релиз, и на этот разminorВерсия обновления в основном отражена в оптимизации уровня исходного кода, на уровне пользователя, по сути, не так много изменений. Один поймал мою точку зрения, чтобы повысить отзывчивую производительность:

  • More efficient ref implementation (~260% faster read / ~50% faster write)
  • ~40% faster dependency tracking
  • ~17% less memory usage

ПереведеноrefПовышение эффективности чтения API260%, эффективность записи повышается примерно50%, повышение эффективности сбора зависимостей составляет около40%, а также уменьшить примерно17%использование памяти.

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

И эта оптимизация осуществляется не официальным персоналом Vue, а большим парнем в сообществе.@basvanmeursпредложенный, актуальныйоптимизировать кодОн был представлен 9 октября 2020 года, но из-за серьезных изменений во внутренней реализации чиновник дождался выпуска Vue.js 3.2, прежде чем включать код.

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

Мы знаем, что по сравнению с Vue 2, Vue 3 сделал много оптимизаций, часть из которых заключается в том, что реализация реагирования на данные реализована с помощьюObject.definePropertyAPI измененProxyAPI.

Когда продвигали Vue 3, было официально объявлено, что он оптимизировал производительность адаптивной реализации.Так какие же аспекты отражены в оптимизации? Некоторые друзья думают, чтоProxyПроизводительность API лучше, чемObject.definePropertyДа не совсем, на самом делеProxyпо производительности, чемObject.definePropertyПлохо, пожалуйста, обратитесь к деталямThoughts on ES6 Proxies PerformanceЭта статья, и я также проверил ее, вывод такой же, как и выше, вы можете обратиться к этомуrepo.

теперь, когдаProxyМедленно, почему Vue 3 выбрал его для реализации реагирования на данные? потому чтоProxyПо сути, это захват объекта, чтобы он мог не только следить за изменением значения атрибута объекта, но и следить за добавлением и удалением атрибутов объекта;Object.definePropertyзаключается в добавлении соответствующего свойства к существующему свойству объектаgetterа такжеsetter, поэтому он может отслеживать только изменения значения этого свойства, но не добавление и удаление свойств объекта.

Оптимизация производительности отзывчивости на самом деле отражается в превращении глубоко вложенных объектов в адаптивные сценарии. В реализации Vue 2, когда данные становятся отзывчивыми на этапе инициализации компонента, они будут выполняться рекурсивно, когда подсвойство все еще является объектом.Object.definePropertyОпределите отзывчивость подобъектов; в реализации Vue 3 только при доступе к свойствам объекта будет оцениваться тип подсвойств, чтобы решить, выполнять ли рекурсивно или нетreactive, который на самом деле является адаптивной реализацией отложенного определения подобъектов, что в определенной степени улучшит производительность.

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

Принцип гибкой реализации

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

Vue 3 реагирует на достижения, в основном за счетProxyAPI захватывает чтение и запись объектов данных, и когда мы получаем доступ к данным, он запускаетgetterВыполнить сбор зависимостей; когда данные будут изменены, он будет запущенsetterУведомление об отправке.

Далее давайте кратко проанализируем реализацию сбора зависимостей и отправки уведомлений (до Vue.js 3.2).

Коллекция зависимостей

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

let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个 key 对应一个 dep 集合
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect)
   // 当前激活的 effect 收集 dep 集合作为依赖
    activeEffect.deps.push(dep)
  }
}

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

trackФункция имеет три параметра, из которыхtargetпредставляет необработанные данные;typeУказывает тип этой коллекции зависимостей;keyПредставляет атрибут для доступа.

trackсоздал глобал вне функцииtargetMapкак исходный объект данныхMap, его ключtarget, значениеdepsMap, как иждивенецMap;этоdepsMapКлючtargetизkey, значениеdepсобирать,depВ коллекциях хранятся зависимые функции побочных эффектов. Для простоты понимания взаимосвязь между ними можно представить следующей схемой:

track.png

Поэтому каждый раз, когда вы выполняетеtrackфункция, которая является функцией побочного эффекта текущей активацииactiveEffectКак зависимость, затем собранаtargetСвязанныйdepsMapвести перепискуkeyКоллекция зависимостей подdepсередина.

Распространить уведомление

Уведомление об отправке происходит на этапе обновления данных, ядро ​​должно срабатывать при изменении ответных данных.setterфункция для выполненияtriggerУведомление о функции распределения:

const targetMap = new WeakMap()
function trigger(target, type, key) {
  // 通过 targetMap 拿到 target 对应的依赖集合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 没有依赖,直接返回
    return
  }
  // 创建运行的 effects 集合
  const effects = new Set()
  // 添加 effects 的函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // SET | ADD | DELETE 操作之一,添加对应的 effects
  if (key !== void 0) {
    add(depsMap.get(key))
  }
  const run = (effect) => {
    // 调度执行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    else {
      // 直接运行
      effect()
    }
  }
  // 遍历执行 effects
  effects.forEach(run)
}

triggerФункция имеет три параметра, из которыхtargetПредставляет целевой примитивный объект;typeУказывает тип обновления;keyУказывает свойство для изменения.

triggerФункция в основном делает четыре вещи:

  1. отtargetMapЗалезайtargetСоответствующий набор зависимостейdepsMap;

  2. создать прогонeffectsсобирать;

  3. согласно сkeyотdepsMapнайти соответствующийeffectдобавить вeffectsсобирать;

  4. траверсeffectsВыполните связанную функцию побочного эффекта.

Поэтому каждый раз, когда вы выполняетеtriggerфункция, согласноtargetа такжеkey,отtargetMapНайдите все связанные функции побочных эффектов в обходе и выполните их снова.

В процессе описания, основанного на уведомлении о сборе и распространении, мы все упоминаем слово: функция побочного эффекта, в зависимости от процесса сбора, мы ставимactiveEffect(в настоящее время активирует функцию побочного эффекта) как набор зависимостей, что это такое? Далее, давайте взглянем на истинное лицо функций побочных эффектов.

функция побочных эффектов

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

import { reactive } from 'vue'
const counter = reactive({
  num: 0
})
function logCount() {
  console.log(counter.num)
}
function count() {
  counter.num++
}
logCount()
count()

Определяем реактивные объектыcounter, затем вlogCountЧтобы получить доступ кcounter.num, мы хотим выполнитьcountмодификация функцииcounter.numзначение, оно может быть выполнено автоматическиlogCountфункция.

Согласно нашему предыдущему анализу процесса сбора зависимостей, еслиlogCountдаactiveEffect, то требование может быть выполнено, но, очевидно, не может быть выполнено, так как код выполняется до тех пор, покаconsole.log(counter.num)Эта линия, когда она сама по себеlogCountОперация в функции игнорируется.

Что же тогда делать? На самом деле, пока мы бежимlogCountПеред функцией поставитьlogCountназначить наactiveEffectДостаточно:

activeEffect = logCount 
logCount()

Следуя этому направлению мысли, мы можем использовать идею функций высшего порядка дляlogCountСделайте один пакет:

function wrapper(fn) {
  const wrapped = function(...args) {
    activeEffect = fn
    fn(...args)
  }
  return wrapped
}
const wrappedLog = wrapper(logCount)
wrappedLog()

wrapperсама является функцией, она принимаетfnВ качестве аргумента возвращает новую функциюwrapped, а затем сохраните глобальную переменнуюactiveEffect,когдаwrappedПри выполнении поставитьactiveEffectУстановить какfn, затем выполнитеfnВот и все.

Итак, когда мы выполняемwrappedLogПосле этого перейдите к изменениюcounter.num, он будет автоматически выполнятьсяlogCountфункция.

Фактически, Vue 3 использует аналогичный подход и имеетeffectФункция побочных эффектов, давайте посмотрим на ее реализацию:

// 全局 effect 栈
const effectStack = []
// 当前激活的 effect
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // 如果 fn 已经是一个 effect 函数了,则指向原始函数
    fn = fn.raw
  }
  // 创建一个 wrapper,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
    effect()
  }
  return effect
}
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effect.active) {
      // 非激活状态,则判断如果非调度执行,则直接执行原始函数。
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      // 清空 effect 引用的依赖
      cleanup(effect)
      try {
        // 开启全局 shouldTrack,允许依赖收集
        enableTracking()
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        // 执行原始函数
        return fn()
      }
      finally {
        // 出栈
        effectStack.pop()
        // 恢复 shouldTrack 开启之前的状态
        resetTracking()
        // 指向栈最后一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid++
  // 标识是一个 effect 函数
  effect._isEffect = true
  // effect 自身的状态
  effect.active = true
  // 包装的原始函数
  effect.raw = fn
  // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
  effect.deps = []
  // effect 的相关配置
  effect.options = options
  return effect
}

В сочетании с приведенным выше кодомeffectвнутренне, выполнивcreateReactiveEffectфункция для создания новогоeffectфункция, для и внешняяeffectфункциональное различие, мы называем этоreactiveEffectфункцию, а также добавить к ней некоторые дополнительные свойства (я отметил их в комментариях). Кроме того,effectФункция также поддерживает передачу параметра конфигурации для поддержки большего количестваfeature, здесь не будет распространяться.

reactiveEffectФункция — это реактивная функция с побочным эффектом, которая при выполненииtriggerКогда процесс отправляет уведомление, выполнениеeffectЭто оно.

Согласно нашему предыдущему анализу,reactiveEffectФункции должны делать только две вещи: сделать глобальнуюactiveEffectУкажите на него, а затем выполните обернутые примитивыfn.

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

Тогда судитеeffectStackсодержит ли онeffect, если не поставитьeffectвставить в стек. Как мы упоминали ранее, просто установитеactiveEffect = effectВот и все, так зачем проектировать структуру стека здесь?

Фактически, учитывая следующие вложенныеeffectСценарий:

import { reactive} from 'vue' 
import { effect } from '@vue/reactivity' 
const counter = reactive({ 
  num: 0, 
  num2: 0 
}) 
function logCount() { 
  effect(logCount2) 
  console.log('num:', counter.num) 
} 
function count() { 
  counter.num++ 
} 
function logCount2() { 
  console.log('num2:', counter.num2) 
} 
effect(logCount) 
count()

Каждый раз, когда мы выполняемeffectфункционировать, если толькоreactiveEffectфункция, возложенная наactiveEffect, то для этого вложенного сценария выполнитеeffect(logCount2)назад,activeEffectещеeffect(logCount2)вернутьreactiveEffectфункцию, чтобы последующий доступcounter.numКогда зависимость собирает соответствующийactiveEffectЭто неправильно, в это время мы выполняем внешнеcountмодификация функцииcounter.numПосле казни нетlogCountфункция, ноlogCount2функции, окончательный вывод выглядит следующим образом:

num2: 0 
num: 0 
num2: 0

И наш ожидаемый результат должен быть следующим:

num2: 0 
num: 0 
num2: 0 
num: 1

Поэтому для вложенныхeffectсценарий, мы не можем просто назначитьactiveEffect, следует учитывать, что выполнение самой функции представляет собой операцию push-and-pop, поэтому мы также можем спроектироватьeffectStack, так что каждый раз, когда вы вводитеreactiveEffectФункция сначала помещает его в стек, а затемactiveEffectуказать на этоreactiveEffectфункцию, то вfnПосле завершения выполнения извлеките стек, а затем поместитеactiveEffectнаправлениеeffectStackПоследний элемент, внешний слойeffectсоответствующий функцииreactiveEffect.

Здесь мы также замечаем деталь, которая будет выполняться до того, как будет помещена в стек.cleanupфункция пустаяreactiveEffectЗависимости, соответствующие функциям. в исполненииtrackфункция, в дополнение к сбору текущих активированныхeffectкак зависимость, также черезactiveEffect.deps.push(dep)Пучокdepтак какactiveEffectзависимости, так что вcleanupкогда мы сможем найтиeffectсоответствующийdep, затем поставьтеeffectот нихdepудалено в.cleanupКод функции выглядит так:

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

зачем нужноcleanupШерстяная ткань? Если вы столкнулись с этим сценарием:

<template>
  <div v-if="state.showMsg">
    {{ state.msg }}
  </div>
  <div v-else>
    {{ Math.random()}}
  </div>
  <button @click="toggle">Toggle Msg</button>
  <button @click="switchView">Switch View</button>
</template>
<script>
  import { reactive } from 'vue'

  export default {
    setup() {
      const state = reactive({
        msg: 'Hello World',
        showMsg: true
      })

      function toggle() {
        state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'
      }

      function switchView() {
        state.showMsg = !state.showMsg
      }

      return {
        toggle,
        switchView,
        state
      }
    }
  }
</script>

В сочетании с кодом вы можете знать, что представление этого компонента будет основано наshowMsgУправление отображением переменныхmsgили случайное число, когда мы нажимаемSwitch Viewкнопку, значение переменной будет изменено.

если нетcleanup, когда шаблон отображается впервые,activeEffectявляется функцией рендеринга побочного эффекта компонента, потому что шаблонrenderпосетил, когдаstate.msg, поэтому выполняется сбор зависимостей с функцией рендеринга побочного эффекта какstate.msgзависимость, мы называем этоrender effect. Затем мы нажимаемSwitch Viewкнопка, вид переключается на отображение случайных чисел, затем снова нажимаемToggle Msgкнопка, так как измененоstate.msgУведомление будет отправлено, найденоrender effectИ выполнить, он запускает повторный рендеринг компонента.

Но это поведение на самом деле не так, как ожидалось, потому что, когда мы нажимаемSwitch ViewКнопка, когда представление переключается на отображение случайных чисел, это также вызывает повторную визуализацию компонента, но представление в это время не визуализируется.state.msg, поэтому изменения в нем не должны влиять на повторный рендеринг компонента.

Итак, в компонентеrender effectПеред выполнением, если прошлоcleanupОчистите зависимости, прежде чем мы сможем удалитьstate.msgсобралrender effectполагаться. На этот раз, когда мы модифицируемstate.msgКогда зависимости нет, повторный рендеринг компонента не будет запущен, как и ожидалось.

Оптимизация для гибкой реализации

Принцип адаптивной реализации был проанализирован ранее, и вроде бы все в порядке, так что тут еще можно оптимизировать?

Оптимизация сбора зависимостей

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

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

Так что здесь нам нужно дать коллекциюdepДобавьте два свойства:

export const createDep = (effects) => {
  const dep = new Set(effects)
  dep.w = 0
  dep.n = 0
  return dep
}

вwУказывает, был ли он собран,nУказывает, является ли он новым.

Затем создайте несколько глобальных переменных,effectTrackDepth,trackOpBit,maxMarkerBits.

вeffectTrackDepthУказывает рекурсивное вложенное выполнениеeffectглубина функции;trackOpBitИспользуется для определения состояния коллекции зависимостей;maxMarkerBitsПредставляет максимальное количество битов в маркере.

Далее посмотрите на их приложения:

function effect(fn, options) {
  if (fn.effect) {
    fn = fn.effect.fn
  }
  // 创建 _effect 实例 
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 拷贝 options 中的属性到 _effect 中
    extend(_effect, options)
    if (options.scope)
      // effectScope 相关处理逻辑
      recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 立即执行
    _effect.run()
  }
  // 绑定 run 函数,作为 effect runner
  const runner = _effect.run.bind(_effect)
  // runner 中保留对 _effect 的引用
  runner.effect = _effect
  return runner
}

class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    // effect 存储相关的 deps 依赖
    this.deps = []
    // effectScope 相关处理逻辑
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        // 压栈
        effectStack.push((activeEffect = this))
        enableTracking()
        // 根据递归的深度记录位数
        trackOpBit = 1 << ++effectTrackDepth
        // 超过 maxMarkerBits 则 trackOpBit 的计算会超过最大整形的位数,降级为 cleanupEffect
        if (effectTrackDepth <= maxMarkerBits) {
          // 给依赖打标记
          initDepMarkers(this)
        }
        else {
          cleanupEffect(this)
        }
        return this.fn()
      }
      finally {
        if (effectTrackDepth <= maxMarkerBits) {
          // 完成依赖标记
          finalizeDepMarkers(this)
        }
        // 恢复到上一级
        trackOpBit = 1 << --effectTrackDepth
        resetTracking()
        // 出栈
        effectStack.pop()
        const n = effectStack.length
        // 指向栈最后一个 effect
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

можно увидеть,effectРеализация функции была доработана и скорректирована в определенной степени, и она используется внутри.ReactiveEffectкласс создает_effectэкземпляр, и функция возвращаетrunnerуказывает наReactiveEffectКатегорияrunметод.

То есть выполнение функций побочных эффектовeffectфункция, которая на самом деле выполняет этоrunфункция.

когдаrunКогда функция выполняется, мы замечаемcleanupФункция больше не выполняется по умолчанию, в инкапсулированной функцииfnПеред выполнением сначала выполнитеtrackOpBit = 1 << ++effectTrackDepthЗаписыватьtrackOpBit, а затем сравните, превышает ли глубина рекурсииmaxMarkerBits, если он превышает (обычно нет) старый все равно выполняетсяcleanupлогика, выполнить, если не превышеноinitDepMarkersОтметьте зависимость, чтобы увидеть ее реализацию:

const initDepMarkers = ({ deps }) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // 标记依赖已经被收集
    }
  }
}

initDepMarkersРеализация функции очень проста, пересекающаяся_effectПримерdepsатрибут, для каждогоdepизwатрибут помечен какtrackOpBitценность .

будет выполняться следующимfnФункция — это функция, инкапсулированная функцией побочного эффекта, такой как рендеринг для компонента,fnЭто функция рендеринга компонента.

когдаfnКогда функция выполняется, она получает доступ к ответным данным и запускает ихgetter, а затем выполнитьtrackФункция выполняет сбор зависимостей. Соответственно, были внесены некоторые коррективы в процесс сбора зависимостей:

function track(target, type, key) {
  if (!isTracking()) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个 key 对应一个 dep 集合
    depsMap.set(key, (dep = createDep()))
  }
  const eventInfo = (process.env.NODE_ENV !== 'production')
    ? { effect: activeEffect, target, type, key }
    : undefined
  trackEffects(dep, eventInfo)
}

function trackEffects(dep, debuggerEventExtraInfo) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // 标记为新依赖
      dep.n |= trackOpBit 
      // 如果依赖已经被收集,则不需要再次收集
      shouldTrack = !wasTracked(dep)
    }
  }
  else {
    // cleanup 模式
    shouldTrack = !dep.has(activeEffect)
  }
  if (shouldTrack) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect)
    // 当前激活的 effect 收集 dep 集合作为依赖
    activeEffect.deps.push(dep)
    if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
      activeEffect.onTrack(Object.assign({
        effect: activeEffect
      }, debuggerEventExtraInfo))
    }
  }
}

Мы обнаружили, что при созданииdep, выполнивcreateDepметод выполняется, кроме того, вdepактивирован доeffectПрежде чем собирать как зависимость, он будет судить об этомdepБыло ли оно собрано, если оно было собрано, его не нужно собирать снова. Кроме того, здесь будет решено, чтоdepЯвляется ли это новой зависимостью, если нет, отметьте ее как новую.

Далее давайте посмотрим наfnЛогика после выполнения:

finally {
  if (effectTrackDepth <= maxMarkerBits) {
    // 完成依赖标记
    finalizeDepMarkers(this)
  }
  // 恢复到上一级
  trackOpBit = 1 << --effectTrackDepth
  resetTracking()
  // 出栈
  effectStack.pop()
  const n = effectStack.length
  // 指向栈最后一个 effect
  activeEffect = n > 0 ? effectStack[n - 1] : undefined
}

При условии выполнения флага зависимости необходимо выполнитьfinalizeDepMarkersЗаполните разметку зависимостей, чтобы увидеть его реализацию:

const finalizeDepMarkers = (effect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      // 曾经被收集过但不是新的依赖,需要删除
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      }
      else {
        deps[ptr++] = dep
      }
      // 清空状态
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

finalizeDepMarkersГлавное — найти те зависимости, которые были собраны, но не собраны в новом раунде сбора зависимостей.depsудалено в. По сути, это для решения упомянутых выше потребностей.cleanupСценарий: к реактивному объекту нет доступа во время рендеринга нового компонента, тогда его изменения не должны вызывать повторный рендеринг компонента.

Вышеприведенное реализует оптимизацию части сбора зависимостей, вы можете видеть, что по сравнению с предыдущим выполнением каждый разeffectФункции должны сначала очистить зависимости, а затем добавить зависимости, Текущая реализация будет выполняться каждый разeffectСостояние зависимостей помечается перед обернутой функцией, и собранные зависимости не будут повторно собираться в процессе.effectФункция также удаляет зависимости, которые были собраны, но не собраны в новом раунде сбора зависимостей.

После оптимизации дляdepКоличество операций, зависящих от коллекций, сокращается, что естественным образом оптимизирует производительность.

Реактивная оптимизация API

Оптимизация отзывчивого API в основном отражена вref,computedи другие оптимизации API.

кrefВозьмем в качестве примера API, давайте посмотрим на его реализацию перед оптимизацией:

function ref(value) {
  return createRef(value)
}

const convert = (val) => isObject(val) ? reactive(val) : val

function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    // 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue
    this._shallow = _shallow
    this.__v_isRef = true
    // 非 shallow 的情况,如果它的值是对象或者数组,则递归响应式
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    // 给 value 属性添加 getter,并做依赖收集
    track(toRaw(this), 'get' /* GET */, 'value')
    return this._value
  }
  set value(newVal) {
    // 给 value 属性添加 setter
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // 派发通知
      trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
    }
  }
}

refфункция возвращенаcreateRefвозвращаемое значение выполнения функции, аcreateRefВнутри вложение обрабатывается в первую очередьrefслучае, если входящийrawValueТакжеref, затем вернитесь напрямуюrawValue; затем вернутьсяRefImplэкземпляр объекта.

а такжеRefImplВнутренняя реализация, в основном захват ее экземпляраvalueатрибутgetterа такжеsetter.

при посещенииrefобъектvalueсвойство, вызоветgetterвоплощать в жизньtrackФункция выполняет сбор зависимостей и возвращает ее значение; при измененииrefобъектvalueзначение, оно сработаетsetterустановить новое значение и выполнитьtriggerуведомление об отправке функции, если новое значениеnewValявляется типом объекта или массива, а затем преобразует его вreactiveобъект.

Далее рассмотрим изменения, связанные с реализацией этой части в Vue.js 3.2:

class RefImpl {
  constructor(value, _shallow = false) {
    this._shallow = _shallow
    this.dep = undefined
    this.__v_isRef = true
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

Основное изменение заключается в том, чтоrefобъектvalueСвойства выполняют логику, основанную на сборе и отправке уведомлений.

В Vue.js версии 3.2refВ реализации о части коллекции зависимостей от оригиналаtrackфункция изменена наtrackRefValue, чтобы увидеть его реализацию:

function trackRefValue(ref) {
  if (isTracking()) {
    ref = toRaw(ref)
    if (!ref.dep) {
      ref.dep = createDep()
    }
    if ((process.env.NODE_ENV !== 'production')) {
      trackEffects(ref.dep, {
        target: ref,
        type: "get" /* GET */,
        key: 'value'
      })
    }
    else {
      trackEffects(ref.dep)
    }
  }
}

Вы можете увидеть здесь непосредственноrefСоответствующие зависимости сохраняются вdepсвойства, находясь вtrackПри реализации функции будет сохранена зависимость от глобальногоtargetMapсередина:

let depsMap = targetMap.get(target)
if (!depsMap) {
  // 每个 target 对应一个 depsMap
  targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
  // 每个 key 对应一个 dep 集合
  depsMap.set(key, (dep = createDep()))
}

Очевидно,trackМожет потребоваться сделать несколько оценок и установить логику внутри функции, а также сохранить зависимости вrefобъектdepЭта серия суждений и настроек исключена из свойств для оптимизации производительности.

соответствующий,refРеализация части про отправку уведомлений, оригиналомtriggerфункция изменена наtriggerRefValue, чтобы увидеть его реализацию:

function triggerRefValue(ref, newVal) {
  ref = toRaw(ref)
  if (ref.dep) {
    if ((process.env.NODE_ENV !== 'production')) {
      triggerEffects(ref.dep, {
        target: ref,
        type: "set" /* SET */,
        key: 'value',
        newValue: newVal
      })
    }
    else {
      triggerEffects(ref.dep)
    }
  }
}

function triggerEffects(dep, debuggerEventExtraInfo) {
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      }
      else {
        effect.run()
      }
    }
  }
}

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

Дизайн trackOpBit

Осторожно, вы можете обнаружить, что эта отметка зависитtrackOpBit, используя оператор сдвига влево в каждом вычисленииtrackOpBit = 1 << ++effectTrackDepth; а при присваивании используется операция ИЛИ:

deps[i].w |= trackOpBit
dep.n |= trackOpBit

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

судитьdepБыл ли он использован при сборе зависимостейwasTrackedфункция:

const wasTracked = (dep) => (dep.w & trackOpBit) > 0

Результат операции И больше, чем0Судя по всему, для этого требуется, чтобы уровень вложенности зависимостей совпадал при их сборе. Например, предположим, чтоdep.wЗначение2, что указывает на то, что он выполняется в первом слоеeffectФункция создана, но в это время выполнен вложенный второй уровеньeffectфункция,trackOpBitСдвиг влево на две позиции становится4,2 & 4Значение0,ТакwasTrackedВозвращаемое значение функции равноfalse, указывая на то, что эту зависимость необходимо собрать. Очевидно, что это требование разумно.

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

Суммировать

Как правило, в приложении Vue.js доступ к адаптивным данным и их изменение являются очень частыми операциями, поэтому оптимизация производительности этого процесса значительно повысит производительность всего приложения.

Большинство людей смотрят на адаптивную реализацию Vue.js, и наиболее вероятной целью является понимание принципа реализации, и редко обращают внимание на то, оптимальна ли реализация. А может начальник basvanmeurs предложить реализацию этой серии оптимизаций, и написать рукописнуюэталонный инструментЧтобы проверить нашу собственную оптимизацию, нам очень полезно учиться.

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

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