Введение
В этой статье подробно объясняются две наиболее сложные части nodejs.Асинхронный ввод-вывода такжецикл событий, чтобы разобраться и дополнить основные точки знаний о nodejs.
Отправка роз, в моей руке стойкий аромат, я надеюсь, что ученики, которые чувствуют себя хорошо после прочтения, могут датьпоставить лайк, побуждает меня продолжать создавать внешний жесткий текст.
Как обычно, начнём сегодняшний разбор с вопросов🤔🤔🤔:
- 1 Говорите об асинхронном вводе-выводе nodejs?
- 2 Говорите о механизме цикла событий nodejs?
- 3 Представьте различные этапы цикла событий в nodejs?
- 4 В чем разница между promise и nextTick в nodejs?
- 5 В чем разница между setImmediate и setTimeout в nodejs?
- 6 Является ли setTimeout точным и что влияет на выполнение setTimeout?
- 7 В чем разница между циклом обработки событий в nodejs и в браузере?
Два асинхронных ввода/вывода
концепция
Процессор может получить доступ к любым ресурсам данных, кроме регистров и кэша, в качестве операций ввода-вывода, включая внешние устройства, такие как память, диск и графическая карта.Звоните как разработчик в Nodejsfs
Такие операции, как чтение локальных файлов или сетевых запросов, являются операциями ввода-вывода. (Наиболее распространенными абстрактными операциями ввода-вывода являются файловые операции и сетевые операции TCP/UDP.)
Nodejs является однопоточным.В однопоточном режиме задачи выполняются последовательно, но если предыдущая задача выполняется слишком долго, это неизбежно повлияет на ход выполнения последующих задач.Обычно возможен расчет между вводом-выводом и процессором.Это выполняется параллельно, но в синхронном режиме ввод-вывод вызовет ожидание последующих задач, что блокирует выполнение задач, а также приводит к неэффективному использованию ресурсов.
Для того, чтобы решить вышеуказанную проблему,Nodejs выбирает режим асинхронного ввода-вывода, чтобы один поток больше не блокировался и ресурсы использовались более рационально.
Как разумно рассматривать асинхронный ввод-вывод в Nodejs
Front-end разработчики могут больше знать об асинхронных задачах JS в среде браузера, таких как однократная инициацияajax
Запрос, точно так же, как ajax — это API, предоставляемый браузером для вызываемой среды выполнения js, модуль http предоставляется в Nodejs, чтобы позволить js делать то же самое. Например, мониторинг|отправка http запросов, помимо http, в nodejs есть еще и файловая система fs для работы с локальными файлами и т.д.
Как указано выше, fs http Эти задачи называются задачами ввода-вывода в nodejs. Разобравшись с задачей ввода-вывода, давайте проанализируем две формы задачи ввода-вывода в Nodejs — блокирующую и неблокирующую.
Nodejs синхронный и асинхронный режим IO
nodejs Для большинства операций ввода-вывода предусмотреныблокироватьа такженеблокирующийДва использования. Блокировка означает, что при выполнении операции ввода-вывода вы должны дождаться результата, прежде чем выполнять код js. Код блокировки выглядит следующим образом
Синхронный режим ввода/вывода
/* TODO: 阻塞 */
const fs = require('fs');
const data = fs.readFileSync('./file.js');
console.log(data)
- Блокировка кода: читать каталог на том же уровне
file.js
файл, результатdata
дляbuffer
структура, так что в процессе чтения выполнение кода будет заблокировано, поэтомуconsole.log(data)
Будет заблокирован, только когда результат будет возвращен, его можно будет распечатать нормальноdata
. - Обработка исключений: фатальная точка описанной выше операции заключается в том, что если возникает исключение (например, в каталоге того же уровня нет файла file.js), вся программа сообщит об ошибке, и следующий код не будет выполнен. . Попытка перехвата обычно требуется для перехвата границ ошибок. код показывает, как показано ниже:
/* TODO: 阻塞 - 捕获异常 */
try{
const fs = require('fs');
const data = fs.readFileSync('./file1.js');
console.log(data)
}catch(e){
console.log('发生错误:',e)
}
console.log('正常执行')
- Даже если произойдет ошибка, как указано выше, это не повлияет на выполнение последующего кода и выход приложения, вызванный ошибкой.
Синхронный режим ввода-вывода заставляет выполнение кода ждать результатов ввода-вывода, тратит время ожидания, вычислительная мощность ЦП используется не полностью, а сбой ввода-вывода приводит к завершению всего потока. Схематическая диаграмма блокировки ввода-вывода для всего стека вызовов выглядит следующим образом:
Асинхронный режим ввода/вывода
Это только что представленный асинхронный ввод-вывод. Сначала посмотрим на операции ввода-вывода в асинхронном режиме:
/* TODO: 非阻塞 - 异步 I/O */
const fs = require('fs')
fs.readFile('./file.js',(err,data)=>{
console.log(err,data) // null <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 68 65 6c 6c 6f 2c 77 6f 72 6c 64 27 29>
})
console.log(111) // 111 先被打印~
fs.readFile('./file1.js',(err,data)=>{
console.log(err,data) // 保存 [ no such file or directory, open './file1.js'] ,找不到文件。
})
- Обратный вызов выполняется асинхронно, первым возвращаемым параметром является сообщение об ошибке, если ошибки нет, то возвращается
null
, второй параметрfs.readFile
Реальное содержание исполнения. - Эта асинхронная форма может изящно перехватывать ошибки при выполнении ввода-вывода, например, при чтении
file1.js
Когда документ не может быть обнаружен поведение исключения, он будет передан непосредственно в обратный вызов в виде первого параметра.
Например, приведенный выше обратный вызов в качестве асинхронной функции обратного вызова, как и fn в setTimeout(fn), не будет блокировать выполнение кода. Он будет запущен после получения результата.Для получения подробной информации об асинхронном выполнении обратного вызова ввода-вывода Nodejs мы медленно проанализируем его далее.
Для обработки асинхронного ввода-вывода Nodejs внутри использует пул потоков для обработки задач асинхронного ввода-вывода. В пуле потоков будет несколько потоков ввода-вывода для одновременной обработки асинхронных операций ввода-вывода. Например, в приведенный выше пример, это справедливо для всей модели ввода/вывода.
Далее мы вместе рассмотрим процесс выполнения асинхронного ввода-вывода.
цикл событий
Как и браузер, Nodejs также имеет свою собственную модель выполнения — цикл событий (eventLoop), на модель выполнения цикла событий влияет хост-среда, он не является частью механизма выполнения javascript (например, v8), что приводит к разные хост-среды Нижний режим цикла событий и механизм могут быть разными.Интуитивное проявление заключается в том, что существуют различия в обработке микрозадач (microtask) и макрозадач (macrotask) в Nodejs и браузерных средах. Далее будет подробно рассмотрен цикл событий Nodejs и каждый из его этапов.
Цикл событий Nodejs имеет несколько этапов, один из которых предназначен для обработки обратных вызовов ввода-вывода, каждый этап выполнения мы можем назватьTick
, КаждыйTick
Он проверит, есть ли еще события и связанные с ними функции обратного вызова, такие как функция обратного вызова асинхронного ввода-вывода выше, проверит, завершен ли текущий ввод-вывод на этапе обработки ввода-вывода, если он завершен, затем выполните соответствующую функцию обратного вызова ввода-вывода, затем Наблюдатель, который проверяет, завершен ли ввод-вывод, называется наблюдателем ввода-вывода.
наблюдатель
Как упоминалось выше, упоминается концепция наблюдателей ввода-вывода, а также упоминается, что в Nodejs будет несколько этапов, фактически каждому этапу соответствует один или несколько соответствующих наблюдателей, и их работа четко определена в каждом соответствующем тике. В процессе соответствующий наблюдатель выясняет, есть ли соответствующее событие для выполнения, и если да, то достает его и выполняет.
События браузера возникают в результате взаимодействия с пользователем и некоторых сетевых запросов, таких какajax
Ждать,Nodejs
, событие исходит из сетевого запросаhttp
, файловый ввод-вывод и т. д. У этих событий есть соответствующие наблюдатели. Здесь я перечисляю некоторые важные наблюдатели.
- файловые операции ввода-вывода — наблюдатели за вводом-выводом;
- сетевые операции ввода-вывода — наблюдатели сетевого ввода-вывода;
- process.nextTick — наблюдатель бездействия
- setImmediate -- проверка наблюдателя
- setTimeout/setInterval - наблюдатель задержки
- ...
В Nodejs соответствующий наблюдатель получает событие соответствующего типа.В процессе цикла обработки событий этих наблюдателей спросят, есть ли задача для выполнения.Если есть, наблюдатель возьмет задачу и передаст ее цикл событий для выполнения.
Объект запроса и пул потоков
отJavaScript
Вызывается в компьютерную систему для завершения обратного вызова ввода-вывода,объект запросаИграет очень важную роль, возьмем в качестве примера операцию асинхронного ввода-вывода.
Объект запроса:например, звонить передfs.readFile
, что по существу вызываетlibuv
Приведенный выше метод создает объект запроса. Этот объект запроса сохраняет информацию об этом запросе ввода-вывода, включая тело и функцию обратного вызова этого ввода-вывода. Затем завершается первый этап асинхронного вызова, JavaScript продолжает выполнять логику кода в стеке выполнения, а текущая операция ввода-вывода помещается в пул потоков в виде объекта запроса, ожидающего выполнения. . Цель асинхронного ввода-вывода достигнута.
Пул потоков:Пул потоков Nodejs предоставляется ядром (IOCP) в Windows и предоставляется системой Unix посредствомlibuv
Самореализованный пул потоков используется для выполнения части операций ввода-вывода (операций с системными файлами). Размер пула потоков по умолчанию равен 4. Запросы на несколько операций с файловой системой могут быть заблокированы в одном потоке. Так как же выполняются операции ввода-вывода в пуле потоков? Как упоминалось на предыдущем шаге, асинхронный ввод-вывод поместит объект запроса в пул потоков. Во-первых, он определит, есть ли в текущем пуле потоков доступные потоки. Если поток доступен, операция ввода-вывода запроса объект будет выполнен, и выполнение будет выполнено.Результат возвращается объекту запроса. На этапе обработки ввода-вывода цикла событий наблюдатель ввода-вывода получает завершенный объект ввода-вывода, а затем извлекает функцию обратного вызова и вызов результата для выполнения.Функция обратного вызова ввода / вывода выполняется так, и результат извлекается из параметра функции обратного вызова.
Асинхронный механизм операции ввода / вывода
Выше описан процесс выполнения всего асинхронного ввода-вывода, от триггера асинхронного ввода-вывода до обратного вызова ввода-вывода и выполнения.цикл событий,наблюдатель,объект запроса,Пул потоковФормирует всю модель выполнения асинхронного ввода-вывода.
Используйте диаграмму, чтобы представить взаимосвязь между четырьмя:
Подводя итог описанному выше процессу:
-
Этап 1: каждый разАсинхронный вызов ввода/вывода, сначала установите параметры запроса и обратный вызов функции обратного вызова в нижней части nodejs, чтобы сформироватьобъект запроса.
-
Второй этап: сформированный объект запроса будет помещен вПул потоков, если в пуле потоков есть простаивающие потоки ввода-вывода, эта задача ввода-вывода будет выполнена и будет получен результат.
-
Третий этап:цикл событийсерединаНаблюдатель за вводом/выводом, он найдет объект запроса ввода-вывода, который получил результат от объекта запроса, извлечет результат и функцию обратного вызова, поместит функцию обратного вызова в цикл обработки событий, выполнит обратный вызов и завершит весь асинхронный ввод-вывод. задача.
-
Ибо как воспринимать завершение задач асинхронного ввода/вывода? А как получить выполненные задания? В качестве промежуточного уровня libuv использует разные методы на разных платформах: он реализуется через epoll polling в Unix, через ядро (IOCP) в Windows и через kqueue во FreeBSD.
Три цикла событий
Механизм цикла событий реализуется средой хоста.
Как упоминалось выше, цикл событий не является частью движка JavaScript. Механизм цикла событий реализуется средой хоста, поэтому цикл событий отличается в разных средах хоста. Различные среды хоста относятся к среде браузера или среде nodejs, но в разных операционных системах. Среда хоста nodejs также отличается. Далее используйте изображение, чтобы описать взаимосвязь между циклом событий в Nodejs и движком javascript.
Взяв в качестве эталона цикл событий nodejs под libuv, отношение будет следующим:
Взяв в качестве эталона цикл обработки событий JavaScript под браузером, соотношение выглядит следующим образом:
Цикл событий по сути похож на цикл while, как показано ниже, позвольте мне смоделировать поток выполнения цикла событий с помощью фрагмента кода.
const queue = [ ... ] // queue 里面放着待处理事件
while(true){
//开始循环
//执行 queue 中的任务
//....
if(queue.length ===0){
return // 退出进程
}
}
- После запуска Nodejs это похоже на создание цикла while,
queue
В нем есть события, которые нужно обработать.Во время каждого цикла, если есть еще события, вынуть события и выполнить события.Если есть функция обратного вызова, связанная с событием, выполнить функцию обратного вызова, а затем начать следующий цикл . - Если в теле цикла нет событий, процесс завершится.
Я резюмирую блок-схему следующим образом:
Так как же цикл обработки событий справляется с этими задачами? Мы перечисляем некоторые общие задачи обработки событий в Nodejs:
-
setTimeout
илиsetInterval
Таймер задержки. - Задачи асинхронного ввода-вывода: файловые задачи, сетевые запросы и т. д.
-
setImmediate
Задача. -
process.nextTick
Задача. -
Promise
микрозадачи.
Далее мы поговорим о принципах этих задач и о том, как nodejs справляется с этими задачами.
1 этап цикла событий
Для разных событийных задач они будут выполняться на разных этапах цикла событий. Согласно официальной документации nodejs, при нормальных обстоятельствах цикл событий в nodejs может иметь специальные этапы в зависимости от разных операционных систем, но в целом его можно разделить на следующие 6 этапов (шесть этапов блоков кода):
/*
┌───────────────────────────┐
┌─>│ timers │ -> 定时器,延时器的执行
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ -> i/o
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
*/
-
Первый этап:
timer
, главное, что делает фаза таймера, это выполнениеsetTimeout
илиsetInterval
Зарегистрированная функция обратного вызова. -
вторая стадия:pending callback, большинство задач обратного вызова ввода-вывода выполняются на этапе опроса, но также будут некоторые отложенные функции обратного вызова ввода-вывода, оставшиеся от последнего цикла обработки событий, поэтому на этом этапе необходимо вызвать функцию обратного вызова ввода-вывода, которая была задерживается предыдущим циклом событий. /O Функция обратного вызова.
-
Третий этап:idle prepareФаза, используется только для использования внутреннего модуля nodejs.
-
Четвертый этап:pollНа этапе опроса эта фаза в основном выполняет две функции: во-первых, выполняет функцию обратного вызова асинхронного ввода-вывода на этой фазе, а во-вторых, вычисляет время, в течение которого текущая фаза опроса блокирует последующие фазы.
-
Пятый этап:этап проверки, когда очередь функции обратного вызова фазы опроса пуста, она начинает входить в фазу проверки, в основном выполняя
setImmediate
Перезвоните. -
Этап 6:закрыть этап, выполнить регистрацию
close
Функция обратного вызова события.
Что касается характеристик выполнения каждого этапа и соответствующих событийных задач, я подробно проанализирую их далее. Давайте посмотрим, как шесть этапов отражены в основном исходном коде.
Давайте взглянемlibuv
Ниже приведен исходный код цикла событий nodejs (вunix
а такжеwin
Есть небольшая разница, но на процесс не влияет.Вот пример unix. ):
libuv/src/unix/core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
// 省去之前的流程。
while (r != 0 && loop->stop_flag == 0) {
/* 更新事件循环的时间 */
uv__update_time(loop);
/*第一阶段: timer 阶段执行 */
uv__run_timers(loop);
/*第二阶段: pending 阶段 */
ran_pending = uv__run_pending(loop);
/*第三阶段: idle prepare 阶段 */
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
/* 计算 timeout 时间 */
timeout = uv_backend_timeout(loop);
/* 第四阶段:poll 阶段 */
uv__io_poll(loop, timeout);
/* 第五阶段:check 阶段 */
uv__run_check(loop);
/* 第六阶段: close 阶段 */
uv__run_closing_handles(loop);
/* 判断当前线程还有任务 */
r = uv__loop_alive(loop);
/* 省去之后的流程 */
}
return r;
}
- Мы видим, что шесть этапов выполняются по порядку, и только после выполнения задач предыдущего этапа можно переходить к следующему этапу
- когда
uv__loop_alive
Судя по тому, что в текущем цикле событий нет задач, выходим из потока.
2 очередь задач
В цикле событий есть четыре очереди (Фактическая структура данных не является очередью) выполняется в цикле событий libuv, а две очереди выполняются в nodejs соответственно.очередь обещанийа такжеnextTickочередь.
В NodeJS есть более одной очереди, разные типы событий помещаются в свою собственную очередь. После обработки одного этапа, прежде чем перейти к следующему этапу, цикл событий будет обрабатывать две промежуточные очереди, пока две промежуточные очереди не опустеют.
libuv обрабатывает очередь задач
Каждый этап цикла обработки событий выполняет содержимое соответствующей очереди задач.
-
очередь таймера (PriorityQueue): По сути, структура данныхдвоичная минимальная куча, корневой узел бинарной минимальной кучи получает функцию обратного вызова, соответствующую таймеру на ближайшей временной шкале.
-
Очередь событий ввода-вывода: сохраняет задачи ввода-вывода.
-
Срочная очередь (ImmediateList): Множественное Немедленное, уровень узла сохраняется в структуре данных связанного списка.
-
Закрыть очередь событий обратного вызова: Поместите функцию обратного вызова для закрытия.
промежуточная очередь без libuv
- nextTickQueue: функция обратного вызова для сохранения nextTick. Это специфично для nodejs.
- MicrotasksMicroqueue Promise: функция обратного вызова, которая хранит обещания.
Характеристики выполнения промежуточной очереди:
-
В первую очередь необходимо понимать, что две промежуточные очереди выполняются не в libuv, а в слое nodejs, после того как слой libuv обработает задачи каждого этапа, он будет общаться со слоем узла, а затем две очереди будут приоритетными.
-
Задача nextTick имеет более высокий приоритет, чем обратный вызов Promise в задаче Microtasks. То есть узел сначала очистит задачи в nextTick, а затем задачи в Promise. Чтобы проверить этот вывод, пример вопроса, который печатает результаты, выглядит следующим образом:
/* TODO: 打印顺序 */
setTimeout(()=>{
console.log('setTimeout 执行')
},0)
const p = new Promise((resolve)=>{
console.log('Promise执行')
resolve()
})
p.then(()=>{
console.log('Promise 回调执行')
})
process.nextTick(()=>{
console.log('nextTick 执行')
})
console.log('代码执行完毕')
Каков порядок выполнения в nodejs в приведенном выше блоке кода?
Эффект:
Результат печати: выполнение обещания -> выполнение кода завершено -> выполнение nextTick -> выполнение обратного вызова обещания -> выполнение setTimeout
Объяснение: легко понять, почему это печатается в основном цикле событий кода, Promise执行
а также代码执行完毕
При первом выводе nextTick помещается в очередь nextTick, обратные вызовы Promise помещаются в очередь Microtasks, а setTimeout помещается в кучу таймера. Далее завершается основной цикл и очищается содержимое двух очередей, сначала очищается очередь nextTick.nextTick 执行
печатается, то очередь микрозадач очищается,Promise 回调执行
Он печатается, и, наконец, оценивается наличие задачи таймера в цикле событий, затем открывается новый цикл событий, и задача таймера выполняется первой.setTimeout 执行
печатается. Весь процесс завершен.
- Будь то задача nextTick или задача обещания,Код в двух задачах блокирует упорядоченный ход цикла событий., что приводит к голоданию ввода-вывода, поэтому логику в этих двух задачах необходимо тщательно обрабатывать. Например:
/* TODO: 阻塞 I/O 情况 */
process.nextTick(()=>{
const now = +new Date()
/* 阻塞代码三秒钟 */
while( +new Date() < now + 3000 ){}
})
fs.readFile('./file.js',()=>{
console.log('I/O: file ')
})
setTimeout(() => {
console.log('setTimeout: ')
}, 0);
Эффект:
- 三秒钟, 事件循环中的 timer 任务和 I/O 任务,才被有序执行。 то есть
nextTick
Код в , блокирует упорядоченный ход цикла событий.
3 Блок-схема цикла событий
Затем используйте блок-схему, чтобы представить последовательность выполнения шести этапов цикла обработки событий и логику выполнения двух очередей с приоритетом.
4 ступень таймера -> таймер таймера/интервал таймера задержки
Истекшие таймеры и интервалы: Наблюдатель таймера задержки, используемый для проверки прохожденияsetTimeout
илиsetInterval
Внутренний принцип созданной асинхронной задачи аналогичен асинхронному вводу-выводу, но внутренняя реализация обычной/отложенной задачи не использует пул потоков. пройти черезsetTimeout
илиsetInterval
Объект таймера будет вставлен в двоичную минимальную кучу внутри наблюдателя таймера таймера задержки.Во время каждого цикла обработки событий объект таймера будет извлечен из верхней части двоичной минимальной кучи, чтобы определить, истек ли таймер/интервал. . , затем назовите его dequeue. Затем проверяем первый в текущей очереди, пока не будет просроченного, переходим к следующему этапу.
Как слой libuv обрабатывает таймеры
Во-первых, давайте посмотрим, как слой libuv обрабатывает таймеры.
libuv/src/timer.c
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
/* 找到 loop 中 timer_heap 中的根节点 ( 值最小 ) */
heap_node = heap_min((struct heap*) &loop->timer_heap);
/* */
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
/* 执行时间大于事件循环事件,那么不需要在此次 loop 中执行 */
break;
uv_timer_stop(handle);
uv_timer_again(handle);
handle->timer_cb(handle);
}
}
- Как можно понимать, как время ожидания времени ожидания, что является функцией таймера обратно в момент выполнения.
- Когда время ожидания больше времени начала текущего контура события, то есть функция обратного вызова не должна выполняться, когда время не выполняется. Затем в соответствии с природой двух вилкой минимальной кучи родительского узла всегда меньше, чем у узела ребенка, то узел временного узла корневого узла не удовлетворяет времени, другой таймер не удовлетворяет времени выполнения. В это время выполняется выхождение функции обратного вызова на этапе таймера, непосредственно введите следующую фазу цикла событий.
- Когда время истечения меньше, чем время начала текущего тика цикла событий, что указывает на наличие хотя бы одного таймера с истекшим сроком действия, цикл повторяет корневой узел минимальной кучи таймера и вызывает функцию обратного вызова, соответствующую таймеру. Таймер, у которого корневой узел минимальной кучи является самым последним узлом, обновляется на каждой итерации цикла.
Выше приведена функция выполнения фазы таймера в libuv. Далее давайте проанализируем, как таймер задержки обрабатывается в node.
Как уровень узла обрабатывает таймеры
в НодейсеsetTimeout
а такжеsetInterval
Он реализован самим nodejs, давайте посмотрим на детали реализации:
node/lib/timers.js
function setTimeout(callback,after){
//...
/* 判断参数逻辑 */
//..
/* 创建一个 timer 观察者 */
const timeout = new Timeout(callback, after, args, false, true);
/* 将 timer 观察者插入到 timer 堆中 */
insert(timeout, timeout._idleTimeout);
return timeout;
}
- setTimeout: Логика очень проста, просто создайте наблюдатель времени таймера и поместите его в кучу таймера.
Так что же делает тайм-аут?
node/lib/internal/timers.js
function Timeout(callback, after, args, isRepeat, isRefed) {
after *= 1
if (!(after >= 1 && after <= 2 ** 31 - 1)) {
after = 1 // 如果延时器 timeout 为 0 ,或者是大于 2 ** 31 - 1 ,那么设置成 1
}
this._idleTimeout = after; // 延时时间
this._idlePrev = this;
this._idleNext = this;
this._idleStart = null;
this._onTimeout = null;
this._onTimeout = callback; // 回调函数
this._timerArgs = args;
this._repeat = isRepeat ? after : null;
this._destroyed = false;
initAsyncResource(this, 'Timeout');
}
- В nodejs как setTimeout, так и setInterval по сути являются классами Timeout. Превышение максимального времени клапана
2 ** 31 - 1
илиsetTimeout(callback, 0)
, для _idleTimeout будет установлено значение 1, преобразованное в setTimeout(callback, 1) для выполнения.
блок-схема обработки таймера
Чтобы описать это с помощью блок-схемы, мы создаем таймер, а затем переходим к процессу, который таймер выполняет в цикле событий.
функция таймера
Здесь следует отметить две вещи:
- исполнительный механизм: наблюдатель таймера задержки, один будет выполняться каждый раз, nextTick и Promise будут очищены после выполнения одного, время истечения является важным фактором в определении того, выполняются ли два, а опрос будет вычислять время для блокировки выполнения таймер.Выполнение стадийных задач также оказывает существенное влияние.
Чтобы проверить вывод, выполняйте по одной задаче таймера за раз, сначала посмотрите на фрагмент кода:
setTimeout(()=>{
console.log('setTimeout1:')
process.nextTick(()=>{
console.log('nextTick')
})
},0)
setTimeout(()=>{
console.log('setTimeout2:')
},0)
распечатать результат:
TextTick Queue В конце каждого этапа выполнения контура события два порога задержки составляет 0, если выполняется, одноразовая задача в таймере срок действия истечения срока действия истечения срока действия, затем печатать SettimeOut1 -> SettimeOut2 -> NextTick, фактический первый таймер на задаче Выполнить, а затем выполнить задачу NextTick и, наконец, таймер к следующей задаче.
-
Проблема точности: Что касается проблемы со счетчиком setTimeout, то таймер не точен, хотя цикл событий в nodejs очень быстрый, но от создания класса тайм-аута таймера задержки он будет занимать некоторые события, а затем до выполнения контекста, я/ Выполнение O, выполнение nextTick Queue, выполнение микрозадач заблокирует выполнение задержки. Даже при проверке истечения срока действия таймера он потребляет некоторое время процессора.
-
проблемы с производительностью: Если вы хотите использовать setTimeout(fn,0) для выполнения некоторых задач, которые не вызываются немедленно, тогда производительность не так хороша, как
process.nextTick
На самом деле, во-первых, setTimeout недостаточно точен.Другой момент в том, что в нем есть объект-таймер, и его нужно выполнять в нижней части libuv, что отнимает определенное количество производительности, поэтому его можно использовалprocess.nextTick
решить этот сценарий.
5 незавершенный этап
Фаза ожидания используется для обработки функции обратного вызова ввода-вывода, отложенной перед этим циклом событий. Сначала посмотрите на время выполнения в libuv.
libuv/src/unix/core.c
static int uv__run_pending(uv_loop_t* loop) {
QUEUE* q;
QUEUE pq;
uv__io_t* w
/* pending_queue 为空,清空队列 ,返回 0 */
if (QUEUE_EMPTY(&loop->pending_queue))
return 0;
QUEUE_MOVE(&loop->pending_queue, &pq);
while (!QUEUE_EMPTY(&pq)) { /* pending_queue 不为空的情况,清空 I/O 回调。返回 1 */
q = QUEUE_HEAD(&pq);
QUEUE_REMOVE(q);
QUEUE_INIT(q);
w = QUEUE_DATA(q, uv__io_t, pending_queue);
w->cb(loop, w, POLLOUT);
}
return 1;
}
- Если задача, удерживающая обратный вызов ввода/вывода,
pending_queue
пусто, то верните 0 напрямую. - Если pending_queue имеет задачу обратного вызова ввода-вывода, выполните задачу обратного вызова.
6 простаивает, стадия подготовки
idle
Выполните некоторые внутренние операции libuv,prepare
Подготовьтесь к следующему опросу ввода/вывода. Далее важнее проанализироватьpoll
сцена.
7 опрос Этап опроса ввода/вывода
Прежде чем формально объяснить, что делает фаза опроса, давайте сначала посмотрим на логику выполнения фазы опроса в libuv:
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
/* 计算 timeout */
timeout = uv_backend_timeout(loop);
/* 进入 I/O 轮询 */
uv__io_poll(loop, timeout);
- Тайм-аут инициализации timeout = 0 через
uv_backend_timeout
Рассчитать это времяpoll
Тайм-аут этапа. Тайм-аут влияет на выполнение асинхронного ввода-вывода и последующих циклов событий.
что означает тайм-аут
Прежде всего, мы должны понять, что означают различные тайм-ауты при опросе ввода-вывода.
- когда
timeout = 0
Когда это означает, что фаза опроса не будет блокировать выполнение цикла событий, это означает, что есть более срочные задачи для выполнения. Тогда текущая стадия опроса не будет блокироваться, она перейдет на следующую стадию как можно быстрее, завершит текущий тик как можно быстрее и войдет в следующий цикл событий, затем этиСрочныйЗадача будет выполнена. - когда
timeout = -1
Когда это означает, что цикл обработки событий будет заблокирован все время, вы можете остаться на этапе опроса асинхронного ввода-вывода и дождаться завершения новой задачи ввода-вывода. - когда
timeout
Если он равен константе, то это означает время, которое в это время может находиться цикл опроса io, поэтому, когда будет таймаут как константа, об этом будет объявлено в ближайшее время.
получить тайм-аут
Таймаут получается через uv_backend_timeout Так как его получить?
int uv_backend_timeout(const uv_loop_t* loop) {
/* 当前事件循环任务停止 ,不阻塞 */
if (loop->stop_flag != 0)
return 0;
/* 当前事件循环 loop 不活跃的时候 ,不阻塞 */
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
/* 当 idle 句柄队列不为空时,返回 0,即不阻塞。 */
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
/* i/o pending 队列不为空的时候。 */
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
/* 有关闭回调 */
if (loop->closing_handles)
return 0;
/* 计算有没有延时最小的延时器 | 定时器 */
return uv__next_timeout(loop);
}
Главное, что делает uv_backend_timeout:
- Не блокировать, когда текущий цикл событий остановлен.
- Когда текущий цикл цикла событий не активен, он не блокируется.
- Когда очередь бездействия ( setImmediate ) не пуста, вернуть 0 и не блокировать.
- Когда очередь ожидания ввода-вывода не пуста, она не блокируется.
- Когда есть функция обратного вызова close, она не блокируется.
- Если ничего из вышеперечисленного не удовлетворяет, то проходите
uv__next_timeout
Вычислить, есть ли таймер с наименьшим порогом задержки | delayer (наиболее срочное выполнение), и вернуть время задержки.
следующий взглядuv__next_timeout
логика.
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
/* 找到延时时间最小的 timer */
heap_node = heap_min((const struct heap*) &loop->timer_heap);
if (heap_node == NULL) /* 如何没有 timer,那么返回 -1 ,一直进入 poll 状态 */
return -1;
handle = container_of(heap_node, uv_timer_t, heap_node);
/* 有过期的 timer 任务,那么返回 0,poll 阶段不阻塞 */
if (handle->timeout <= loop->time)
return 0;
/* 返回当前最小阀值的 timer 与 当前事件循环的事件相减,得出来的时间,可以证明 poll 可以停留多长时间 */
diff = handle->timeout - loop->time;
return (int) diff;
}
uv__next_timeout
Он делает следующее:
- Найдите таймер с наименьшим порогом времени (тот, что с наивысшим приоритетом), если таймера нет, то верните -1. Этап опроса будетНеограниченная блокировка. Преимущество этого заключается в том, что после выполнения ввода-вывода функция обратного вызова ввода-вывода будет напрямую добавлена к poll, а затем будет выполнена соответствующая функция обратного вызова.
- Если есть таймер, но
timeout <= loop.time
Если срок действия доказательства истек, то возвращается 0, этап опроса не блокируется, а просроченные задачи выполняются первыми. - Если он не истекает, таймер, который возвращает текущий минимальный порог, вычитается из событий текущего цикла событий, что может показать, как долго может оставаться опрос. Когда пребывание завершено, доказывается, что есть истекший таймер, затем переходим к следующему тику.
выполнить io_poll
Далееuv__io_poll
Настоящее исполнение, имеющееepoll_wait
Метод в соответствии с тайм-аутом опрашивает, завершен ли ввод-вывод, и если да, то выполняет обратный вызов ввода-вывода. Это также важная часть реализации асинхронного ввода-вывода в Unix.
Суть этапа опроса
Далее резюмируем суть фазы опроса:
- Стадия опроса оценивается по таймауту, будь то блокировка контура событий. Опрос также своего рода опрос, опрос - это задачи ввода / вывода, события, имеющие тенденцию к постоянному этапу цикла опроса, и его цель - выполнять более быстрые задачи ввода-вывода. Если ни одна другая задача, то этап был в опросе.
- Если есть более срочные задачи, которые необходимо выполнить на других этапах, например, таймер, закрытие, то этап опроса не будет блокироваться и перейдет к следующему этапу тика.
Схема этапа опроса
Я представил всю фазу опроса в виде блок-схемы, опуская некоторые детали.
8 этап проверки
Если фаза опроса переходит в состояние ожидания, а функция setImmediate имеет функцию обратного вызова, то фаза опроса нарушит неограниченное состояние ожидания и перейдет в фазу проверки для выполнения функции обратного вызова фазы проверки.
Все, что делает проверка, это обрабатывает обратный вызов setImmediate., давайте посмотрим, как это определяется в NodejssetImmediate
.
setImmediate в нижнем слое Nodejs
setImmediateDefinition
node/lib/timer.js
function setImmediate(callback, arg1, arg2, arg3) {
validateCallback(callback); /* 校验一下回调函数 */
/* 创建一个 Immediate 类 */
return new Immediate(callback, args);
}
- при звонке
setImmediate
По сути, вызовите метод setImmediate в nodejs, сначала проверьте функцию обратного вызова, а затем создайтеImmediate
Добрый. Затем взгляните на класс Immediate.
node/lib/internal/timers.js
class Immediate{
constructor(callback, args) {
this._idleNext = null;
this._idlePrev = null; /* 初始化参数 */
this._onImmediate = callback;
this._argv = args;
this._destroyed = false;
this[kRefed] = false;
initAsyncResource(this, 'Immediate');
this.ref();
immediateInfo[kCount]++;
immediateQueue.append(this); /* 添加 */
}
}
- Класс Immediate инициализирует некоторые параметры, а затем вставляет текущий класс Immediate в
immediateQueue
в связанном списке. - ImmediateQueue — это, по сути, связанный список, в котором хранится каждое Immediate.
setImmediate выполняет
После этапа опроса он сразу перейдет к этапу проверки и выполнит Immediate в немедленной очереди. В каждом цикле событий сначала выполняется обратный вызов setImmediate, а затем содержимое очередей nextTick и Promise очищается. Чтобы проверить этот вывод, также как setTimeout, взгляните на следующий блок кода:
setImmediate(()=>{
console.log('setImmediate1')
process.nextTick(()=>{
console.log('nextTick')
})
})
setImmediate(()=>{
console.log('setImmediate2')
})
Выведите setImmediate1 -> nextTick -> setImmediate2 , в каждом цикле событий выполните setImmediate , затем выполните, чтобы очистить очередь nextTick, и выполните еще один setImmediate2 в следующем цикле событий.
блок-схема выполнения setImmediate
setTimeout & setImmediate
Далее сравнитьsetTimeoutа такжеsetImmediate, если разработчик ожидает отложенные асинхронные задачи, то сравнитеsetTimeout(fn,0)
а такжеsetImmediate(fn)
разница.
- setTimeout используется для выполнения функции обратного вызова в пределах минимальной ошибки установленного порога.Существует проблема точности с setTimeout.Создание этапов setTimeout и опроса может повлиять на выполнение функции обратного вызова setTimeout.
- После этапа опроса setImmediate немедленно переходит к этапу проверки и выполняет
setImmediate
Перезвоните.
Если setTimeout и setImmediate идут вместе, кто выполняет первым?
Сначала напишите демо:
setTimeout(()=>{
console.log('setTimeout')
},0)
setImmediate(()=>{
console.log( 'setImmediate' )
})
догадка
Первое предположение, setTimeout происходитtimer
этапе setImmediate происходит вcheck
фаза, фаза таймера предшествует фазе проверки, тогда печать setTimeout имеет приоритет над печатью setImmediate. Но так ли это?
фактический результат печати
Из приведенных выше результатов печатиsetTimeout
а такжеsetImmediate
Время выполнения не определено, почему так происходит?Как упоминалось выше, даже если второй параметр setTimeout равен 0, он будет обработан в nodejssetTimeout(fn,1)
. Когда код синхронизации основного процесса будет выполнен, он войдет в стадию цикла событий и впервые войдет в таймер, в это время порог времени таймера, соответствующий settimeout, равен 1. Если в предыдущем uv__run_timer(loop ), вызов системного времени и сравнение времени. Если общее время процесса не превышает 1 мс, то на этапе таймера будет найден неистекший таймер, тогда текущий таймер не будет выполняться, а затем будет выполнен обратный вызов setImmediate. выполняться на этапе проверки.Последовательность выполнения в это время такова:setImmediate -> setTimeout.
Однако, если общее затраченное время превысит одну миллисекунду, порядок выполнения изменится: на этапе таймера просроченная задача setTimeout вынимается и выполняется, а затем на этапе проверки снова выполняется setImmediate.setTimeout -> setImmediate.
Причина этого в том, что интервал между проверкой времени таймера и текущим тиком цикла событий может быть меньше 1 мс или больше порога 1 мс, поэтому он определяет, выполняется ли setTimeout в первом цикле событий или нет.
Далее, когда я блокирую код, существует высокая вероятность того, что setTimeout всегда будет выполняться до setImmediate.
/* TODO: setTimeout & setImmediate */
setImmediate(()=>{
console.log( 'setImmediate' )
})
setTimeout(()=>{
console.log('setTimeout')
},0)
/* 用 100000 循环阻塞代码,促使 setTimeout 过期 */
for(let i=0;i<100000;i++){
}
Эффект:
100000
Зациклить блокирующий код, который заставит setTimeout выполняться за порогом времени, что гарантирует, что каждый раз, когда он выполняется первымsetTimeout -> setImmediate.
Частный случай: определить последовательную согласованность. Давайте рассмотрим частный случай.
const fs = require('fs')
fs.readFile('./file.js',()=>{
setImmediate(()=>{
console.log( 'setImmediate' )
})
setTimeout(()=>{
console.log('setTimeout')
},0)
})
Поскольку описанная выше ситуация приведет к тому, что setImmediate всегда имеет приоритет над setTimeout, почему, давайте проанализируем причины вместе.
- Сначала проанализируйте асинхронную задачу — в основном процессе есть задача асинхронного ввода-вывода, а в обратном вызове ввода-вывода — setimmediate и settimeout.
- существует
poll
Этапы выполняют обратные вызовы ввода-вывода. Затем обработайте setImmediate
Пока вы овладеете характеристиками вышеперечисленных этапов, вы сможете четко различать реализацию разных ситуаций.
9 закрытый этап
Фаза закрытия используется для выполнения некоторых функций обратного вызова закрытия. Выполнить все события закрытия. Следующий взгляд на близкое событиеlibuv
реализация.
libuv/src/unix/core.c
static void uv__run_closing_handles(uv_loop_t* loop) {
uv_handle_t* p;
uv_handle_t* q;
p = loop->closing_handles;
loop->closing_handles = NULL;
while (p) {
q = p->next_closing;
uv__finish_close(p);
p = q;
}
}
-
uv__run_closing_handles
Этот метод перебирает функции обратного вызова в очереди закрытия.
10 Обзор цикла событий Nodejs
Затем суммируйте цикл событий Nodejs.
-
Цикл событий Nodejs разделен на 6 основных этапов. Это этап таймера, этап ожидания, этап подготовки, этап опроса, этап проверки и этап закрытия.
-
Характеристики выполнения очереди nextTick и очереди микрозадач выполняются после завершения каждого этапа, а приоритет nextTick выше, чем у микрозадач (Promise).
-
Фаза опроса в основном связана с вводом-выводом, если нет других задач, она будет в фазе блокировки опроса.
-
Фаза таймера в основном имеет дело с таймерами/задержками, которые неточны и требуют дополнительных потерь производительности для создания, и на их выполнение также влияет фаза опроса.
-
Ожидающая фаза обрабатывает задачи обратного вызова ввода-вывода с истекшим сроком действия.
-
Этап проверки обрабатывает setImmediate. Время и разница между setImmediate и setTimeout.
Пошаговое руководство по четырем упражнениям цикла событий Nodejs
Далее, чтобы сделать процесс цикла событий более понятным, вот две проблемы с циклом событий. Как практика:
Упражнение 1
process.nextTick(function(){
console.log('1');
});
process.nextTick(function(){
console.log('2');
setImmediate(function(){
console.log('3');
});
process.nextTick(function(){
console.log('4');
});
});
setImmediate(function(){
console.log('5');
process.nextTick(function(){
console.log('6');
});
setImmediate(function(){
console.log('7');
});
});
setTimeout(e=>{
console.log(8);
new Promise((resolve,reject)=>{
console.log(8+'promise');
resolve();
}).then(e=>{
console.log(8+'promise+then');
})
},0)
setTimeout(e=>{ console.log(9); },0)
setImmediate(function(){
console.log('10');
process.nextTick(function(){
console.log('11');
});
process.nextTick(function(){
console.log('12');
});
setImmediate(function(){
console.log('13');
});
});
console.log('14');
new Promise((resolve,reject)=>{
console.log(15);
resolve();
}).then(e=>{
console.log(16);
})
Если вы просто посмотрите это демо, вы можете запутаться, но весь цикл событий упоминается выше, и очень легко посмотреть на эту проблему еще раз.Давайте проанализируем общий процесс:
- Первый этап: Сначала запускаем js файл, затем входим в первый цикл событий, тогда сначала будет выполняться задача синхронизации:
Первая печать:
распечатать console.log('14');
напечатать console.log(15);
очередь nextTick:
nextTick -> console.log(1) nextTick -> console.log(2) -> setImmediate(3) -> nextTick(4)
Обещанная очередь
Promise.then(16)
проверить очередь
setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13)
очередь таймера
setTimeout(8) -> promise(8+'promise') -> promise.then(8+'promise+then') setTimeout(9)
- Второй этап: перед входом в новое событие цикла, NextTick пустые очеретые очеренные очеренные очеренные очеренные очеренные очеренные очереди, порядок очереди больше, чем обещание NextTick Queue.
Пустой следующий тик,Распечатать:
console.log('1');
console.log('2');
Когда выполняется второй nextTick, есть еще один nextTick, поэтому этот nextTick также будет добавлен в очередь. Выполнить немедленно.
console.log('4')
Следующие пустые микрозадачи
console.log(16);
В настоящее время в очередь проверки добавлен новый setImmediate.
проверить очередь setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13) setImmediate(3)
- Затем введите новый цикл событий и сначала выполните задачи в таймере. Выполните первый setTimeout.
Запустите первый таймер:
console.log(8);
В этот момент находится Обещание. В обычном контексте выполнения:
console.log(8+'promise');
Затем добавьте Promise.then в очередь nextTick. Очередь nextTick будет немедленно очищена.
console.log(8+'promise+then');
Запустите второй таймер:
console.log(9)
- Далее, на этапе проверки, выполните содержимое очереди проверки:
Выполните первую проверку:
console.log(5);
В этот момент найден nextTick, а затем есть setImmediate, который добавляет setImmediate в очередь проверки. Затем выполните nextTick .
console.log(6)
выполнить вторую проверку
console.log(10)
На данный момент найдены два nextTick и один setImmediate. Затем очистите очередь nextTick. Добавьте setImmediate в очередь.
console.log(11)
console.log(12)
Очередь проверки в это время выглядит следующим образом:
setImmediate(3) setImmediate(7) setImmediate(13)
Далее очередь чеков очищается по порядку. Распечатать
console.log(3)
console.log(7)
console.log(13)
На данный момент выполняется весь цикл обработки событий. Тогда общее содержание печати выглядит следующим образом:
5 Резюме
Основное содержание этой статьи следующее:
- Введение в асинхронный ввод-вывод и его внутренности.
- Цикл событий Nodejs, шесть этапов.
- Принципы и различия setTimeout, setImmediate, асинхронного ввода/вывода, nextTick и Promise в Nodejs.
- Практика цикла событий Nodejs.
использованная литература
- Просмотр цикла событий nodejs из libuv
- Подробное объяснение Nodejs
- Рабочий процесс и жизненный цикл цикла событий Node.js
«Расширенное практическое руководство по React»
Содержание буклета будет постоянно обновляться и поддерживаться по мере обновления версии React, а главы будут постоянно обновляться~
В раскрытый заранее буклет будут добавлены:React context
Основная часть, содержание дополняется главой 8.
Скидка 30% на несколько брошюр с кодом купонаF3Z1VXtv, первый пришел первый обслужен ~