Цикл событий Node.js «Атака фронтенд-инженера»

Node.js внешний интерфейс
Цикл событий Node.js «Атака фронтенд-инженера»

В этой статье используется лицензионное соглашение «Signature 4.0 International (CC BY 4.0)», приветствуется перепечатка или изменение для использования, но источник должен быть указан.Атрибуция 4.0 Международная (CC BY 4.0)

Восприятие: 🌟🌟🌟🌟🌟

Вкус: Французская фуа-гра

Время приготовления: 20мин

цикл событий

Последовательность выполнения цикла событий можно увидеть на рисунке.Каждый цикл событий содержит этапы 6 на рисунке выше.Далее давайте интерпретируем их один за другим.

таймеры таймеры

Таймеры делятся на две категории:

  • Немедленное выполняется на следующем этапе проверки
  • Тайм-аут выполняется после истечения таймера (значение параметра задержки по умолчанию равно 1 мс)

Существует два типа таймеров тайм-аута:

  • Interval
  • Timeout

这个阶段会执行setTimeout()和setInterval()设定的回调

timers的执行是由poll阶段控制的

setTimeout() и setInterval() такие же, как API в браузере. Принцип их реализации аналогичен асинхронному вводу-выводу, но не требует участия пула потоков ввода-вывода.

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

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

Нет обработки ввода-вывода

setTimeout(function timeout () {
    console.log('timeout');
},0);
setImmediate(function immediate () {
    console.log('immediate');
});

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

Поскольку setTimeout(fn, 0) имеет погрешность в несколько миллисекунд, нет гарантии, что, войдя в фазу таймера, таймер сможет немедленно выполнить обработчик.

Есть обработка ввода-вывода

var fs = require('fs');
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
})
// immediate
// timeout

В это время setImmediate имеет приоритет над setTimeout, потому что после завершения фазы опроса она переходит в фазу проверки, а фаза таймеров находится в следующей фазе цикла событий.

ожидающие обратные вызовы ожидающие обратные вызовы

执行部分回调,除了close,times和setImmediate()设定的回调

会在下一次loop中执行的系统级回调队列,如TCP的错误捕获等

праздный, готовься

仅供内部使用

опрос

获取新的I/O事件,在适当的条件下,Node.js会在这里阻塞

这个阶段的主要任务是执行到达delay时间的timers定时器的回调,并且处理poll队列里的事件。

Когда цикл событий переходит в фазу опроса, а таймер не вызывается, происходят две вещи:

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

2. Если очередь опроса пуста, возможны два случая:

  • При вызове обратным вызовом setImmediate() цикл событий завершит фазу опроса и перейдет в фазу проверки.

  • Если обратный вызов setImmediate() не вызывается, цикл обработки событий будет заблокирован и будет ожидать добавления обратного вызова в очередь опроса для выполнения.

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

проверить обнаружение

setImmediate()设定的回调会在这一阶段执行

Как и во втором случае фазы опроса выше, если очередь опроса пуста и вызвана обратным вызовом setImmediate(), цикл обработки событий перейдет непосредственно к фазе проверки.

закрыть обратные вызовы Закрыть функцию обратного вызова

socket.on('close',callback)的回调会在这个阶段执行

libuv

libuv为Node.js提供了整个事件循环功能。

Как показано выше, в Windows цикл событий основан наIOCPСоздать, пройти под linuxepollРеализовано, перешло под FreeBSDkqueueРеализация под Solaris черезEvent portsвыполнить.

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

По сути, когда мы пишем код JavaScript для вызова основного модуля Node, основной модуль будет вызывать встроенный модуль C++, а встроенный модуль будет выполнять системные вызовы через libuv.

Основная проблема, которую решает libuv

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

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

Интерпретация основного исходного кода

Основная функция uv_run

исходный код

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
    int timeout;
    int r;
    int ran_pending;
    // 检查loop中是否有异步任务,没有就结束。
    r = uv__loop_alive(loop);
    if (!r)
      uv__update_time(loop);
    // 事件循环while
    while (r != 0 && loop->stop_flag == 0) {
        // 更新事件阶段
        uv__update_time(loop);  
        // 处理timer回调
        uv__run_timers(loop);        
        // 处理异步任务回调
        ran_pending = uv__run_pending(loop);      
        // 供内部使用
        uv__run_idle(loop);
        uv__run_prepare(loop);        
        // uv_backend_timeout计算完毕后,会传给uv__io_poll
        // 如果timeout = 0,则uv__io_poll会直接跳过
        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending || mode == UV_RUN_DEFAULT))
          timeout = uv_backend_timeout(loop);
        uv__io_poll(loop, timeout);
        // check阶段
        uv__run_check(loop);
        // 关闭文件描述符等操作
        uv__run_closing_handles(loop);
        // 检查loop中是否有异步任务,没有就结束。
        r = uv__loop_alive(loop);
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
    }
    return r;
}

Настоящее лицо цикла событий — это время.

Как упоминалось выше, сетевой ввод-вывод, файловый ввод-вывод, DNS и т. д. реализуются двумя механизмами.

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

(uv__io_pollЧасть кода слишком длинная, если интересно, можете проверить сами)

uv__io_start

void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  assert(0 == (events & ~(POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI)));
  assert(0 != events);
  assert(w->fd >= 0);
  assert(w->fd < INT_MAX);
  w->pevents |= events;
  maybe_resize(loop, w->fd + 1);
  if (w->events == w->pevents)
    return;
  if (QUEUE_EMPTY(&w->watcher_queue))
    QUEUE_INSERT_TAIL(&loop->watcher_queue, &w->watcher_queue);
  if (loop->watchers[w->fd] == NULL) {
    loop->watchers[w->fd] = w;
    loop->nfds++;
  }
}

Как показано в процессе реализации основной линии сетевого ввода-вывода libuv.

Другая основная линия заключается в том, что такие операции, как Fs I/O и DNS, будут вызыватьuv__work_sumitЭта функция, эта функция предназначена для выполнения инициализации пула потоков.uv_queue_workФункция, которая в итоге вызывается в .

void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once);
  w->loop = loop;
  w->work = work;
  w->done = done;
  post(&w->wq, kind);
}
int uv_queue_work(uv_loop_t* loop,
                  uv_work_t* req,
                  uv_work_cb work_cb,
                  uv_after_work_cb after_work_cb) {
  if (work_cb == NULL)
    return UV_EINVAL;
  uv__req_init(loop, req, UV_WORK);
  req->loop = loop;
  req->work_cb = work_cb;
  req->after_work_cb = after_work_cb;
  uv__work_submit(loop,
                  &req->work_req,
                  UV__WORK_CPU,
                  uv__queue_work,
                  uv__queue_done);
  return 0;
}

Очередь событий в Node.js

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

В родном цикле событий libuv есть четыре основных типа очередей:

  • Истекшие таймеры и интервальные очереди

  • очередь событий ввода-вывода

  • Немедленная очередь

  • закрыть очередь обработчиков

Кроме того, в Node.js есть две промежуточные очереди.

  • Следующая очередь тиков

  • Другая очередь микрозадач

Различия между Node.js и циклом событий браузера

Мы можем просмотреть цикл событий JavaScript в браузере, пожалуйста, посетите мои другие серии столбцовСерия "Attack Front-End Engineer" - Цикл обработки событий JavaScript в браузере

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

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

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

(Макрозадача в этой статье называется задачей в WHATWG. Макротаска не имеет фактического источника для простоты понимания.)

По сравнению с браузерами, node имеет большеsetImmediate(宏任务)а такжеprocess.nextTick(微任务)Оба являются асинхронными операциями.

setImmediateФункция обратного вызова находится вcheckсценическое исполнение. а такжеprocess.nextTickбудет рассматриваться какmicrotask, после каждого этапа будут выполняться всеmicrotask, вы можете понимать это какprocess.nextTickМогурезать по линии, выполненный перед следующим этапом.

Опасности перехода process.nextTick из очереди

Обратный вызов process.nextTick предотвратит переход цикла событий к следующему этапу. После того, как обработка ввода-вывода завершена или таймер истек, ее все еще нельзя выполнить. приведет к голоданию других обработчиков событий. Чтобы предотвратить это, Node.js предоставляетprocess.maxTickDepth(по умолчанию 1000).

Микрозадачи в Node.js

  • process.nextTick()
  • Promise.then()
Promise.resolve().then(function(){
    console.log('then')
})
process.nextTick(function(){
    console.log('nextTick')
});
// nextTick
// then

Мы видим, что nextTick выполняется до этого.

Цикл событий для изменений Node.js v11

Начиная с Node.js v11 принцип цикла событий изменился: пока макрозадача выполняется на одном и том же этапе, очередь микрозадач будет выполняться немедленно, что согласуется с производительностью браузера. Пожалуйста, обратитесь к этомуpr.

❤️ После прочтения трех вещей

1. Когда увидишь это, ставь лайк и поддержи, твой лайк - движущая сила моего творчества.

2. Подпишитесь на официальный аккаунт前端食堂, ваша передовая столовая, не забывайте есть вовремя!

3. Сейчас зима, не простудитесь, надевая больше одежды~!