我们知道js的最大特点就是单线程,就是同一时间只能做一件事。即便HTML5提出Web Worker允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
1、任务队列
因为js的单线程的特性,所有运行在js线程中的代码需要根据某种规则来按队列执行。因为js中既有同步事件,又有异步事件,所以有时候我们在工作中写js代码时总会一脸懵逼,特别是在浏览器环境中进行断点调试时,所以弄懂js代码的执行顺序非常重要。按照js回调事件的特性,将任务分为同步任务和异步任务。
- 1、所有同步任务都在主线程中执行,并形成一个执行栈,异步任务在有了运行结果之后才会将回调函数添加到任务队列中。
- 2、代码运行时会先去执行主线程中的代码,等到主线程中的代码执行完毕后,才会去读取任务队列中的回调函数并放置到执行栈上来执行,先添加到任务队列中的会被先执行。等到任务队列中的回调函数执行完毕后,又会回到主线程上来。
- 3、主线程中会不断重复执行第二步,并形成一个循环。这就是所谓的EventLoop。
2、事件队列
任务队列按照某种条件又可以细分为microtask 和 macrotask,通常我们会称之为微任务和宏任务。代码执行的优先级为:主线程>微任务>宏任务。
- 微任务主要有:ES6的Promise(then函数),node.js中的process.nextTick(),MessageChannel(消息通道,类似worker)
- 宏任务主要有:整体代码script、setTimeout 和 setInterval、还有 MessageChannel,setImmediate, I/O
js中代码执行的顺序是:首先执行主线程中的代码(宏任务),开始第一次循环,执行完毕后,再执行所有的微任务,然后再执行宏任务,看该宏任务中是否有微任务,如果有的话,就将所有的微任务执行完毕。再执行新的宏任务,如此不断的反复执行下去。
事件执行机制(即js代码执行机制)如下图(图片来自ssssyoki)所示。
//宏任务
setTimeout(function(){
console.log("我是宏任务1");
})
let p=new Promise((resolve,reject)=>{
console.log("promise1");
resolve();
});
//微任务
p.then(res=>{
console.log("我是微任务1")
});
let p2=new Promise((resolve,reject)=>{
console.log("promise2");
resolve();
});
//微任务
p2.then(res=>{
console.log("我是微任务2")
});
//宏任务
setTimeout(function(){
console.log("我是宏任务2");
})
console.log("我是主线程");
执行结果如下所示:
代码解析:
- 1、js代码开始执行后,遇到setTimeout,将其回调函数添加到宏任务队列中,记为setTimeout1;
- 2、遇到Promise,Promise构造函数中的代码是同步任务,所以最先输出promise1,同时将then函数添加到微任务队列中,记为then1;
- 3、接着再次遇到Promise,然后输出promise2,并将其then函数添加在微任务队列中,记为then2。
- 4、执行到setTimeout,将其回调函数添加到宏任务队列中,记为setTimeout2。
- 5、最后输出"我是主线程"。此时主线程中的代码执行完毕。此时任务队列中的情况如下。
宏任务 | 微任务 |
---|---|
setTimeout1 | then1 |
setTimeout2 | then2 |
- 6、接着开始执行微任务队列中的所有任务,输出"我是微任务1","我是微任务2",此时第一轮事件事件循环结束。
- 7、随着setTimeout1回调事件的执行,意味着第二轮事件循环的开始,接着输出"我是宏任务1",它的结束意味着第二轮事件循环的结束。
- 8、随着setTimeout2回调事件的执行,意味着第三轮事件循环的开始,它的结束就意味着第三轮事件循环的结束。
- 9、开始第二次循环,执行第一个宏任务中的代码,并输出"我是宏任务1",接着输出"我是宏任务2"。 所以我的理解其实就是宏任务队列中有几个宏任务,就意味着在此次js代码执行过程中有几次事件循环。
3、浏览器环境中和node环境中EventLoop的异同
示例代码如下所示; //宏任务,第二次事件循环的开始
setTimeout(function() {
console.log('setTimeout1');
new Promise(function(resolve) {
console.log('setTimeout1-Promise');
resolve();
//微任务
}).then(function() {
console.log('setTimeout1-then')
})
})
new Promise(function(resolve) {
console.log('Promise');
resolve();
//微任务
}).then(function() {
console.log('then')
})
//宏任务,第三次事件循环的开始
setTimeout(function() {
console.log('setTimeout2');
new Promise(function(resolve) {
console.log('setTimeout2-promise');
resolve();
}).then(function() {
console.log('setTimeout2-then')
})
})
1、浏览器中js事件循环机制
上面代码在浏览器环境中的输出结果如下所示:
所以我得出结论:在浏览器环境下js代码的执行顺序是。主线程>微任务队列中所有的回调函数>宏任务队列中的所有宏任务。主线程和微任务成为第一次事件循环。宏任务队列中的一个宏任务就是一个事件循环。2、node环境中js事件循环机制
个人觉得node中的js执行机制比在浏览器中的要复杂一些。盗用掘金网友的一张图很好的解释了node环境中的事件循环机制。 node环境中的eventLoop是按阶段来执行的,主要有6个阶段,这个阶段里的代码执行完毕,才会去执行下一个阶段里的代码。6个阶段中的代码都执行完毕才算是完成一个事件循环。:- Node的Event Loop分阶段,阶段有先后,依次是
1、expired timers and intervals,即到期的setTimeout/setInterval
2、I/O events,包含文件,网络等等
3、immediates,通过setImmediate注册的函数
4、close handlers,close事件的回调,比如TCP连接断开 - 同步任务及每个阶段之后都会清空microtask队列
1、优先清空next tick queue,即通过process.nextTick注册的函数
2、再清空other queue,常见的如Promise - 而和规范的区别,在于node会清空当前所处阶段的队列,即执行所有task
而在node环境中的输出结果是这样的,两次执行结果还不一样
上面的代码在node环境中的执行结果如下所示。
结果分析:
- 首先执行主线程中的代码,输出promise1。
- 接着清空微队列中的任务,即执行外层then函数中的代码,输出then1(微任务不在任何一个阶段执行,在各个阶段切换的中间执行)。
- 接着开始执行timer阶段中的所有任务,所以此时输出setTimeOut1——>setTimeOut1-Promise——>setTimeOut2——>setTimeOut2-Promise。
- timer阶段中的任务执行完毕后,又开始执行微任务队列中的所有任务。所以此时输出setTimeOut1-then、setTimeOut1-then2。
最后: node中的eventLoop运行机制比较复杂,所以还需要花费更多的时间去多多研究。
参考文档
1、这一次,彻底弄懂 JavaScript 执行机制
2、JavaScript 运行机制详解:再谈Event Loop
3、Eventloop不可怕,可怕的是遇上Promise
4、浏览器说:虽然都叫event loop,但是我和node不一样