1.JavaScript是单线程语言
既然JavaScript是单线程语言,那么就会存在一个问题,所有的代码都是一句一句的来执行。
一、概念
1.事件循环(Event Loop)
什么是Event Loop?
javascript有一个主线程 main thread [θred], 和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
1.任务队列(task queue)
每一个语句就是一个任务, task Queue就是承载的任务队列。JavaScript的Event Loop 就是会不断的过来找这个queue,问有没有task可以运行
2.同步任务(SyncTask)、异步任务(AsyncTask)
同步任务说白了就是主线程来执行的时候立即就能执行的代码
异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(()=>{
console.log(2)
});
console.log(1);
例如: setTimeout就是一个异步任务,主线程去执行的时候遇到setTimeout发现是一个异步任务。就先注册一个任务的回调,然后接着执行下面的语句console.log(1),等上面的异步任务等待时间到了之后,在执行console.log(2)。
1.主线程自上而下执行所有代码
2.同步任务直接进入到主线程被执行,异步任务则进入到Event Table 并注册相应的回调函数
3.异步任务完成后,Event Table 会将这个函数移入Event Queue
4.主线程任务执行完了以后,会从Event Queue中读取任务,进入到主线程去执行
5.循环如上
上述动作不断循环,就是我们所说的事件循环(Event Loop)
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
1.ajax进入Event Table,注册回调函数success
2.执行console.log('代码执行结束')
3.ajax事件完成,回调函数success进入Event Queue
4.主线程从Event Queue读取回调函数success并执行
3.宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
先说说 MacroTask,所有的同步任务代码都是MacroTask(这么说其实不是很严谨,下面解释),setTimeout、setInterval、I/O、UI Rendering 等都是宏任务。
MicroTask,为什么说上述不严谨我却还是强调所有的同步任务都是 MacroTask 呢,因为我们仅仅需要记住几个 MicroTask 即可,排除法!别的都是 MacroTask。MicroTask 包括:Process.nextTick、Promise.then catch finally(注意我不是说 Promise)、MutationObserver。
一次事件循环回来后,开始去执行Task Queue中的Task;但是这里的Task有有优先级。所以优先执行MicroTask Queue 中的task,执行完后在执行MacroTask Queue中的task
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一轮事件循环流程分析如下:
- 整体script作为第一个宏任务进入主线程,遇到
console.log
,输出1。 - 遇到
setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。 - 遇到
process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。 - 遇到
Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。 又遇到了
setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | setTimeout1 | process1 | | setTimeout2 | then1 |上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
- 我们发现了
process1
和then1
两个微任务。 - 执行
process1
,输出6。 - 执行
then1
,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了
process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | setTimeout2 | process2 | | | then2 |第二轮事件循环宏任务结束,我们发现有
process2
和then2
两个微任务可以执行。- 输出3。
- 输出5。
- 第二轮事件循环结束,第二轮输出2,4,3,5。
- 第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将
process.nextTick()
分发到微任务Event Queue中。记为process3
。 - 直接执行
new Promise
,输出11。 将
then
分发到微任务Event Queue中,记为then3
。 | 宏任务Event Queue | 微任务Event Queue | | :—-: | :—-: | | | process3 | | | then3 |第三轮事件循环宏任务执行结束,执行两个微任务
process3
和then3
。- 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)