Почему адаптивные обновления Vue точны на уровне компонентов? (принцип углубленный анализ)

Vue.js опрос

предисловие

Все мы знаем, что Vue будет точно обновлять только зависимости, собранные для обновления адаптивных свойств.当前组件, вместо рекурсивного обновления подкомпонентов, что является одной из причин его высокой производительности.

пример

Например, такой компонент:

<template>
   <div>
      {{ msg }}
      <ChildComponent />
   </div>
</template>

мы запускаемthis.msg = 'Hello, Changed~'Когда компонент обновляется, представление повторно визуализируется.

но<ChildComponent />Этот компонент на самом деле не будет перерисовываться, что намеренно делает Vue.

Некоторое время назад я думал, что, поскольку компонент представляет собой дерево, его обновление, разумеется, представляет собой глубокий обход дерева, рекурсивное обновление. В этой статье вы проанализируете с точки зрения исходного кода, как это делает Vue.精确更新из.

Реагистрирование обновления гранулярности

И React в аналогичном сценарии自顶向下的进行递归更新的, то есть если в ReactChildComponentСуществует десять уровней вложенных подэлементов, тогда все уровни будут рекурсивно перерисовываться (без ручной оптимизации), что является катастрофой для производительности. (Таким образом, React создалFiber,созданный异步渲染, фактически компенсируя производительность, которая была испорчена сама собой).

Могут ли они использовать эту систему сбора зависимостей? нет, потому что они следуютImmutableКонструкторские идеи, не всегда изменяющие свойства исходного объекта, основаны наObject.definePropertyилиProxyмеханизм реактивного сбора зависимостей бесполезен (вы всегда возвращаете новый объект, как мне узнать, какую часть старого объекта вы изменили?)

В то же время, поскольку адаптивного набора зависимостей нет, React может только рекурсивно сбрасывать все подкомпоненты.renderодин раз (за исключением таких оптимизаций, как memo и shouldComponentUpdate), а затем передатьdiff算法Решение о том, какую часть представления следует обновить, представляет собой рекурсивный процесс, называемыйreconciler, звучит круто, но производительность катастрофа.

Детализация обновления Vue

Итак, как же Vue выполняет это точное обновление? На самом деле, каждый компонент имеет свой собственный渲染 watcher, который управляет обновлениями представления для текущего компонента, но не управляетChildComponentобновление.

Конкретно для исходного кода, как это реализовано?

существуетpatchпроцесс, когда компонент обновляется доChildComponent, пойду кpatchVnode, то что примерно делает этот метод?

patchVnode

воплощать в жизньvnodeизprepatchкрюк.

Обратите внимание, что только组件vnodeбуду иметьprepatchэтот жизненный цикл,

пойдет сюдаupdateChildComponentметод, этоchildЧто именно ты имеешь ввиду?

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    // 注意 这个child就是ChildComponent组件的 vm 实例,也就是咱们平常用的 this
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

На самом деле об этом можно догадаться, посмотрев на переданные параметры, то есть:

  1. Обновление реквизита (подробности позже)
  2. обновить событие привязки
  3. Внесите некоторые обновления в слот (подробнее позже)

Если есть дочерние узлы, сравните дочерние узлы.

Например, эта сцена:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
<ul>

дляulтри изliдочерний узелvnodeиспользоватьdiffАлгоритм обновлен, что пропущено в этой статье.

И до сих пор,patchVnodeЭто закончено, и он не рекурсивно обновляет дерево подкомпонентов, как в обычном мышлении.

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

А подкомпоненты?

Предположим, список выглядит так:

<ul>
  <component>1</component>
  <component>2</component>
  <component>3</component>
<ul>

Затем в процессе сравнения толькоcomponentЗаявлениеprops,listenersи другие свойства для обновления,не углубляясь внутрь компонента для обновления.

ПРИМЕЧАНИЕ. Не углубляйтесь в компонент для обновления! (фокус, это также ключ к гранулярности обновлений, упомянутой в этой статье)

Как обновление реквизита вызывает повторный рендеринг?

Так что некоторые студенты могут спросить, а не рекурсивные подсборки обновлять, если мыmsgЭтот отзывчивый элемент передается через свойства вChildComponent, как он обновляется в это время?

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

const props = vm._props = {}

 for (const key in propsOptions) {
    // 经过一系列验证props合法性的流程后
    const value = validateProp(key, propsOptions, propsData, vm)
    // props中的字段也被定义成响应式了
    defineReactive(props, key, value)
}

До сих пор было достигнуто_propsПерехват изменений поля. То есть это становится отзывчивыми данными, а позже мы делаем что-то вроде_props.msg = 'Changed'Во время работы (конечно, мы этого делать не будем, сделает Vue) инициировать обновление представления.

фактически,msgПри передаче дочернему компоненту он будет сохранен в экземпляре дочернего компонента._props, и определяется как响应式属性, а в шаблоне дочернего компонента дляmsgдоступ на самом деле проксируется_props.msgвверх, поэтому, естественно, зависимости могут быть точно собраны, еслиChildComponentЭто свойство также считывается в шаблоне.

Здесь стоит обратить внимание на деталь, когда родительский компонент рендерится, он будет пересчитывать подкомпоненты.props, конкретно вupdateChildComponentсередина:

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    // 注意props被指向了 _props
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      // 就是这句话,触发了对于 _props.msg 的依赖更新。
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

Итак, из-за фрагмента кода, отмеченного в комментарии выше,msgменяется через_propsОтзывчивость подкомпонентов также приводит к повторному рендерингу подкомпонентов, которые до сих пор использовались только по-настоящему.msgКомпонент перерисовывается.

Как указано в официальной документации API сайта:

vm.$forceUpdate: принудительно выполнить повторный рендеринг экземпляра Vue. Обратите внимание, что это влияет только на сам экземпляр и субкомпоненты, которые вставляют содержимое слота, а не на все субкомпоненты. ——vm-forceОбновление документации

Нам нужно знать немного знаний,vm.$forceUpdateПо сути, это вызывает渲染watcherПовторное выполнение точно такое же, как когда вы изменяете отзывчивое свойство для запуска обновления, оно просто вызывает его для вас.vm._watcher.update()(просто предоставляет вам удобный API, вызываемый в режиме разработки门面模式)

Как обновляются слоты?

Обратите внимание, что здесь также упоминается деталь, а именно插入插槽内容的子组件:

Например

Предположим, у нас есть родительский компонентparent-comp:

<div>
  <slot-comp>
     <span>{{ msg }}</span>
  </slot-comp>
</div>

Подсборкаslot-comp:

<div>
   <slot></slot>
</div>

компонент содержитslotОбновление , является относительно особым сценарием.

здесьmsgКогда атрибуты собираются для зависимости, то, что собирается,parent-comp`наблюдатель рендеринга. (Что касается того, почему, вы можете увидеть контекст рендеринга, в котором он находится.)

Итак, мы представляемmsgОбновлено в это время,

<div>
  <slot-comp>
     <span>{{ msg }}</span>
  </slot-comp>
</div>

Когда этот компонент обновляется, он сталкивается с подкомпонентомslot-comp, в соответствии с точной стратегией обновления Vue дочерние компоненты не будут повторно отображаться.

Но внутри исходного кода он выносит решение и выполняетslot-compизprepatchКогда этот хук, он выполнитupdateChildComponentЛогика, внутри этой функции найдет этоslotэлемент.

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    // 注意 这个child就是 slot-comp 组件的 vm 实例,也就是咱们平常用的 this
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

существуетupdateChildComponentвнутренний

  const hasChildren = !!(
    // 这玩意就是 slot 元素
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject // has old scoped slots
  )

Тогда иди в суд

  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

звонил сюдаslot-compкомпонент на экземпляре vm$forceUpdate, то срабатывает渲染watcherпринадлежитslot-compиз渲染watcher.

В общем, на этот разmsgОбновление не только вызвалоparent-compПовторный рендеринг далее запускает подкомпоненты со слотамиslot-compповторный рендеринг.

Запускается только два рендеринга, еслиslot-compВнутренний и рендеринг других компонентовslot-child, то в это время он не будет рекурсивно обновляться. (если толькоslot-childКомпоненты больше не имеют слотов).

Это все еще намного лучше, чем рекурсивное обновление React?

Обновление родительского и дочернего компонентов будет проходить через два этапа.nextTick?

Ответ - нет: Обратите внимание на исходный кодqueueWatcherЛогика, глобальная переменная при обновлении родительского компонентаisFlushingравно true, поэтому он не будет ждать выполнения следующего тика, а будет помещен непосредственно в очередь и обновлен вместе за один тик.

родительский компонент обновленnextTickЭто будет выполняться в цикле, и оно будет выполняться в циклеqueueвнутреннийwatcher

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  for (index = 0; index < queue.length; index++) {
     // 更新父组件
     watcher.run()
  }
}

В процессе обновления родительского компонента запускается адаптивное обновление дочернего компонента, что приводит к срабатываниюqueueWatcher, потому чтоisFlushingЭто правда, это пойдетelseЛогика в подкомпонентахidбольше, чем родительский компонентid, поэтому он будет вставлен в родительский компонентwatcherПосле этого, после выполнения функции обновления родительского компонента, естественно будет выполняться функция обновления дочернего компонента.watcher. Это в том же тике.

if (!flushing) {
  queue.push(watcher)
} else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}

просто добавил это в очередьwatcherвыполнять напрямую.

Оптимизации для Vue 2.6

Vue 2.6 превращает вышеуказанное вslotОперация была дополнительно оптимизирована.Короче говоря, использование

<slot-comp>
  <template v-slot:foo>
    {{ msg }}
  </template>
</slot-comp>

Слоты, сгенерированные этим синтаксисом, будут компилироваться в функции единообразно и выполняться в контексте дочернего компонента, поэтому родительский компонент больше не будет собирать свои внутренние зависимости, если он не используется в родительском компоненте.msg, обновление повлияет только на сам дочерний компонент. вместо изменения родительского компонента_propsдля уведомления подкомпонентов об обновлении.

Подарите небольшой выпуск

Кто-то предложил версию Vue 2.4.2issue, ошибка будет возникать в следующих сценариях.

let Child = {
  name: "child",
  template:
    '<div><span>{{ localMsg }}</span><button @click="change">click</button></div>',
  data: function() {
    return {
      localMsg: this.msg
    };
  },
  props: {
    msg: String
  },
  methods: {
    change() {
      this.$emit("update:msg", "world");
    }
  }
};

new Vue({
  el: "#app",
  template: '<child :msg.sync="msg"><child>',
  beforeUpdate() {
    alert("update twice");
  },
  data() {
    return {
      msg: "hello"
    };
  },
  components: {
    Child
  }
});

Конкретная производительность кликclick按钮, дважды предупредитupdate twice. Это потому, что дочерний компонент выполняетсяdataКогда эта функция инициализирует данные компонента, она снова соберет их по ошибке.Dep.target(то есть,渲染watcher).

Поскольку время инициализации данныхbeforeCreated -> createdВ промежутке, так как этап рендеринга дочернего компонента еще не наступил,Dep.targetили родительский компонент渲染watcher.

Это приводит к повторному сбору зависимостей и повторному запуску одного и того же обновления.Конкретную производительность можно увидеть здесь:jsfiddle.net/sbmLobvr/9.

Как это решить? очень просто, выполнитьdataДо и после функции поставьтеDep.targetСначала установите значение null, затемfinallyТаким образом, отзывчивые данные не смогут собирать зависимости.

export function getData (data: Function, vm: Component): any {
  const prevTarget = Dep.target
+ Dep.target = null
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
+ } finally {
+   Dep.target = prevTarget
  }
}

постскриптум

если тыDep.target,渲染watcherЕсли вы все еще не понимаете концепции, вы можете прочитать статью, которую я написал о простейшей реализации отзывчивости Vue.

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

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

Особая благодарность

благодарныйДжи ЧжиБольшой парень исправил некоторые детали этой статьи.

❤️Спасибо всем

1. Если эта статья была вам полезна, пожалуйста, поддержите ее лайком, ваш "лайк" - движущая сила моего творчества.

2. Подпишитесь на официальный аккаунт «Front-end from advanced to accept», чтобы добавить меня в друзья, я втяну вас в «Front-end группу расширенного обмена», все смогут общаться и добиваться прогресса вместе.

公众号