En En

Цикл исполнения


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

Это достигается путем планировки работы в особых очередях. Очереди имеют порядок срочности и обрабатываются согласно этому порядку.

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

Самый частый случай использования такого цикла — интеграция с API не-Ember, который включает какой-либо асинхронный обратный вызов. Например:

  • Обновление DOM и обратные вызовы событий;
  • Обратные вызовы setTimeout и setInterval;
  • Обработчики событий postMessage и messageChannel;
  • Обратные вызовы AJAX;
  • Обратные вызовы Websocket.

Чем полезен цикл исполнения?

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

Рассмотрим следующий фрагмент HTML:

<div id="foo"></div>
<div id="bar"></div>
<div id="baz"></div>

И выполним следующий код:

foo.style.height = '500px' // write
foo.offsetHeight // read (recalculate style, layout, expensive!)

bar.style.height = '400px' // write
bar.offsetHeight // read (recalculate style, layout, expensive!)

baz.style.height = '200px' // write
baz.offsetHeight // read (recalculate style, layout, expensive!)

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

foo.style.height = '500px' // write
bar.style.height = '400px' // write
baz.style.height = '200px' // write

foo.offsetHeight // read (recalculate style, layout, expensive!)
bar.offsetHeight // read (fast since style and layout is already known)
baz.offsetHeight // read (fast since style and layout is already known)

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

Давайте взглянем на простой пример, который оптимизирован в Ember, начиная с объекта User:

var User = Ember.Object.extend({
  firstName: null,
  lastName: null,
  fullName: Ember.computed('firstName', 'lastName', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  })
});

и шаблона для отображения атрибутов:

{{firstName}}
{{fullName}}

Если мы выполним следующий код без цикла исполнения:

var user = User.create({ firstName: 'Tom', lastName: 'Huda' });
user.set('firstName', 'Yehuda');
// {{firstName}} and {{fullName}} are updated

user.set('lastName', 'Katz');
// {{lastName}} and {{fullName}} are updated

Мы увидим, что браузер отобразит шаблон дважды.

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

var user = User.create({ firstName: 'Tom', lastName: 'Huda' });
user.set('firstName', 'Yehuda');
user.set('lastName', 'Katz');
user.set('firstName', 'Tom');
user.set('lastName', 'Huda');

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

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

Как цикл исполнения работает в EMBER?

Как упоминалось ранее, мы планируем работу (в форме вызовов функции) по очередям, и эти очереди обрабатываются согласно порядку срочности.

Что такое очереди, и что за порядок срочности?

Ember.run.queues
// => ["sync", "actions", "routerTransitions", "render", "afterRender", "destroy"]

Так как приоритет — это следование от первого пункта до последнего, очередь «синхронизации» имеет более высокий приоритет перед очередями «отображения» или «удаления».

Что происходит в этих очередях?

  • Очередь sync содержит задачи по синхронизации привязок.
  • Очередь actions выполняет общую работу и обычно содержит запланированные задачи, например, обещания.
  • Очередь routerTransitions включает задачи по переходу в роутере.
  • Очередь render содержит задачи по отображению; обычно они связаны с обновлением DOM.
  • Очередь afterRender включает задачи, которые запускаются после выполнения всех предыдущих запланированных задач по отображению. Для сторонних библиотек, которые предназначены для работы с DOM, это зачастую хорошо, так как их следует запускать только после того, как все дерево DOM обновится.
  • Очередь destroy содержит задачи по завершении удаления объектов, которые согласно другим задачам запланировано убрать.

в каком порядке задачи выполняются в очередях?

Алгоритм работает следующим образом:

  1. Приоритетная очередь с группой задач определяется как CURRENT_QUEUE; если нет очередей с группой задач, цикл исполнения считается завершенным.
  2. Новая временная очередь определяется как WORK_QUEUE.
  3. Задачи из CURRENT_QUEUE перемещаются в WORK_QUEUE.
  4. В WORK_QUEUE последовательно обрабатываются все задачи.
  5. Возвращение к пункту 1.

Пример внутренних составляющих

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

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

Пример можно посмотреть на оригинальном сайте.

Как сообщить EMBER, чтобы он начал цикл исполнения?

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

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

Этот пример использует синтаксис функции =>, который является новым синтаксисом ES2015 для функций обратного вызова. Он предоставляет лексический this. Если этот синтаксис новый для вас, рассматривайте его как функцию, которая имеет тот же this, что и контекст, где она определена.

$('a').click(() => {
  Ember.run(() => {  // begin loop
    // Code that results in jobs being scheduled goes here
  }); // end loop, jobs are flushed and executed
});

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

Как упоминалось выше, вам следует заключать любые асинхронные обратные вызовы не-Ember в Ember.run. Если вы этого не сделаете, Ember попытается приблизить для вас начало и конец. Рассмотрим следующий обратный вызов:

$('a').click(() => {
  console.log('Doing things...');

  Ember.run.schedule('actions', () => {
    // Do more things
  });
});

Вызовы API цикла исполнения, которые планируют работу, то есть run.schedule, run.scheduleOnce, run.once, имеют свойство приближать цикл исполнения, если его еще не существует. Эти автоматически созданные циклы исполнения мы называем автозапусками (autoruns).

Ниже представлен некоторый псевдокод. Он описывает, что происходит, если использовать пример выше:

$('a').click(() => {
  // 1. autoruns do not change the execution of arbitrary code in a callback.
  //    This code is still run when this callback is executed and will not be
  //    scheduled on an autorun.
  console.log('Doing things...');

  Ember.run.schedule('actions', () => {
    // 2. schedule notices that there is no currently available runloop so it
    //    creates one. It schedules it to close and flush queues on the next
    //    turn of the JS event loop.
    if (! Ember.run.hasOpenRunloop()) {
      Ember.run.start();
      nextTick(() => {
        Ember.run.end()
      }, 0);
    }

    // 3. There is now a runloop available so schedule adds its item to the
    //    given queue
    Ember.run.schedule('actions', () => {
      // Do more things
    });

  });

  // 4. This schedule sees the autorun created by schedule above as an available
  //    runloop and adds its item to the given queue.
  Ember.run.schedule('afterRender', () => {
    // Do yet more things
  });
});

Хотя автозапуски удобны, они субоптимальные. Текущая структура JS завершается прежде, чем заканчивается цикл исполнения. Иногда это значит, что браузер воспользуется случаем и выполнит еще какую-нибудь работу вроде чистки памяти. Чистка, которая запускается между изменением данных и новым отображением DOM, может привести к визуальным задержкам. Поэтому ее следует минимизировать.

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

Как поведение цикла исполнения различается во время тестирования?

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

Автозапуски отключены во время тестирования по нескольким причинам:

  1. Автозапуски — это способ Ember «не наказывать» вас во время производства, если вы забыли открыть цикл исполнения до того, как запланировали в нем обратные вызовы. Это полезно на стадии производства, но эти ситуации все еще следует обнаруживать во время тестирования, чтобы править их.
  2. Обещания в Ember можно считать помощниками для тестирования. Они ожидают, пока цикл исполнения опустеет, прежде чем завершиться. Если ваше приложение имеет код, который запускается вне цикла исполнения, они завершатся слишком рано и дадут ложные отказы теста. А их сложно обнаружить. Отключение автозапусков поможет вам определить эти сценарии и протестировать приложение.

Где я могу найти больше информации?

Посмотрите документацию по API Ember.run и библиотеке Backburner, которая применяется для цикла исполнения.


Комментарии (0)

    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.