Углубленный анализ цикла событий и очереди сообщений Node.js

Node.js внешний интерфейс JavaScript C++ V8

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

Чтение SSD происходит быстро, но не на порядок по сравнению со скоростью обработки инструкций ЦП, а время прохождения пакета в сеть и из сети медленнее:

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

Следовательно, асинхронный ввод-вывод будет играть роль. Например, при использовании многопоточности, если вы используете Java для чтения файла, это блокирующая операция. Вы ничего не можете сделать, ожидая возврата данных, поэтому новый поток открывается для его обработки.Файл читается, и основной поток уведомляется после завершения операции чтения.

Хотя это работает, код писать сложнее. Как насчет чего-то вроде Node.js V8, который не может открыть поток?

1. Что такое процесс Node.js

Сначала мы можем молча ответить на следующие 9 вопросов, понятно?

1.1 Асинхронный ввод-вывод

Асинхронный ввод-выводОтносится к возможностям ввода-вывода (ввод и вывод данных), предоставляемым операционной системой, таким как ввод с клавиатуры, будет специальный интерфейс вывода данных, соответствующий дисплею, который представляет собой возможность ввода-вывода, видимую в нашей жизни; этот интерфейс войдет в операционная система вниз.На этом уровне в операционной системе он будет предоставлять множество возможностей, таких как: чтение и запись диска, DNS-запрос, подключение к базе данных, обработка сетевых запросов и т. д.;

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

koaЭто структура веб-сервисов верхнего уровня, полностью реализованная с помощью js, и она взаимодействует между операционными системами на всем протяжении.nodejsдобиться; какnodejsизreadFileЭто асинхронный неблокирующий интерфейс.readFileSyncЭто синхронный блокирующий интерфейс, здесь в основном даны ответы на три вышеуказанных вопроса;

1.2 Цикл событий

цикл событийОтносится кNode.jsВыполнение неблокирующих операций ввода-вывода.Хотя JavaScript является однопоточным, поскольку большинство ядер являются многопоточными, node.js максимально загружает операции в системное ядро. Таким образом, они могут обрабатывать несколько операций, выполняемых в фоновом режиме. Когда одна из этих операций завершается, ядро ​​сообщает Node.js, чтобы node.js мог добавить соответствующий обратный вызов в очередь опроса для возможного выполнения.

1.3 Резюме

nodejs этовыполнение одного потока, и он основан на事件驱动из非阻塞IOмодель программирования. Это позволяет нам продолжить выполнение кода, не дожидаясь возврата результата асинхронной операции. Когда запускается асинхронное событие, основной поток уведомляется, и основной поток выполняет обратный вызов соответствующего события.

2. Анализ архитектуры Nodejs

Говоря об архитектуре Nodejs, давайте сначала перейдем к взаимосвязи и роли Nodejs с V8 и libUV:

  • V8:Движок, который исполняет JS.То есть транслирует JS.Включая знакомую нам оптимизацию компиляции,сборку мусора и т.д.
  • libUV:поставкаasync I/O, обеспечивает цикл обработки сообщений.Как видно, это уровень абстракции уровня API операционной системы.

Так как же Nodejs их организует?

2.1 Application Code(JS)

Код фреймворка и пользовательский код — это код приложения, который мы пишем, пакет npm, модуль js, встроенный в nodejs, и т. д. Большая часть нашей повседневной работы — это написание кода на этом уровне.

2.2 обязательный код

связующий код или сторонний плагин (код js или C/C++).

Возможность заставить JS вызывать C/C++ код. Его можно понимать как бридж, бридж — это JS, бридж — это C/C++, и JS может вызывать C/C++ через этот бридж. В NodeJS основная роль связующих кодов заключается в предоставлении библиотеки C/C++, реализованной NodeJS, в среду JS. Тройной плагин — это библиотека C/C++, которую мы реализовали, и нам нужен собственный связующий код для соединения JS и C/C++.

Nodejs передает JS в V8 через уровень привязки C++.После синтаксического анализа V8 он передается libUV для инициации ввода-вывода asnyc и ожидает планирования цикла обработки сообщений.

2.3 Низкоуровневая библиотека

Зависимые библиотеки nodejs, в том числе знаменитые V8 и libuv.

  • V8: Мы все знаем, что это набор эффективной среды выполнения javascript, разработанной Google, и основная причина, по которой nodejs может эффективно выполнять код js, в основном связана с ним.
  • libuv: это набор библиотек асинхронных функций, реализованных на языке C. Эффективная модель асинхронного программирования nodejs во многом связана с реализацией libuv, и libuv находится в центре нашего сегодняшнего анализа.

Есть также некоторые другие зависимые библиотеки

  • http-parser: отвечает за разбор HTTP-ответов.
  • openssl: шифрование и дешифрование
  • c-ares: разрешение DNS
  • npm: менеджер пакетов nodejs

3. либувская архитектура

Мы знаем, что ядром асинхронного механизма nodejs является libuv. libuv отвечает за коммуникационный мост между nodejs и асинхронными задачами, такими как файлы и сети. Следующая картинка дает нам общее представление о libuv:

Это изображение официального сайта libuv.Очевидно, что сетевой ввод-вывод nodejs, файловый ввод-вывод, операции DNS и некоторый пользовательский код работают в libuv. Поскольку мы говорили об асинхронности, давайте сначала суммируем асинхронные события в nodejs:

3.1 Операции без ввода-вывода:

  • таймер (setTimeout, setInterval)
  • микрозадача (обещание)
  • process.nextTick
  • setImmediate
  • DNS.lookup

3.2 Операции ввода/вывода:

  • Сетевой ввод-вывод

Для сетевого ввода-вывода механизм реализации каждой платформы отличается: Linux — это модель epoll, Unix-подобная — kquene, Windows — эффективный порт завершения IOCP, SunOs — порт событий, а libuv выполняет эти сетевые операции ввода-вывода. О модели пакет.

  • Файловый ввод-вывод и операции DNS

libuv также поддерживает пул из 4 потоков по умолчанию, эти потоки отвечают за выполнение файловых операций ввода-вывода, операций DNS и пользовательского асинхронного кода. Когда уровень js передает операционную задачу в libuv, libuv добавит задачу в очередь. Тогда есть два случая:

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

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

Конечно, если вы чувствуете, что 4 потоков недостаточно, вы можете установить переменную среды UV_THREADPOOL_SIZE для настройки при запуске nodejs.Из соображений производительности системы libuv предусматривает, что количество потоков, которые можно установить, не может превышать 128.

4 модель потоков Nodejs

Процесс запуска node.js можно разделить на следующие этапы:

1. Вызовите метод platformInit, чтобы инициализировать рабочую среду nodejs.

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

3. Мнение о настройках openssl.

4. Вызовите v8_platform.Initialize для инициализации пула потоков libuv.

5. Вызовите V8::Initialize, чтобы инициализировать среду V8.

6. Создайте работающий экземпляр nodejs.

7. Запустите экземпляр, созданный на предыдущем шаге.

8. Запустить выполнение js-файла.После выполнения кода синхронизации войти в цикл обработки событий.

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

Выше приведен весь процесс выполнения nodejs файла js. Далее мы сосредоточимся на восьмом шаге, цикле событий.

Nodejs полностью однопоточный.После запуска процесса основной поток загружает наш js-файл (main.js на рисунке ниже), а затем входит в цикл обработки сообщений.Видно, что js-программа выполняется полностью в одном потоке .

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

4.1 Разработка цикла сообщений

Давайте снова посмотрим на часть цикла сообщений в JS:

  • timers: Выполнение обратных вызовов, срок действия которых истекает в setTimeout() и setInterval().
  • I/O callbacks: Небольшое количество обратных вызовов ввода-вывода в предыдущем раунде циклов будет отложено до этой стадии этого раунда.
  • idle, prepare: Только для внутреннего пользования
  • poll: самый важный этап, выполнение обратного вызова ввода-вывода, будет заблокирован на этом этапе при соответствующих условиях.
  • check: выполнить обратный вызов setImmediate
  • close callbacks: выполнить обратный вызов события закрытия, например socket.on("close",func)

Nodejs подразделяет цикл обработки сообщений на 6 этапов (официально называемых этапами), каждый этап будет иметь структуру, аналогичную очереди, в которой хранятся функции обратного вызова, которые необходимо обработать на этом этапе.Давайте рассмотрим роль этих 6 этапов. , Основной код этих шести этапов выглядит следующим образом:

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;
//判断事件循环是否存活。
  r = uv__loop_alive(loop);
  //如果没有存活,更新时间戳
  if (!r)
    uv__update_time(loop);
//如果事件循环存活,并且事件循环没有停止。
  while (r != 0 && loop->stop_flag == 0) {
    //更新当前时间戳
    uv__update_time(loop);
    //执行 timers 队列
    uv__run_timers(loop);
    //执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。
    ran_pending = uv__run_pending(loop); 
    //内部调用,用户不care,忽略
    uv__run_idle(loop); 
    //内部调用,用户不care,忽略
    uv__run_prepare(loop); 
    
    timeout = 0; 
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
    //计算距离下一个timer到来的时间差。
      timeout = uv_backend_timeout(loop);
   //进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。
    uv__io_poll(loop, timeout);
    //进入check阶段,主要执行 setImmediate 回调。
    uv__run_check(loop);
    //进行close阶段,主要执行 **关闭** 事件
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      
      //更新当前时间戳
      uv__update_time(loop);
      //再次执行timers回调。
      uv__run_timers(loop);
    }
    //判断当前事件循环是否存活。
    r = uv__loop_alive(loop); 
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

4.2 Timer Phase

Это первая фаза цикла сообщений, использующаяforперебрать всеsetTimeoutа такжеsetIntervalОбратный вызов.Основной код выглядит следующим образом:

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
  //取出定时器堆中超时时间最近的定时器句柄
    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)
      break;
    // 停止最近的定时器句柄
    uv_timer_stop(handle);
    // 判断定时器句柄类型是否是repeat类型,如果是,重新创建一个定时器句柄。
    uv_timer_again(handle);
    //执行定时器句柄绑定的回调函数
    handle->timer_cb(handle);
  }
}

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

Также очень просто определить, соответствует ли обратный вызов на условие. Структура сообщений сохранит системное время в то время каждый раз, когда вы вводите таймеру PHA, а затем вы можете увидеть, набор времени запуска в наименее куче, больше, чем Ввод времени, сохраненного при таймере PHA, если вы превысите его.

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

4.3 Pending I/O Callback Phase

Этот этап заключается в выполнении вашегоfs.read, socketФункции обратного вызова, такие как операции ввода-вывода, а также обратные вызовы для различных ошибок.

4.4 Idle, Prepare Phase

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

4.5 Poll Phase

Это самая важная фаза во всем цикле обработки сообщений, которая используется для ожидания асинхронных запросов и данных (оригинал:accepts new incoming connections (new socket establishment etc) and data (file read etc)). Говорят, что он самый важный, потому что поддерживает весь механизм цикла сообщений.

Этап опроса будет выполняться первымwatch_queueДля запросов ввода-вывода в очереди, как только очередь watch_queue станет пустой, весь цикл обработки сообщений перейдет в спящий режим, ожидая пробуждения событиями ядра.Исходный код находится здесь:

void uv__io_poll(uv_loop_t* loop, int timeout) {
  /*一连串的变量初始化*/
  //判断是否有事件发生    
  if (loop->nfds == 0) {
    //判断观察者队列是否为空,如果为空,则返回
    assert(QUEUE_EMPTY(&loop->watcher_queue));
    return;
  }
  
  nevents = 0;
  // 观察者队列不为空
  while (!QUEUE_EMPTY(&loop->watcher_queue)) {
    /*
    取出队列头的观察者对象
    取出观察者对象感兴趣的事件并监听。
    */
    ....省略一些代码
    w->events = w->pevents;
  }

  
  assert(timeout >= -1);
  //如果有超时时间,将当前时间赋给base变量
  base = loop->time;
  // 本轮执行监听事件的最大数量
  count = 48; /* Benchmarks suggest this gives the best throughput. */
  //进入监听循环
  for (;; nevents = 0) {
  // 有超时时间的话,初始化spec
    if (timeout != -1) {
      spec.tv_sec = timeout / 1000;
      spec.tv_nsec = (timeout % 1000) * 1000000;
    }
    
    if (pset != NULL)
      pthread_sigmask(SIG_BLOCK, pset, NULL);
    // 监听内核事件,当有事件到来时,即返回事件的数量。
    // timeout 为监听的超时时间,超时时间一到即返回。
    // 我们知道,timeout是传进来得下一个timers到来的时间差,所以,在timeout时间内,event-loop会一直阻塞在此处,直到超时时间到来或者有内核事件触发。
    nfds = kevent(loop->backend_fd,
                  events,
                  nevents,
                  events,
                  ARRAY_SIZE(events),
                  timeout == -1 ? NULL : &spec);

    if (pset != NULL)
      pthread_sigmask(SIG_UNBLOCK, pset, NULL);

    /* Update loop->time unconditionally. It's tempting to skip the update when
     * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the
     * operating system didn't reschedule our process while in the syscall.
     */
    SAVE_ERRNO(uv__update_time(loop));
    //如果内核没有监听到可用事件,且本次监听有超时时间,则返回。
    if (nfds == 0) {
      assert(timeout != -1);
      return;
    }
    
    if (nfds == -1) {
      if (errno != EINTR)
        abort();

      if (timeout == 0)
        return;

      if (timeout == -1)
        continue;

      /* Interrupted by a signal. Update timeout and poll again. */
      goto update_timeout;
    }

    。。。
    //判断事件循环的观察者队列是否为空
    assert(loop->watchers != NULL);
    loop->watchers[loop->nwatchers] = (void*) events;
    loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
    // 循环处理内核返回的事件,执行事件绑定的回调函数
    for (i = 0; i < nfds; i++) {
        。。。。
    }
    
}

uv__io_pollИсходный код этапа самый длинный, а логика самая сложная Его можно обобщить следующим образом: когда обратный вызов события, зарегистрированный кодом слоя js, не возвращается, цикл событий будет заблокирован на этапе опроса. Увидев это, вы можете подумать, а не заблокируется ли он здесь навсегда? Конечно, этап опроса не может ждать вечно.

Он имеет утонченный дизайн.Проще говоря,

  1. Он сначала рассудит последнегоCheck Phaseтак же какClose PhaseЕсть ли еще обратный вызов, ожидающий обработки. Если есть, он не будет ждать и сразу перейдет к следующему этапу.

  2. Если нет других обратных вызовов, ожидающих выполнения, это дастepollТакой метод устанавливает тайм-аут.

Можете ли вы догадаться, каково подходящее значение для этого тайм-аута? Ответом является разница между временем начала обратного вызова, который должен быть выполнен недавно в фазе таймера, и сейчас, если предположить, что разница в деталях. Потому что нет никакого обратного вызова, ожидающего выполняться после фазы опроса.Так что здесь Ждать не более дельта-времени.Если событие пробуждает цикл сообщений в течение периода, то продолжить работу следующей фазы;если ничего не происходит в течение периода, то после таймаута, цикл сообщений все еще должен войти в следующую фазу, чтобы можно было также выполнить таймер следующей фазы итерации. Nodejs управляет всем циклом обработки сообщений через фазу опроса, ожидая событий ввода-вывода и поступления асинхронных событий ядра.

4.6 Check Phase

Далее следует этап проверки. Этот этап касается толькоsetImmediateфункция обратного вызова. Так почему же здесь особое отношение?setImmediateА как насчет фазы?Проще говоря, потому что фаза опроса может установить некоторые обратные вызовы, надеясь запуститься после фазы опроса.Поэтому эта фаза проверки добавляется после фазы опроса.

4.7 Close Callbacks Phase

Специально обрабатывать некоторые обратные вызовы закрытого типа, напримерsocket.on('close', ...), Используется для очистки ресурсов.

5 Подраздел цикла событий Node.js


  • инициализация узла

    • Инициализируйте среду узла.
    • Выполните введенный код.
    • Выполните обратный вызов process.nextTick.
    • Выполнение микрозадач (Promise).
  • В цикл событий

    • 1. Войдите в стадию таймеров

      • Проверьте, есть ли в очереди таймера обратные вызовы таймера с истекшим сроком действия, и если да, выполните обратные вызовы таймера с истекшим сроком действия в порядке возрастания timerId.
      • Проверить, есть ли задачи process.nextTick, если есть, выполнить их все.
      • Проверяем, есть ли микрозадачи, и если да, то выполняем их все.
      • выйти из этого этапа.
    • 2. Войдите в стадию обратных вызовов ввода-вывода.

      • Проверьте наличие ожидающих обратных вызовов ввода-вывода. Если есть, выполните обратный вызов. Если нет, выйдите из этого этапа.
      • Проверить, есть ли задачи process.nextTick, если есть, выполнить их все.
      • Проверяем, есть ли микрозадачи, и если да, то выполняем их все.
      • выйти из этого этапа.
    • 3. Войдите в режим ожидания, подготовьте этап:

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

    • 4. Войдите в стадию опроса

      • Сначала проверьте, есть ли незаконченный callback, если есть, то тут два случая.

      Первый случай:

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

      Второй случай:

      • Если обратный звонок недоступен.

      • Проверьте, есть ли немедленный обратный вызов, если да, выйдите из фазы опроса. Если нет, заблокируйте на этом этапе, ожидая новых уведомлений о событиях.

      • Если нет ожидающих обратных вызовов, выйдите из фазы опроса.

    • 5. Войдите в фазу проверки

      • Если есть немедленные обратные вызовы, выполняются все немедленные обратные вызовы.
      • Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
      • Проверьте наличие микротаков, и если да, то выполните их все.
      • Выйти из этапа проверки
    • 6. Войдите в фазу закрытия.

      • Если есть немедленные обратные вызовы, выполняются все немедленные обратные вызовы.
      • Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
      • Проверьте наличие микротаков, и если да, то выполните их все.
      • Выход из заключительной фазы

    Проверьте, есть ли активные дескрипторы (дескрипторы событий, такие как таймеры, ввод-вывод и т. д.)

    • Если это так, переходите к следующему циклу.
    • Если нет, завершите цикл событий и выйдите из программы.

Тщательная детская обувь может обнаружить, что следующие процессы выполняются в порядке, прежде чем каждая подзафаза циклов событий выходит:

  • Проверьте, есть ли обратный вызов process.nextTick, и если да, выполните их все.
  • Проверьте наличие микротаков, и если да, то выполните их все.
  • Выйти из текущего этапа.

6 часто задаваемых вопросов

6.1 process.nextTick и обещания

Видно, что диаграмма очереди циклов сообщений не задействованаprocess.nextTickтак же какPromiseТак что же особенного в этих двух обратных вызовах?

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

Кроме того, на разных этапахprocess.nextTickтак же какPromiseКоличество обратных вызовов ограничено, то есть, если вы продолжите добавлять обратные вызовы в эту очередь, весь цикл обработки сообщений будет "зависать". Давайте посмотрим на картинкуprocess.nextTickтак же какPromise:

6.2 setTimeout(..., 0) vs. setImmediate

setTimeout(..., 0)vs. setImmediateКто быстрее?

Давайте возьмем пример, чтобы интуитивно почувствовать это.Это классический вопрос интервью FE.Могу ли я спросить вывод следующего кода:

// index.js

setImmediate(() => console.log(2))
setTimeout(() => console.log(1),0)

Ответ: может быть 1 2, может быть 2 1

Давайте посмотрим на основную проблему этого цикла сообщений с точки зрения принципа: Во-первых, Nodejs запускается, загружает наш JS-код после инициализации среды.(index.js)Произошли две вещи (еще не в цикле сообщений на данный момент):setImmediateДобавьте обратный вызов, чтобы проверить фазуconsole.log(2); setTimeoutДобавлены обратные вызовы в фазу таймераconsole.log(1)На данный момент, чтобы завершить фазу инициализации, необходимо войти в цикл обработки сообщений Nodejs, как показано ниже:

Почему выходов два? Следующий шаг имеет решающее значение:

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

  • еслиTimer Phase 中回调预设的时间 > 消息循环所保存的时间, затем выполните обратный вызов в фазе таймера.В этом случае сначала выведите 1, пока после выполнения фазы проверки не выведите 2. В общем случае результат равен 1 2.

  • Если операция выполняется относительно быстро, заданное время обратного вызова в фазе таймера может точно совпадать со временем, сэкономленным циклом сообщений. В этом случае обратный вызов в фазе таймера не может быть выполнен, и следующая фаза будет продолжена. Check Phase, output 2. Затем дождитесь фазы таймера следующей итерации, и время должно быть удовлетворено.Timer Phase 中回调预设的时间 > 消息循环所保存的时间, такconsole.log(1)Выполнено, вывод 1. Итого результат 2 1.

Следовательно, причина нестабильного вывода зависит от того, соответствует ли время входа в фазу таймера и выполнениеsetTimeoutВремя находится в пределах 1 мс.Если вы измените код на следующий, вы получите стабильный вывод:

require('fs').readFile('my-file-path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});
// 2 1

Это связано с тем, что цикл сообщенийPneding I/O PhaseОбратные вызовы вставляются в очереди Timer и Check, в это время, согласно порядку выполнения цикла сообщений, Check должен быть выполнен до Timer.

Наконец, давайте рассмотрим еще один вопрос из интервью, чтобы углубить наше понимание цикла событий Node:

setImmediate(() => {
    console.log('setImmediate1');
    setTimeout(() => {
        console.log('setTimeout1')
    }, 0);
});
Promise.resolve().then(res=>{
    console.log('then');
})
setTimeout(() => {
    process.nextTick(() => {
        console.log('nextTick');
    });
    console.log('setTimeout2');
    setImmediate(() => {
        console.log('setImmediate2');
    });
}, 0);
//then setTimeout2  nextTick  setImmediate1 setImmediate2 setTimeout1

Почему такой порядок?

  • Сначала выполните микрозадачу Promise и выведитеthen;
  • Войдите в стадию таймера, добавьте обратный вызов микрозадачи process.nextTick, выводsetTimeout2;setImmediateна следующий этап;
  • Войдите в фазу проверки, во время которой переключатель фаз будет выводитьnextTick, выполнить все микрозадачи, затем вывестиsetImmediate1,
  • Войдите в следующий этап, выведите таймерsetImmediate1и проверитьsetImmediate2;

6.3 Можно ли использовать setTimeout(..., 0) вместо setImmediate

С точки зрения производительности,setTimeoutОбработка находится в фазе таймера, где минимальная куча сохраняет обратный вызов таймера, поэтому каждый раз, когда выполняется обратный вызов, будет задействована настройка кучи.setImmediateПросто очистите очередь, эффективность, естественно, будет намного выше.

Тогда поговорим о прессе.setTimeout(..., 0)а такжеsetImmediateПолностью принадлежат двум Фазам.

7. Цикл событий в браузере

Цикл событий в браузере и узле отличается.Цикл событий браузера — это спецификация, определенная в HTML5, а в узле он реализован библиотекой libuv.

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

  • макрозадача:setTimeout, setInterval, setImmediate, I/O,UI Rendering,异步事件绑ждать
  • микрозадача:process.nextTick, РоднойPromise, MutationObserverЖдать

Как видно из рисунка, синхронные задачи будут попадать в стек выполнения js, а асинхронные задачи будут попадать в очередь задач (очередь обратных вызовов) для ожидания выполнения. Как только содержимое стека выполнения будет выполнено, задачи, ожидающие в очереди задач, будут прочитаны и помещены в стек выполнения для начала выполнения.

Конкретный процесс цикла событий:

  • 1. В браузере сначала выполняется текущий стек, а затем выполняются задачи в основном потоке выполнения.

  • 2. Извлеките задачи из очереди микрозадач Microtask и выполняйте их, пока они не опустеют.

  • 3. Выньте задачу в задаче макроса Macrotask для выполнения.

  • 4. Проверить есть ли задачи в микрозадаче Микрозадача, если есть задачи, выполнить их до сброса.

  • Повторите 3 и 4.

Итак, давайте проверим вопросы интервью: когда мы запустим следующий код в браузере, что получится на выходе?

setTimeout(() => {
    console.log('setTimeout1');
    Promise.resolve().then(data => {
        console.log('then3');
    });
},1000);
Promise.resolve().then(data => {
    console.log('then1');
});
Promise.resolve().then(data => {
    console.log('then2');
    setTimeout(() => {
        console.log('setTimeout2');
    },1000);
});
console.log(2);
// 输出结果:2 then1 then2 setTimeout1  then3  setTimeout2

Сначала выполняется содержимое стека, то есть код синхронизации, поэтому выводится 2; Затем очистите микрозадачу, чтобы результат был тогда1, затем2; Поскольку код выполняется сверху вниз, setTimeout1 выполняется и выводится через 1 с; Затем снова очистить микрозадачу, затем выводится 3; Наконец выполните вывод setTimeout2

Ссылаться на: