Спецификация и реализация цикла событий

Node.js JavaScript
Спецификация и реализация цикла событий

Об авторе: nekron Ant Financial Data Experience Technology Team

последовательность

я всегдаEvent LoopКогнитивные определения программы — это все оценки, которые могут быть известны или нет, поэтому я сохраняю только поверхностные понятия и никогда не изучаю их, пока не прочитаю эту статью——«На этот раз досконально изучите механизм выполнения JavaScript». Автор этой статьи очень дружелюбен, и я извлек много пользы из самого маленького примера, но последний пример включает в себяchromeа такжеNodeРезультаты операции ниже очень разные, мне очень любопытно, и я чувствую, что необходимо изучить этот кусок знания.

По вышеперечисленным причинам родилась эта статья, изначально я планировал разделить полный текст на три части: спецификация, реализация и применение. Но, к сожалению, из-за своих ограниченных знаний я не могуEvent LoopКогда мы представляем сценарий приложения с характеристиками приложения, на самом деле нет вывода, в результате чего для приложения слишком мало места, поэтому это не отражено в заголовке.

(Все среды выполнения кода в этой статье включают только Node v8.9.4 и Chrome v63)

ЧАСТЬ 1: Спецификация

Зачем нужен цикл событий?

Поскольку Javascript изначально разрабатывался как однопоточный язык, чтобы добиться неблокировки основного потока,Event LoopТакой план появился.

викторина (1)

Давайте сначала посмотрим на кусок кода, каким будет результат печати?

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0)

Promise.resolve().then(() => {
	console.log(3)
}).then(() => {
	console.log(4)
})

console.log(5)

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

  1. Прежде всего, давайте сначала исключим асинхронный код, сначала узнаем код, выполняемый синхронно, мы можем знать, что первая печать должна быть1、5
  2. Однако имеют ли setTimeout и Promise приоритет? Или зависит от порядка выполнения?
  3. Кроме того, будет ли setTimeout вставлен между многоуровневым и Promise?

В замешательстве я попытался запустить код, и правильный результат:1、5、3、4、2.

Так почему же?

определение

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

  1. Выполнить самую старую задачу (один раз)
  2. Проверьте, есть ли микрозадача, затем продолжайте выполнение, пока очередь не будет очищена (несколько раз)
  3. выполнить рендеринг

Молодец, я еще не разобрался с проблемой, а тут вдруг еще 2 понятияtaskа такжеmicrotask, что меня еще больше смутило. . .

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

В задачу в основном входит:setTimeout,setInterval,setImmediate,I/O,UI交互事件

Микрозадача в основном включает в себя:Promise,process.nextTick,MutaionObserver

весь основнойEvent Loopкак показано на рисунке:

  • Очередь можно рассматривать как структуру данных для хранения функций, которые необходимо выполнить.
  • Функция, зарегистрированная API типа таймера (setTimeout/setInterval), войдет в очередь задач по истечении срока ее действия (механизм работы таймера здесь подробно описываться не будет)
  • Остальные функции регистрации API напрямую входят в соответствующие им очереди задач/микрозадач.
  • Цикл событий выполняется один раз, извлекает задачу из очереди задач для выполнения
  • Цикл событий продолжает проверять, пуста ли очередь микрозадач, и выполняется последовательно до тех пор, пока очередь не опустеет.

规范.png | center | 585x357

Продолжить тест (2)

В это время оглянитесь на предыдущую测试(1), я обнаружил, что концепция была очень ясной, я сразу получил правильный ответ, я чувствовал себя мило и больше не боялсяEvent Loopсейчас~

Затем приготовьтесь ответить на более сложные вопросы (этот вопрос взят изпоследовательностьСтатья, упомянутая в, я удалила первойprocess.nextTick):

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})

проанализируйте, как показано ниже:

  1. Код, который выполняется синхронно, сначала выводит:1、7
  2. Затем очистите очередь микрозадач:8
  3. Первая задача выполняет:2、4
  4. Затем очистите очередь микрозадач:5
  5. Вторая задача выполняет:9、11
  6. Затем очистите очередь микрозадач:12

существуетchromeДавай запустим, хорошо!

Уверен, я надут, готов добавитьprocess.nextTickЗатем продолжите тестирование на node. Сначала тестирую первую задачу, код такой:

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

С предыдущим накоплением я уверенно написал ответ на этот раз:1、7、8、6、2、4、5、3.

Однако, красавчик всего на 3 секунды, правильный ответ:1、7、6、8、2、4、3、5.

打脸3.png | left | 64x64

Я запутался, но быстро понял, что значит**process.nextTickЗарегистрированная функция имеет более высокий приоритет, чемPromise**, так что все логично~

Далее я тестирую вторую задачу:

console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
    process.nextTick(() => {
        console.log(3)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

process.nextTick(() => {
    console.log(6)
})

setTimeout(() => {
    console.log(9)
    process.nextTick(() => {
        console.log(10)
    })
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})

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

  • Вывод первой задачи:1、7、6、8、2、4、3、5
  • Затем вторая задача выводит:9、11、10、12

Впрочем, плевок в лицо. . .

В первый раз, когда я выполняю его, вывод:1、7、6、8、2、4、9、11、3、10、5、12(То есть выполнение двух задач смешивается вместе). Я продолжаю, и иногда он печатает ответ, который я ожидал.

Реальность так необъяснима! какие! какие!

吐血1.jpg | left | 200x117

(Ах, простите, кровь не могла остановиться какое-то время)Итак, почему это? ? ?

ЧАСТЬ 2: Реализация

Как говорится:

Спецификации составляются людьми, код пишется людьми. --анонимный

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

Викторина (3)

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

Во-первых, давайте проведем небольшой тест: что он выдаст?

setTimeout(() => {
	console.log(2)
}, 2)

setTimeout(() => {
	console.log(1)
}, 1)

setTimeout(() => {
	console.log(0)
}, 0)

Студенты, у которых не было глубокого контакта с таймером, ответят непосредственно из настройки задержки в коде:0、1、2.

И некоторые другие студенты с некоторым опытом могут ответить:2、1、0. потому чтоДокументация MDN для setTimeoutВ спецификации HTML указано, что минимальная задержка составляет 4 мс:

(Дополнительное примечание: минимальная задержка устанавливается, чтобы оставить время отдыха ЦП)

In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.

И студенты, которые действительно пострадали, скажут вам, что ответ таков:1、0、2. и, будь тоchromeещеnodeСледующие результаты совпадают.

(Исправление ошибки: после многих проверок порядок вывода в узле все еще не гарантирован, а таймер узла на самом деле является метафизикой~)

таймер в Хроме

от测试(3)Из результатов видно, что эффект задержки 0 мс и 1 мс одинаков, в чем причина? Давайте сначала проверимblinkреализация.

(Я не знаю, как искать, где размещен код Blink. К счастью, имя файла относительно очевидно. Ответ не занял много времени)

(Я сразу выложил нижний код, если вас интересует верхний код, проверьте сами)

// https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/DOMTimer.cpp#93

double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond); 

Здесь интервал - это входящее значение.Можно видеть, что результат входящего 0 и входящего 1 составляет одну миллисекунду, что составляет 1 мс.

Это объясняет, почему поведение 1 мс и 0 мс одинаково, и что происходит с 4 мс? я снова подтвердилСпецификация HTML, обнаружил, что хотя и есть ограничение в 4 мс, но есть условия, подробности см. в пункте 11 спецификации:

If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

И что интересно,Документация MDN на английском языкеОписание также соответствует этой спецификации.

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

таймер в узле

ЭтоnodeПочему эффект задержки 0 мс и 1 мс одинаков?

(Или управляемый код github выглядит удобным, просто найдите целевой код напрямую)

// https://github.com/nodejs/node/blob/v8.9.4/lib/timers.js#L456

if (!(after >= 1 && after <= TIMEOUT_MAX))
  after = 1; // schedule on next tick, follows browser behavior

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

Цикл событий в узле

Таймер выше — это небольшой эпизод, теперь мы возвращаемся к сути этой статьи —Event Loop.

давайте сосредоточимся наnodeпо реализацииblinkРеализация этой статьи не расширена, в основном потому, что:

  • chromeВ настоящее время поведение соответствует норме
  • Доступно не так много документации
  • Я не умею искать и даже не знаю, где найти код ядра. . .

原谅1.jpg | left | 264x250

(Пропустить весь процесс исследования...)

Глядя непосредственно на вывод, следующий рисунокnodeизEvent Loopвыполнить:

node_event_loop.png | center | 832x460

Дополнительные инструкции:

  • NodeизEvent Loopпоэтапно, этапы последовательно
    • expired timers and intervals, то есть setTimeout/setInterval, срок действия которого истекает
    • I/O events, включая файлы, сети и т. д.
    • immediates, функция, зарегистрированная через setImmediate
    • close handlers, обратный вызов близкого события, такое как отключение соединения TCP
  • Очередь микрозадач будет очищаться после задач синхронизации и каждого этапа.
    • Приоритет очищенnext tick queue, то есть черезprocess.nextTickзарегистрированная функция
    • снова пустоother queue, обычный как Обещание
  • Отличие от спецификации в том, что нода будет очищать очередь на текущем этапе, то есть выполнять все задачи

Повторный тест (2)

Понять реализацию, оглянуться назад测试(2):

// 代码简略表示
// 1
setTimeout(() => {
	// ...
})

// 2
setTimeout(() => {
	// ...
})

Видно, что из-за двухsetTimeoutта же задержка, слитая в ту жеexpired timers queue, выполняя вместе. Так что ставь второйsetTimeoutЕсли задержка изменена на более чем 2 мс (1 мс недействительна, подробности см. выше), вы можете гарантировать, что эти дваsetTimeoutОн не истечет одновременно, а также может обеспечить согласованность результатов вывода.

тогда, если я поставлю один изsetTimeoutизменить наsetImmediate, можно ли также гарантировать порядок вывода?

ответне можем. Хотя можно гарантироватьsetTimeoutа такжеsetImmediateОбратные вызовы не будут выполняться в беспорядке, но нет гарантии, чтоsetTimeoutа такжеsetImmediateПорядок, в котором выполняются обратные вызовы.

существуетnodeДалее, глядя на простейший пример, вывод следующего кода не гарантируется:

setTimeout(() => {
	console.log(0)	
})

setImmediate(() => {
	console.log(1)
})

// or
setImmediate(() => {
	console.log(0)
})

setTimeout(() => {
	console.log(1)	
})

Суть дела в томsetTimeoutКогда он истекает, только просроченныйsetTimeoutчтобы убедиться, что вsetImmediateвыполнял раньше.

Но если так例子(2), хотя согласованность вывода в принципе может быть гарантирована, но настоятельно не рекомендуется:

// 先使用setTimeout注册
setTimeout(() => {
	// ...
})

// 一系列micro tasks执行,保证setTimeout顺利到期
new Promise(resolve => {
	// ...
})
process.nextTick(() => {
	// ...
})

// 再使用setImmediate注册,“几乎”确保后执行
setImmediate(() => {
	// ...
})

Или другой способ мышления для обеспечения порядка:

const fs = require('fs')

fs.readFile('/path/to/file', () => {
    setTimeout(() => {
        console.log('timeout')
    })
    setImmediate(() => {
        console.log('immediate')
    })
})

Итак, почему такой код гарантируетsetImmediateОбратный вызов имеет приоритет надsetTimeoutКак насчет выполнения обратного вызова?

Потому что, когда два обратных вызова успешно зарегистрированы одновременно, текущийnodeизEvent LoopОн находится вI/O queueэтап, а следующий этапimmediates queue, поэтому можно гарантировать, что даже еслиsetTimeoutистек и также будетsetImmediateВыполняется после обратного вызова.

ЧАСТЬ 3: Применение

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

Проверить на ошибки

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

опрос

Когда будут сложные сценарии очереди? Например, на собеседовании нет гарантии, что будет такой странный тест, чтобы с ним легко справились~

приоритет выполнения

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

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

Таким образом, высокоприоритетный код может использоватьPromise/process.nextTickРегистрация выполняется.

эффективность

отnodeС точки зрения реализации,setTimeoutэтот тип таймераAPI, вам нужно создавать объекты таймера и повторять операции, а обработка задач должна выполняться в небольшой корневой куче, а временная сложность составляетO(log(n)). Напротив,process.nextTickа такжеsetImmediateВременная сложностьO(1), более высокая эффективность.

Если есть требование к эффективности выполнения, используйте его в первую очередь.process.nextTickа такжеsetImmediate.

разное

Добро пожаловать всем, чтобы добавить вместе~

Ссылаться на

Студенты, которым интересна команда, могут подписаться на колонку или отправить свое резюме по адресу tao.qit####alibaba-inc.com.replace('####', '@'). Добро пожаловать, присоединяйтесь~

Оригинальный адрес:GitHub.com/proto team/no…