введение
В недавнем интервью меня спросили,JSПоскольку он однопоточный, почему можно выполнять асинхронные операции?
В то время мой разум был затуманен, и мое мышление было в ловушке单线程По этому вопросу я думал о том, почему один поток может запускать дополнительные задачи.На самом деле, я написал связанный контент в блоге, который я написал давно, но это заняло слишком много времени, чтобы забыть, поэтому мне приходится часто просматривать его :(О принципе и реализации Генератора и Промиса)
- JS однопоточный, есть только один основной поток
- Код в функции выполняется последовательно сверху вниз, при встрече с вызываемой функцией она сначала входит в вызываемую функцию для выполнения и продолжает выполняться после завершения.
- Когда встречается асинхронное событие, браузер открывает другой поток, основной поток продолжает выполняться, и после возврата результата выполняется функция обратного вызова.
фактическиJSЯзык запускается в хост-среде, например浏览器环境,nodeJs环境
- В браузере браузер отвечает за предоставление этого дополнительного потока.
- существует
Nodeсередина,Node.jsс помощьюlibuvв качестве абстрактного уровня инкапсуляции для защиты различий между различными операционными системами,Nodeможно использоватьlibuvдля достижения многопоточности.
И этот асинхронный поток делится на微任务а также宏任务, в этой статье будет рассмотреноJSАсинхронный принцип и механизм цикла событий
ЗачемJavaScriptоднопоточный
JavaScriptГлавной особенностью языка является то, что он является однопоточным, то есть одновременно может выполняться только одно действие. Сконструированная таким образом схема в основном обусловлена ее языковыми особенностями, т.к.JavaScriptэто язык сценариев браузера, который манипулируетDOM, он может отображать анимацию и взаимодействовать с пользователями.Если он многопоточный, порядок выполнения нельзя предсказать, а также сложно решить, на каком потоке основана операция.
Таким образом, чтобы избежать сложности, JavaScript с самого начала был однопоточным, что было основной особенностью языка и не изменится в будущем.
существуетHTML5раз, браузеры, чтобы в полной мере использоватьCPUпреимущество в производительности, позволяющееJavaScriptСоздайте несколько потоков, но даже если дополнительные потоки могут быть созданы, эти дочерние потоки по-прежнему контролируются основным потоком и не должны работать.DOM, аналогично открытию потока для расчета сложных задач. После завершения расчета основной поток уведомляется о том, что расчет завершен, и результат предоставляется вам. Это похоже на метод асинхронной обработки, поэтому он не изменение по существу.JavaScriptоднопоточный характер.
Стек вызовов функций и очередь задач
стек вызовов функций
JavaScriptСуществует только один основной поток и один стек вызовов (call stack), что такое стек вызовов?
Это похоже на ведерко для пинг-понга, первый шарик для пинг-понга выпадет последним.
Возьмите каштан:
function a() {
console.log("I'm a!");
};
function b() {
a();
console.log("I'm b!");
};
b();
Процесс выполнения следующий:
-
Первым шагом является выполнение файла, который будет помещен в стек вызовов (например, имя файла
main.js)call stack main.js -
Второй шаг, знакомство
b()синтаксис, вызовb()метод, стек вызовов будет помещен в этот метод для вызова:call stack b()main.js -
Шаг 3: звонок
b()функция, которая вызывается внутриa(),В настоящее времяa()будет помещен в стек вызовов:call stack a()b()main.js -
четвертый шаг:
a()вывод после звонкаI'm a!, стек вызовов будетa()всплывает, становится следующим:call stack b()main.js -
пятый шаг:
b()вывод после звонкаI'm b!, стек вызовов будетb()Он всплывает и становится следующим:call stack main.js -
Шаг 6:
main.jsПосле выполнения файла стек вызовов будетb()Вытолкните его, станьте пустым стеком, ожидая выполнения следующей задачи:call stack
Это простой стек вызовов.В стеке вызовов, когда выполняется предыдущая функция, все следующие функции должны дождаться завершения предыдущей задачи перед выполнением.
Однако есть много задач, выполнение которых занимает много времени, а если все время ждать, то эффективность стека вызовов будет крайне низкой.JavaScriptРазработчик языка понял, что основной поток этих задач вообще не нужно ждать, пока эти задачи приостановлены, сначала вычисляются последующие задачи, а когда выполнение завершается, то возвращаются к задаче, так что нет является任务队列Концепция чего-либо.
очередь задач
Все задачи можно разделить на два типа, один из них同步任务(synchronous), другой异步任务(asynchronous).
Синхронная задача относится к задаче, поставленной в очередь для выполнения в основном потоке, и последняя задача может быть выполнена только после завершения выполнения первой задачи.
Асинхронные задачи относятся к тому, чтобы не входить в основной поток, а входить"任务队列"(task queue)задачи, только"任务队列"Уведомление основного потока о том, что асинхронная задача может быть выполнена до того, как задача войдет в основной поток для выполнения.
Итак, когда что-то вродеsetTimeoutПри ожидании асинхронной операции она будет передана другим модулям браузера для обработки.setTimeoutПосле указанного времени задержки выполнения функция обратного вызова будет помещена в очередь задач.
Конечно, функции обратного вызова разных асинхронных задач обычно помещаются в разные очереди задач. После выполнения всех задач в стеке вызовов выполняется функция обратного вызова в очереди задач.
Он представлен картинкой:
На приведенном выше рисунке стек вызовов вызывается последовательно, и как только асинхронная операция будет найдена, она будет передана другим модулям ядра браузера для обработки.ChromeДля браузеров этот модульwebcoreмодули, упомянутый выше асинхронный API,webcoreпредоставляется отдельноDOM Binding,network,timerмодуль для обработки. Когда эти модули завершают обработку этих операций, функции обратного вызова помещаются в очередь задач, а затем функции обратного вызова в очереди задач выполняются после выполнения задач в стеке.
Давайте сначала посмотрим на интересное явление, я запускаю кусок кода, и как вы думаете, каков порядок вывода:
setTimeout(() => {
console.log('setTimeout')
}, 22)
for (let i = 0; i++ < 2;) {
i === 1 && console.log('1')
}
setTimeout(() => {
console.log('set2')
}, 20)
for (let i = 0; i++ < 100000000;) {
i === 99999999 && console.log('2')
}
Вот так! Результат довольно квантован:
Так что же это за процесс на самом деле? Затем я проанализирую вышеописанный процесс:
-
Сначала файл помещается в стек
-
Запустите файл, прочитайте первую строку кода, когда встретите
setTimeout, исполнительный механизм добавляет его в стек. (Я сделал его немного толще, потому что шрифт слишком тонкий...) -
обнаружение стека вызовов
setTimeoutдаWebapisсерединаAPI, поэтому передайте его браузеруtimerМодуль обрабатывается при обработке следующей задачи.
-
второй
setTimeoutвставить в стек -
Как показано выше, асинхронный запрос помещается в
异步APIОбработка и следующая операция push одновременно: -
При выполнении асинхронного
app.jsПосле того, как файл называется, стек вызовов загоняется. После завершения асинхронного выполнения функция обратного вызова введена в очередь задач: -
Очередь задач сообщает стеку вызовов, что на моей стороне есть еще не выполненные задачи, и стек вызовов выполнит задачи в очереди задач:
Приведенный выше процесс объясняет, что браузер встречаетsetTimeoutПосле этого, как это реализовать, можно резюмировать в следующих пунктах:
- Стек вызовов вызывает задачи последовательно
- Когда стек вызовов находит асинхронную задачу, он передает асинхронную задачу другим модулям для обработки и продолжает самостоятельно выполнять следующие вызовы.
- После завершения асинхронного выполнения асинхронный модуль помещает задачу в очередь задач и уведомляет об этом стек вызовов.
- После того, как стек вызовов завершит выполнение текущей задачи, он выполнит задачи в очереди задач.
- После того, как стек вызовов выполняет задачи в очереди задач, он продолжает выполнять другие задачи.
Весь этот процесс называется事件循环(Event Loop).
Итак, зная так много, можете ли вы, ребята, разобрать вывод следующего кода из цикла событий?
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
console.log(i)
Разобрать:
- Во-первых, потому что
varПеременное продвижение ,iДействителен в глобальном масштабе - Опять же, код сталкивается
setTimeoutПосле этого передайте функцию другим модулям для обработки, и продолжайте выполнять ее самостоятельно.console.log(i), благодаря переменному продвижению,iНа данный момент он был пройден 10 раз.iценность10, то есть выход10 - После этого, после того, как асинхронный модуль обработает функцию, он помещает callback в очередь задач и уведомляет стек вызовов
- Через 1 секунду стек вызовов последовательно выполняет callback-функцию, потому что в это время
iстал10, то есть вывести 10 раз10
Для иллюстрации используйте следующую схему:
Теперь пусть друзья внезапно осознают и поймут, почему этот код выводит это содержимое снизу вверх:
Затем проблема возникает снова, давайте посмотрим на следующий код:
setTimeout(() => {
console.log(4)
}, 0);
new Promise((resolve) =>{
console.log(1);
for (var i = 0; i < 10000000; i++) {
i === 9999999 && resolve();
}
console.log(2);
}).then(() => {
console.log(5);
});
console.log(3);
Как вы думаете, что на выходе?
Некоторые друзья начали анализировать,promiseОн также асинхронный, сначала выполняет содержимое функции внутри и выводит1а также2, а затем выполните следующую функцию, выведите3,ноPromiseЕго нужно прокрутить 9,99 миллиона раз,setTimeoutВыполняется за 0 миллисекунд,setTimeoutдолжны быть немедленно помещены в стек выполнения,PromiseПосле помещения в стек выполнения результат должен быть следующим:
На самом деле ответ1,2,3,5,4О, почему это? Это включает в себя внутреннюю часть очереди задач, макро-задач и микро-задач.
Макрозадачи и микрозадачи
Что такое макрозадачи и микрозадачи
Очередь задач делится наmacro-task(宏任务)а такжеmicro-task(微任务), в последнем стандарте они соответственно называютсяtaskа такжеjobs.
-
macro-task(宏任务)Вероятно, включают:script(整体代码),setTimeout,setInterval,setImmediate(NodeJs),I/O,UI rendering. -
micro-task(微任务)Вероятно, включают:process.nextTick(NodeJs),Promise,Object.observe(已废弃),MutationObserver(html5新特性) - Задачи из разных источников задач будут попадать в разные очереди задач. в
setTimeoutа такжеsetIntervalгомологичны.
Фактически, цикл событий определяет порядок выполнения кода, начиная с глобального контекста, входящего в стек вызовов функций, до тех пор, пока стек вызовов не будет опустошен, а затем выполняя всеmicro-task(微任务), когда всеmicro-task(微任务)После выполнения выполнитьmacro-task(宏任务),один изmacro-task(宏任务)Очередь задач завершила выполнение (например,setTimeoutочереди), выполнить всеmicro-task(微任务), и циклы, пока выполнение не будет завершено.
Разобрать
Теперь я начинаю анализировать код выше.
-
Первый шаг, общий код
scriptнажмите стек и выполнитеsetTimeoutПосле этого выполнитеPromise: -
Второй шаг, возникающий при выполнении
Promiseпример,PromiseПервым параметром конструктора являетсяnew, поэтому она не попадет ни в какую другую очередь, а будет выполняться непосредственно в текущей задаче, а последующие.thenбудет распространяться наmicro-taskизPromiseиди в очередь. -
Третий шаг, стек вызовов продолжает выполнять задачу макроса
app.js, выход3и вытолкнуть стек вызовов,app.jsПосле выполнения извлеките стек вызовов: -
Четвертый шаг, на этот раз,
macro-task(宏任务)серединаscriptКогда очередь завершает выполнение, цикл обработки событий начинает выполнять всеmicro-task(微任务): -
Пятый шаг, стек вызовов находит все
micro-task(微任务)Все сделано, бегиmacro-task(宏任务)передачаsetTimeoutочередь: -
Шаг 6,
macro-task(宏任务)setTimeoutПосле выполнения очереди стек вызовов обращается к микрозадаче, чтобы проверить, есть ли невыполненные микрозадачи, если нет, он обращается к макрозадаче, чтобы выполнить следующую очередь. нет очереди для выполнения макрозадачи.Вызов завершается, и содержимое вывода1,2,3,5,4.
Тогда вывод приведенного выше примера очевиден. Вы можете попробовать сами.
Суммировать
- Разные задачи будут помещены в разные очереди задач.
- выполнить первым
macro-task, подождите, пока стек вызовов функций не будет очищен, прежде чем выполнять все вызовы в очередиmicro-task. - подожди пока все
micro-taskПосле выполнения изmacro-taskНачинается выполнение одной из очередей задач и так далее. - Порядок выполнения очереди макрозадач и микрозадач устроен следующим образом:
-
macro-task(宏任务):script(整体代码),setTimeout,setInterval,setImmediate(NodeJs),I/O,UI rendering. -
micro-task(微任务):process.nextTick(NodeJs),Promise,Object.observe(已废弃),MutationObserver(html5新特性)
Расширенный пример
Итак, позвольте мне придумать интересный код:
<script>
setTimeout(() => {
console.log(4)
}, 0);
new Promise((resolve) => {
console.log(1);
for (var i = 0; i < 10000000; i++) {
i === 9999999 && resolve();
}
console.log(2);
}).then(() => {
console.log(5);
});
console.log(3);
</script>
<script>
console.log(6)
new Promise((resolve) => {
resolve()
}).then(() => {
console.log(7);
});
</script>
В каком порядке выводится этот код?
На самом деле, студенты, которые понимают описанный выше процесс, должны знать весь процесс.Чтобы некоторые студенты не поняли, я кратко проанализирую:
-
первый,
script1Входим в очередь задач (для удобства ставлю две штукиscriptпо имениscript1,script2): -
Второй шаг,
script1Сделайте вызов и извлеките стек вызовов: -
третий шаг,
script1После завершения выполнения, после опустошения стека вызовов все микрозадачи вызываются напрямую: -
На четвертом шаге, после выполнения всех микрозадач, стек вызовов продолжит вызывать очередь макрозадач:
-
Пятый шаг, выполнить
script2, и всплывает: -
На шестом шаге стек вызовов начинает выполнять микрозадачи:
-
Седьмой шаг, стек вызовов вызывает все микрозадачи, а затем запускается для выполнения макрозадач:
На этом все задачи выполнены, вывод1,2,3,5,6,7,4
После понимания приведенного выше содержания, я думаю, вы можете сделать это с немного более сложными отношениями асинхронного вызова:
setImmediate(() => {
console.log(1);
},0);
setTimeout(() => {
console.log(2);
},0);
new Promise((resolve) => {
console.log(3);
resolve();
console.log(4);
}).then(() => {
console.log(5);
});
console.log(6);
process.nextTick(()=> {
console.log(7);
});
console.log(8);
//输出结果是3 4 6 8 7 5 2 1
окончательное испытание
setTimeout(() => {
console.log('to1');
process.nextTick(() => {
console.log('to1_nT');
})
new Promise((resolve) => {
console.log('to1_p');
setTimeout(() => {
console.log('to1_p_to')
})
resolve();
}).then(() => {
console.log('to1_then')
})
})
setImmediate(() => {
console.log('imm1');
process.nextTick(() => {
console.log('imm1_nT');
})
new Promise((resolve) => {
console.log('imm1_p');
resolve();
}).then(() => {
console.log('imm1_then')
})
})
process.nextTick(() => {
console.log('nT1');
})
new Promise((resolve) => {
console.log('p1');
resolve();
}).then(() => {
console.log('then1')
})
setTimeout(() => {
console.log('to2');
process.nextTick(() => {
console.log('to2_nT');
})
new Promise((resolve) => {
console.log('to2_p');
resolve();
}).then(() => {
console.log('to2_then')
})
})
process.nextTick(() => {
console.log('nT2');
})
new Promise((resolve) => {
console.log('p2');
resolve();
}).then(() => {
console.log('then2')
})
setImmediate(() => {
console.log('imm2');
process.nextTick(() => {
console.log('imm2_nT');
})
new Promise((resolve) => {
console.log('imm2_p');
resolve();
}).then(() => {
console.log('imm2_then')
})
})
// 输出结果是:?
Вы можете оставить свои результаты в комментариях~
- Обновлено 15 октября 2018 г.
P.S. Некоторые студенты спрашивали меняajaxЧто за задача, нашел, что забыл написать,ajaxпринадлежать宏任务, отсортировано как
script > DOM (onclick, onscroll...) > ajax > setTimeout > setInterval > setImmediate (NodeJs) > ввод-вывод > рендеринг пользовательского интерфейса
Наконец, я сожалею, что продвигаю свою работу на основеTaroБиблиотека компонентов, написанная фреймворком:MP-ColorUI.
Я очень рад, что могу сыграть главную роль, спасибо.
Щелкните здесь для документации
Нажмите здесь, чтобы узнать адрес GitHub