Принцип асинхронности JavaScript
Для FEer JavaScript является однопоточным, и одновременно может выполняться только одна задача. Преимущество этого режима в том, что его относительно просто реализовать, а среда выполнения относительно проста; недостатком является то, что пока одна задача занимает много времени, все последующие задачи должны ждать в очереди, что приведет к задержке выполнения всей программы. Обычная невосприимчивость браузера (ложная смерть) часто связана с тем, что определенный фрагмент кода JavaScript выполняется в течение длительного времени (например, бесконечный цикл), в результате чего вся страница зависает на этом месте, и другие задачи не могут быть выполнены. Для вычислительных задач нужно использовать ЦП, поэтому можно только ждать выполнения задачи; но много раз ЦП простаивает, например, выполняет операции ввода-вывода (ввод и вывод), запросы ajax, чтение и запись файлов и т. д. Эти операции ЦП может полностью игнорировать операции ввода-вывода и продолжать выполнять другие задачи. Event loop
Эту проблему решает асинхронный механизм (Event Loop), используемый внутри JavaScript.
JavaScript является однопоточным, и одновременно может выполняться только одна задача.
Конечно, существует решение Web Worker, которое может открывать несколько потоков в браузере, но оно может выполнять только расчетные операции и не может работать с DOM.
цикл событий
Цикл событий имеет очередь событий (здесь хранятся все происходящие события, которые на рисунке ниже называются очередью задач). Есть еще один Event Loop
, который постоянно удаляет эти события из очереди и вызывает обратные вызовы в событиях (стек вызовов будет выполнять все обратные вызовы). API — это API, используемый для обработки асинхронных функций, таких как обработка и ожидание ответов от клиентов или серверов, чтение локальных файлов, установка времени ожидания и т. д.
В этом процессе все вызовы функций сначала попадают в стек вызовов, а затем выполняют асинхронные задачи через API. Когда асинхронная задача завершена, обратный вызов входит в очередь задач, а затем снова входит в стек вызовов. После выполнения задачи цикл событий снова перейдет в очередь задач, чтобы повторить описанный выше процесс.
тип задачи
Очередь задач, упомянутая выше, в браузере в основном делится на два типа задач: макрозадачи и микрозадачи.
Все они генерируются путем вызова API, предоставленного браузером.
Ниже перечислены API, которые могут генерировать асинхронные задачи в браузерах и Nodejs.
Макрозадача (macrotask)
- setTimeout
- setInterval
- setImmediate (только узел)
- requestAnimationFrame (только браузер)
- I/O
- Рендеринг пользовательского интерфейса (только в браузере)
микрозадача
- process.nextTick (только узел)
- Обещать
- Объект.наблюдать
- MutationObserver
Полный процесс выполнения цикла событий можно найти в «Познакомьтесь с механизмом цикла событий (Event Loop)»[ .
Асинхронное программирование JavaScript
Развитие асинхронного программирования JavaScript в браузерах можно разделить на четыре этапа.
- Перезвонить
- Обещать
- Генератор
- асинхронно/ожидание
Перезвонить
Функция обратного вызова очень проста, ее легко понять и реализовать. Недостатком является то, что это не способствует сопровождению и чтению кода. Сильная связь между различными частями также вызовет ад обратного вызова.
Возьмем в качестве примера реализацию светофоров.
function red() { console.log('red') } function green() { console.log('green') } function yellow() { console.log('yellow') } const light = (timer, light, callback) => { setTimout(() => { switch(light) { case 'red': red(); break; case 'green': green(); break; case 'yellow': yellow(); break; } callback() }, timer) } const work = () => { task(3000, 'red', () => { task(1000, 'green', () => { task(2000, 'yellow', work) }) }) } work()
Обещать
Promise был предложен для решения проблемы ада обратных вызовов, которая позволяет преобразовать традиционное написание вложенных функций обратного вызова в цепные вызовы.
const promiseLight = (timer, light) => { return new Promise((resolve, reject) => { setTimeout(() => { switch (light) { case 'red': red(); break; case 'green': green(); break; case 'yellow': yellow(); break; } resolve() }, timer) }) } const work = () => { promiseLight(3000, 'red') .then(() => promiseLight(1000, 'green')) .then(() => promiseLight(2000, 'yellow')) .then(work) }
Генератор
Функция-генератор может приостанавливать выполнение и возобновлять выполнение, а также имеет две функции: преобразование данных и механизм обработки ошибок в теле функции. Я считаю, что многие студенты редко используют генератор в своей реальной работе, но понимание его может позволить нам реализовать много интересных функций. Подробное введение см. в двух статьях "Что такое генераторы JavaScript и как их использовать" и "Значение и использование функций генератора". >
const generator = function *() { yield promiseLight(3000, 'red') yield promiseLight(1000, 'green') yield promiseLight(2000, 'yellow') yield generator() } const generatorObj = generator() generatorObj.next() generatorObj.next() generatorObj.next()
асинхронно/ожидание
Этот синтаксис позволяет нам программировать асинхронный код так же, как мы пишем синхронный код. Генератор на самом деле является синтаксическим сахаром функции asyc.
Если вы хотите узнать больше об использовании async/await, вы можете обратиться к "Значению и использованию асинхронной функции"
const asyncTask = async () => { await promiseLight(3000, 'red') await promiseLight(1000, 'green') await promiseLight(2000, 'yellow') } asyncTask()
Сходства и различия между браузерами и Nodejs
До Node11.0.0 (не включая Nodejs 11) существовали некоторые различия в деталях между асинхронными процессами Node и браузерами.
Версии Nodejs до 11.0.0.0 имеют цикл обработки событий:
После выполнения всех задач в основной очереди выполните задачи в очереди микрозадач.
Всего в Node 6 очередей задач: включая 4 основные очереди (основная очередь) и две промежуточные очереди (промежуточная очередь)
Подробнее см. в «[Translation] Node Event Loop Series — 2, Timer, Immediate и nextTick» и «Event Loop Node.js, таймеры и процесс. следующийTick()”.
Версии Nodejs после 11.0.0 имеют тот же цикл обработки событий, что и браузеры:
После выполнения задачи в основной очереди немедленно выполнить все задачи в очереди микрозадач, а затем выполнить следующую задачу в основной очереди задач
BadCase асинхронного программирования
В реальном процессе разработки как при разработке требований к внешнему интерфейсу, так и при разработке функций Nodejs используется async/await
grammar, что обеспечивает большое удобство разработки. Однако, если вы не знакомы с асинхронным механизмом JS, это приведет к ошибкам использования и, в конечном итоге, к функциональным ошибкам, которые иногда чрезвычайно трудно обнаружить.
Далее поясняется на практических примерах.
Последовательное выполнение асинхронной функции
Иногда необходимо вызвать несколько асинхронных функций в одной и той же функции, но между вызываемыми асинхронными функциями нет прямой и обратной зависимости, которая может выполняться параллельно, например несколько запросов асинхронного интерфейса; используя метод записи async/await, легко написать в строке execute. Следующий пример
function sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } async function main() { const start = console.time('async'); await sleep(1000); await sleep(2000); const end = console.timeEnd('async'); } // 3s
Решение
Для асинхронных функций, выполняемых в одном стеке выполнения, если между ними нет зависимости, вы можете использовать Promise.all() для параллельного выполнения; или сначала выполните функцию без ожидания, а затем дождитесь обещания, возвращаемого асинхронной функцией.
function sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } // 1 async function main() { const start = console.time('async'); await Promise.all([sleep(1000), sleep(2000)]); const end = console.timeEnd('async'); } // 2 async function main() { const start = console.time('async'); const promise1 = sleep(1000); const promise2 = sleep(2000); const s1 = await promise1; const s2 = await promise2; const end = console.timeEnd('async'); } //2s
не могу поймать ошибку
Используя использование Promise, вы можете перехватить исключение, которое происходит в промисе, только с помощью .catch, а try/catch не может его перехватить; в то время как синтаксис async/await должен использовать try/catch, чтобы поймать его.
В некоторых случаях, даже если для переноса тела асинхронной функции используется try/catch, ошибки все равно не будут обнаружены.
async function err() { throw "error" } async function main() { try { return err(); } catch (err) { console.log(err); } } main();
Для удобства асинхронная функция возвращается напрямую. В этом случае, если в функции err возникает исключение, исключение не может быть перехвачено.
Следует избегать, насколько это возможно, прямого возврата асинхронной функции без ожидания непосредственно в асинхронной функции; вышеуказанное может быть решено двумя способами.
- Используйте await в теле асинхронной функции, чтобы дождаться выполнения всех асинхронных функций.
async function main() { try { const res = await err(); return res; } catch (err) { console.log(err); } }
- Используйте catch вне тела основной функции для перехвата исключений.
async function main() { try { return err(); } catch (err) { console.log(err); } } main().catch(err => { console.log(err); })
Кроме того, можно использовать библиотеку await-to-js
для перехвата, который используется аналогично обработке ошибок в Go.
async function main() { try { const [err, res] = await to(err()); return err(); } catch (err) { console.log(err); } }
Исходный код этой библиотеки также очень прост, если вам интересно, обратитесь к scopsy/await-to-js[7] .
Думайте синхронно и пишите асинхронный код
При написании кода с помощью async/await может быть проще «обмануться», потому что async/await утверждает, что асинхронный код может быть написан синхронно. При реализации некоторых более сложных функций легко игнорировать проблему асинхронных сценариев.
Например, интерфейсная страница должна реализовать функцию задачи, нажать кнопку задачи (при условии, что есть 2 кнопки задач), она сначала запросит интерфейс для получения данных, а затем изменит цвет страницы.
Кнопка A, измененная страница красного цвета; кнопка B, измененная страница синего цвета;
Ожидаемый эффект заключается в том, что цвет страницы должен быть цветом, соответствующим последнему нажатию кнопки задачи.
async function taskA() { return new Promise((resolve) => { setTimeout(() => { changePageColor('red') resolve() }, 500); }) } async function taskB() { return new Promise((resolve) => { setTimeout(() => { changePageColor('blue') resolve() }, 1000); }) } function changePageColor(color) { console.log(color); } async function executeTask(task) { await task() } //click B executeTask(taskB); //click A executeTask(taskA);
Приведенный выше код сначала имитирует нажатие кнопки B, а затем нажатие кнопки A. Кнопка A запрашивает возврат перед кнопкой B. Если он реализован в соответствии с синхронным мышлением, возможный код реализации будет таким, как указано выше. Конечным результатом является то, что страница сначала становится красной, а затем синей; окончательный цвет ожидаемой страницы должен быть красным.
Вышеуказанные проблемы должны учитывать непредсказуемость времени завершения асинхронных операций и влияние различных асинхронных операций на одни и те же данные. Вышеуказанные проблемы могут быть решены с помощью идеи замка. Прежде чем изменить цвет страницы, сначала оцените, равен ли тип текущей блокировки типу блокировки, соответствующей задаче. Если они равны, измените цвет, в противном случае не выполняйте его.
let workingLock = false; async function taskA() { return new Promise((resolve) => { workingLock = 'red'; setTimeout(() => { if (workingLock === 'red') { changePageColor('red') } resolve() }, 500); }) } async function taskB() { return new Promise((resolve) => { workingLock = 'blue' setTimeout(() => { if (workingLock === 'blue') { changePageColor('blue') } resolve() }, 1000); }) } function changePageColor(color) { console.log(color); } async function executeTask(task) { await task(); } executeTask(taskB); executeTask(taskA);