Оригинал: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
Вот перевод:
Что такое цикл событий (Цикл событий, обратите внимание на пробелы)
JavaScript однопоточный, с благословлением Event Loop, node.js может выполнять операции ввода/вывода без блокировки, и передавать эти операции в операционную систему.
Мы знаем, что большинство современных операционных систем являются многопоточными, и эти операционные системы могут выполнять несколько операций в фоновом режиме. Когда операция завершена, операционная система уведомит Node.js, и Node.js (возможно) добавит соответствующие функции обратного вызова в очередь опроса (опроса), и в конечном итоге эти функции обратного вызова будут выполнены. Мы объясним его детали ниже.
Сведения о цикле событий
Когда Node.js запускается, он делает следующее
- Инициализировать цикл событий
- Начните выполнять скрипт (или в reft, эта статья не включает REPL). Сценарий может вызвать некоторую асинхронную API, установить таймер или путем вызова процесса .NextTick ()
- Начать обработку цикла событий
Как обрабатывать цикл событий? На следующем рисунке представлен простой обзор:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
Каждое из этих полей является этапом в цикле событий.
На каждом этапе есть «очередь в порядке очереди», в которой хранятся функции обратного вызова, которые должны быть выполнены. Но у каждого этапа есть своя уникальная миссия. Вообще говоря, когда цикл событий достигает определенного этапа, на этом этапе будут выполняться какие-то специальные операции, а затем будут выполняться все коллбэки в очереди этого этапа. Когда прекратить выполнение этих обратных вызовов? Одно из двух остановит:
- Все операции в очереди выполнены
- Количество выполненных обратных вызовов достигает указанного максимального значения Затем цикл событий переходит к следующему этапу, а затем к следующему этапу.
С одной стороны, вышеуказанные операции могут добавлять таймеры, с другой стороны, операционная система будет добавлять новые события в очередь опроса, а новые события опроса могут поступать в очередь опроса по мере обработки событий в очереди опроса. В результате длинная функция обратного вызова может удерживать цикл событий в фазе опроса так долго, что он пропускает время срабатывания таймера. Вы можете узнать больше об этом в разделе таймеров и разделе опроса ниже.
Обратите внимание, что реализация для Windows и реализация для Unix/Linux немного отличаются, но мало влияют на эту статью. В этой статье рассматривается наиболее важная часть цикла событий, на разных платформах может быть семь или восемь этапов, но вышеперечисленные этапы — это этапы, которые нам действительно важны, а на реальном этапе используется Node.js.
Обзор каждого этапа
- Фаза таймеров: на этой фазе выполняются функции обратного вызова setTimeout и setInterval.
- Этап обратных вызовов ввода-вывода: обратные вызовы, которые не выполняются на трех этапах этапа таймеров, этапа обратных вызовов закрытия и этапа проверки, отвечают за этот этап, который содержит почти все функции обратного вызова.
- холостой ход, этап подготовки
- Этап опроса: получение новых событий ввода-вывода. В некоторых случаях Node.js блокируется на этом этапе.
- фаза проверки: выполнить функцию обратного вызова setImmediate().
- Этап закрытых обратных вызовов: выполнить функцию обратного вызова события закрытия, например, fn в socket.on('close', fn).
Когда программа Node.js завершается, Node.js проверяет, ожидает ли цикл событий завершения асинхронной операции ввода-вывода, ожидает ли он срабатывания таймера, и, если нет, закрывает цикл событий.
Подробное объяснение каждого этапа
этап таймеров
Вместо того, чтобы указывать точное время выполнения функции, таймер на самом деле указывает, сколько времени до того, как функция обратного вызова может быть выполнена. Когда указанное время истечет, функция обратного вызова таймера будет выполнена как можно скорее. Если операционная система занята или Node.js выполняет трудоемкую функцию, функция обратного вызова таймера будет отложена.
Обратите внимание, что, в принципе, фаза опроса определяет, когда выполняется функция обратного вызова таймера.
Например, вы установили таймер на выполнение через 100 мс, а вашему скрипту требуется 95 мс для асинхронного чтения файла:
const fs = require('fs');
function someAsyncOperation(callback) {
// 假设读取这个文件一共花费 95 毫秒
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}毫秒后执行了 setTimeout 的回调`);
}, 100);
// 执行一个耗时 95 毫秒的异步操作
someAsyncOperation(() => {
const startCallback = Date.now();
// 执行一个耗时 10 毫秒的同步操作
while (Date.now() - startCallback < 10) {
// 什么也不做
}
});
Когда цикл событий входит в фазу опроса, обнаруживается, что очередь опроса пуста (поскольку файл не завершен), цикл событий проверяет самый последний таймер, вероятно, есть 100 миллисекунд, поэтому цикл событий решает на этот раз остановиться на этапе опроса. за это время. . После того, как фаза опроса остановилась на 95 миллисекунд, операция fs.readfile была завершена, и функция обратного вызова на 10 миллисекунд была помещена в очередь опроса, поэтому цикл обработки событий выполнил эту функцию обратного вызова. После выполнения команда опроса была пуста, поэтому цикл событий отправился к ближайшему таймеру (перевод: цикл событий нашел лежащий слот, его время 95 + 10 - 100 = 5 миллисекунд), поэтому через фазу CHECK, Close Фаза обратных вызовов Вернитесь к фазе таймеров, выполните функцию обратного вызова в очереди таймеров. В этом примере 100-миллисекундный таймер фактически выполняется через 105 миллисекунд.
Примечание. Чтобы фаза опроса не занимала все время цикла событий, libuv (библиотека, написанная на языке C, используемая Node.js для реализации цикла событий и всех асинхронных поведений) ограничивает максимальное время пребывания опроса. фаза, конкретное время Зависит от операционной системы.
Этап обратных вызовов ввода/вывода
На этом этапе будут выполняться некоторые функции обратного вызова для системных операций, такие как отчет об ошибках TCP.Если ошибка ECONNREFUSED возникает при подключении TCP-сокета, некоторые системы *nix уведомляют (в Node.js) об этой ошибке. Уведомление будет помещено в очередь обратных вызовов ввода/вывода.
фаза опроса (фаза опроса)
Этап опроса выполняет две функции:
- Если время таймера найдено, он вернется к фазе таймеров, чтобы выполнить обратный вызов таймера.
- Затем выполните обратный вызов в очереди опроса.
Когда цикл обработки событий входит в фазу опроса, если он обнаружит, что таймер отсутствует, он будет:
- Если очередь опроса не пуста, цикл обработки событий будет по очереди выполнять функции обратного вызова в очереди, пока очередь не опустеет или не будет достигнуто ограничение по времени для фазы опроса.
- Если очередь опроса пуста, она:
- Если есть задача setImmediate(), цикл событий завершает фазу опроса и переходит к фазе проверки.
- Если нет задачи setImmediate(), цикл обработки событий будет ждать, пока новая функция обратного вызова войдет в очередь опроса, и немедленно ее выполнит.
Когда очередь опроса опустеет, цикл событий проверит таймер на предмет отсутствия истечения срока действия, если есть истечение срока действия таймера, цикл событий вернется к фазе таймеров, чтобы выполнить обратный вызов таймера.
этап проверки
Эта фаза позволяет разработчикам выполнять некоторые функции сразу после завершения фазы опроса. Если фаза опроса простаивает и в то же время выполняется задача setImmediate(), цикл обработки событий переходит в фазу проверки.
setImmediate() на самом деле представляет собой особый вид таймера со своей фазой. Он реализован через API в libuv, который может планировать выполнение обратных вызовов после фазы опроса.
Вообще говоря, когда код выполняется, цикл событий в конечном итоге достигает стадии опроса, ожидая новых соединений, новых запросов и т. д. Но если обратный вызов выдается setImmediate(), а фаза опроса простаивает, цикл событий завершит фазу опроса и перейдет к фазе проверки, больше не ожидая новых событий опроса.
(Аннотация: мне кажется, что я сказал одно и то же три раза)
закрыть этап обратных вызовов
Если сокет или дескриптор внезапно отключаются (например, socket.destroy()), для входа в эту фазу будет происходить событие CLOSE. В противном случае (перевод: я не видел этого иначе в отрицании, это отрицательное «внезапное»?), это событие закрытия войдет в process.nexttick().
setImmediate() vs setTimeout()
setImmediate похож на setTimeout, но время его функции обратного вызова отличается.
Роль setImmediate() заключается в вызове функции после завершения текущей фазы опроса. Что делает setTimeout(), так это вызывает функцию через определенное время. Порядок, в котором выполняются эти два обратных вызова, зависит от среды, в которой вызываются setTimeout и setImmediate.
Если в основном модуле вызываются и setTimeout, и setImmediate, порядок выполнения обратных вызовов зависит от производительности текущего процесса, на который влияют другие процессы приложения.
Например, если следующий сценарий запускается в основном модуле, порядок выполнения двух обратных вызовов не определен:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
Результаты приведены ниже:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
Однако, если вы поместите приведенный выше код в обратный вызов операции ввода-вывода, обратный вызов setImmediate всегда будет иметь приоритет над обратным вызовом setTimeout:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
Результаты приведены ниже:
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
Основное преимущество setImmediate заключается в том, что при обратном вызове операции ввода-вывода обратный вызов setImmediate всегда выполняется перед обратным вызовом setTimeout. (Примечание переводчика: почему вы всегда говорите правду снова и снова)
process.nextTick()
Вы можете обнаружить, что важный асинхронный API process.nextTick() отсутствует ни на одном этапе, потому что технически process.nextTick() не является частью цикла обработки событий. Фактически, независимо от того, на каком этапе находится цикл обработки событий, очередь nextTick выполняется после текущего этапа.
Оглядываясь назад на нашу диаграмму этапов, мы видим, что если вы вызовете process.nextTick(callback) на любом этапе, обратный вызов будет вызван до того, как текущий этап продолжит выполнение. Такое поведение может иногда приводить к плохим результатам, потому что вы можете рекурсивно вызывать process.nextTick(), поэтому цикл обработки событий всегда останавливается на текущем этапе... и не может перейти на этап опроса.
Почему Node.js проектирует process.nextTick таким образом?
Поскольку некоторые асинхронные API должны обеспечивать согласованность, даже если они могут выполняться синхронно, порядок асинхронных операций должен быть гарантирован. См. следующий код:
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback, new TypeError('argument should be string'));
}
Этот код проверяет тип параметра и, если тип не строковый, передает ошибку обратному вызову.
Этот код гарантирует, что синхронный код после вызова apiCall запускается перед обратным вызовом. Используется для использования process.nextTick(), поэтому обратный вызов будет выполнен до того, как цикл обработки событий перейдет к следующему этапу. Для этого стек вызовов JS можно раскрутить перед выполнением обратного вызова nextTick, так что независимо от того, сколько раз вы рекурсивно вызываете process.nextTick(), это не приведет к переполнению стека вызовов (RangeError: Maximum call stack размер превышен в V8).
Если это не спроектировано таким образом, это вызовет некоторые потенциальные проблемы, такие как следующий код:
let bar;
// 这是一个异步 API,但是却同步地调用了 callback
function someAsyncApiCall(callback) { callback(); }
//`someAsyncApiCall` 在执行过程中就调用了回调
someAsyncApiCall(() => {
// 此时 bar 还没有被赋值为 1
console.log('bar', bar); // undefined
});
bar = 1;
Хотя разработчик назвал someAsyncApiCall асинхронной функцией, на самом деле эта функция выполняется синхронно. Когда вызывается someAsyncApiCall, обратный вызов также вызывается в той же фазе цикла событий. В результате значение bar не может быть получено в обратном вызове. Потому что оператор присваивания еще не был выполнен.
Если обратный вызов выполняется в process.nextTick(), последующий оператор присваивания может быть выполнен первым. И обратный вызов process.nextTick() будет вызван до того, как eventLoop перейдет к следующему этапу. (Аннотация: Опять же, истина повторяется снова и снова)
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
Вот более реалистичный пример:
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
Предложение .listen(8080) выполняется синхронно. Проблема в том, что обратный вызов прослушивания не может быть запущен, потому что код прослушивания для прослушивания находится после .listen(8080).
Чтобы решить эту проблему, функция .listen() может использовать process.nextTick() для выполнения обратного вызова события прослушивания.
process.nextTick() vs setImmediate()
Эти две функции похожи по функциям и имеют сбивающее с толку название.
Обратный вызов Process.NextTick () будет выполнен «немедленно» во время текущего этапа цикла событий. Обратный вызов Setimmediate () будет выполнен в последующем цикле цикла событий (галочкой).
(Аннотация: Похоже, имя перевернуто)
Их имена следует поменять местами. process.nextTick() немного быстрее, чем setImmediate().
Это устаревшая проблема, и вряд ли она будет улучшена для обеспечения обратной совместимости. Так что, даже если эти два названия звучат запутанно, в будущем никаких изменений не будет.
Мы рекомендуем разработчикам использовать setImmediate() во всех случаях, поскольку он более совместим и понятен.
Когда использовать process.nextTick()?
Есть две основные причины: Есть две причины его использовать:
- Позвольте разработчику обрабатывать ошибки, очищать бесполезные ресурсы или пытаться повторно запрашивать ресурсы до завершения текущей фазы цикла событий.
- Иногда необходимо, чтобы обратный вызов выполнялся после раскручивания стека вызовов, но до того, как цикл обработки событий перейдет на следующую стадию.
Чтобы сделать код более разумным, мы могли бы написать такой код:
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
Предполагая, что listen () выполняется при запуске цикла событий, а обратный вызов события прослушивания помещается в setImmediate (), действие прослушивания происходит немедленно.Если вы хотите, чтобы цикл событий выполнял обратный вызов прослушивания, вы должны сначала перейти через этап опроса этап опроса может остаться в это время для ожидания соединения, поэтому возможно, что обратный вызов события подключения будет выполнен до обратного вызова события прослушивания. (Аннотация: это явно неразумно, поэтому нам нужно использовать process.nextTick)
В качестве другого примера класс расширяет EventEmitter и хочет инициировать событие при создании экземпляра:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
Вы не можете сделать this.emit('event') непосредственно в конструкторе, потому что тогда последующий обратный вызов никогда не будет выполнен. Поместите this.emit('event') в process.nextTick(), и последующий обратный вызов может быть выполнен. Это наше ожидаемое поведение:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});