Skip to content
This repository has been archived by the owner on May 8, 2023. It is now read-only.

Latest commit

 

History

History
302 lines (221 loc) · 17.3 KB

05-dynamic-data-lazy-sync-and-queue.md

File metadata and controls

302 lines (221 loc) · 17.3 KB

Изменяемые данные - Lazy, Sync и Queue (Лень, синхронность и очередь)

Этот материал - часть серии статей Читая исходный код Vue Source.

В этой части мы рассмотрим:

  • Три способа обновлелния Watcher
  • Как запустить обновление представления (view)
  • Как организовать порядок обновления

Три способа обновления Whatcher

В прошлой части мы разобрали как Vue работает с изменяемыми данными с помощью Observer, Dep и Watcher. Но это было мельком, есть ещё несколько важных вещей, которые стоит обсудить.

Вернёмся к ./watcher.js.

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

Напомню, когда вы обновляете реактивное свойство, вызывается его сеттер, что вызывает dep.notify(), который вызывает update() у своих подписчиков (тех, которые Watcher'ы).

Перейдём прямо в update().

/**
 * Интерфейс подписчика
 * Будет вызываться при изменении зависимостей
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

Эта конструкция if-else имеет три ветки, рассмотрим их одну за одной.

Lazy (Ленивое обновление)

Если наблюдатель создан ленивым - в соответствии с параметрами, переданными при инициализации, он просто будет помечен как dirty (изменённый).

Давайте найдём, где этот флай dirty используется.

Поискав dirty, вы получите:

/**
 * Вычисляет значение наблюдателя
 * Вызывается только для "ленивых" наблюдателей
 */
evaluate () {
  this.value = this.get()
  this.dirty = false
}

Когда вызывается evaluate(), он вызывает this.get() для получения актуального значения и утсановки флага dirty в false. Где вызывается evaluate() ? Сделаем поиск по всему проекту.

В Sublime Text - клик правой кнокой на директории src и выбираете Find in Folder....

Вводим evaluate и нажимаем Find.

Первый же результат - watcher.evaluate(), двойной клик по строчке перекинет нас в файл.

Код:

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

Вот оно. Когда вызывается геттер вычисляемого свойства, если наблюдатель помечен как dirty, будет выполнено вычислениe. Использование ленивого режима может отложить вычисления пока вам действительно не понадобится значение.

Sync (Синхронное обновление)

Вернёмся ко второй ветке конструкции if-else.

/**
 * Интерфейс подписчика
 * Будет вызываться при изменении зависимостей
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

Если у наблюдателя поле sync установлено в true, будет вызван this.run(). Поищем run.

/**
 * Интерфейс подписчика
 * Будет вызываться при изменении зависимостей
 */
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // "Глубокие" наблюдатели, и наблюдатели за объектами и массивами
      // должны отрабатывать даже если значение не изменилось
      // потому, что объект мог быть изменён
      isObject(value) ||
      this.deep
    ) {
      // устанавливаем новое значение
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

Эта фукнция вызывает this.get(). Если значение изменилось, или является объектом, или мы имеем дело с глубоким наблюдением, старое значение будет заменено и функция будет вызвана.

В прошлой части мы разобрали как работает this.get(), можете перечитать, если забыли.

Синхронный режим лёгок для понимания, но увы - значение этого флаза по-умолчанию - false. Самый частоиспользуемый режим - асинхронный.

Queue (Очередь)

/**
 * Интерфейс подписчика
 * Будет вызываться при изменении зависимостей
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

Если налюдатель не относится ни к синхронным, ни к ленивым, выполнение приведёт к queueWatcher(this).

/**
 * Закинем наблюдатель в очередь наблюдателей
 * Задачи с повторяющимися ID будут пропустаться
 * Пока состояние очереди не будет сброшено
 */
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      // Если сбросили состояние - удалим наблюдатель по его id
      // Если задача с таким id уже добавлена - переходим к следующей
      let i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // сброс очереди
    if (!waiting) {
      waiting = true;
      nextTick(flushSchedulerQueue);
    }
  }
}

Если очередь не в процессе очистки (флаг flushing), этот код просто добавит налюдателя в очередь.

Иначе - он попытается найти правильную позицию для наблюдателя на основании его id.

Ну и наконец, если флаг ожидания (waiting) не установлен - вываем flushSchedulerQueue() через nextTick.

Здесь мы встречаем два флага: flushing и waiting. Они кажутся очень похожими, так зачем нам два флага?

Можем зайти с другого конца. Что если бы мы использовали только flushing ?

flushing устанавливается в true, когда выполнена flushSchedulerQueue(). И обратите внимание, что функция flushSchedulerQueue() вызывается с помощью nextTick(), так что она не выполняется немедленно. Если мы вызовем queueWatcher() несколько раз подряд, мы получим дублирующиеся вызовы flushSchedulerQueue() на следующем шаге!

Вот оно. flushing помечает, что задачи в очереди выполняются, waiting указывает, что операция сброса очереди находится в очереди в nextTick.

Как запустить обновление представления (View)

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

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

Давайте снова воспользуемся глобальным поиском. Что будем искать? Помните те _update и _render, которые мы встретили в процессе инициализации. Давайте начнём с _update.

Похоже, что updateComponent в первом же результате это то, что нам нужно. Двойной клик и переходим к коду.

Код:

  } else {
    updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
}

Вот оно. Мы были правы, Vue создаёт наблюдателей для updateComponent. Эти строчки внутри mountComponent, а mountComponent - это ядро $mount. Так что после инициализации компонента, Vue вызывает $mount и уже внутри создаётся наблюдатель.

Когда новый наблюдатель (Watcher) создан, его флаг lazy по-умолчанию установлен в false, так что в конце конструктора вызывается get() и создаётся целая сеть динамических данных.

Обратите внимание, что updateComponent это второй параметр, и он станет геттером у наблюдателя. Так что когда Vue попытается получить значение, это обновит представление. Если это сложно понять - можете думать об этом геттере как об обёртке над оригинальным геттером, который обновляет представление после вызова оригинального геттера (строчка vm.render()).

Немного мудрено, но это работает.

Как организовать порядок обновления

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

Как гарантировать, что все данные и представления обновляютс в правильном порядке?

Рассмотрим маленький пример:

<div id="app">{{ newName }}</div>

var app = new Vue({ el: '#app', data: { name: 'foo' }, computed: { newName () {
return this.name + 'new!' } } })

В этом примере у нас есть одно свойство и одно вычисляемое свойство. И мы выводим это вычисляемое свойство в шаблоне.

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

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

Как Vue решает эту проблему?

Давайте представим процесс обновления.

Когда обновляется name, это вызывает dep.notify(). notify() пройдёт по массиву подписчиков и вызовет у них update(). По-умолчанию, наблюдатели имеют и lazy и syn в стостоянии false, так что обе задачи будут добавлены в очередь.

Ок, ключевой момент здесь - порядок задач.

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

Очередь создаёт новую проблему: если вы используете очередь и читаете вычисляемые свойство сразу за изменением данных, вы получите старое значение. но после поиска по проекту, мы узнаем, что sync всегда true, так что , похоже, очередь никогда не используется.

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

Вы можете спросить, что если бы вычисляемый наблюдатель был lazy ? Это я оставлю вам.

Следующий шаг

Мы разобрали три варианта обновления и как поддерживается правильный порядок обновления. Но это все происходит внутри, a как Vue применяет это к обновлению DOM? Как преобразовать ваши .vue файлы в код, выполняемый браузером? Следующие несколько статей мы поговорим провесь процесс рендеринга.

В следующей части: Ввердение в рендеринг представлений.

Практика

Попробуйте разобраться как Vue разбирается с корректным порядком для наблюдателей с флагом lazy.

Подсказка: просчитайте, что происходит при обновлении самостоятельно.