事件循环
javascript本身是一门单线程的语言,但是js中却时时存在着异步的概念。要完全理解异步,就要先了解js的运行核心:事件循环(event loop)。
事件循环大体上来说就是javascript引擎会不断地检索任务队列(task queue),将task queue中的任务通过call stack执行的过程。
知识点:
1. 一个线程中,event loop是唯一的,但是task queue可以拥有多个;
2. task queue又分为macro-task(宏任务)与micro-task(微任务),新标准中,分别称为task与jobs;
3. macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;
4. micro-task大概包括: process.nextTick, Promise.then, MutationObserver(html5新特性);
5. setTimeout/Promise等我们称之为任务源,而进入task queue的是它们的具体执行任务。
6. 不同任务源的任务会进入到不同的task queue,但setTimeout与setInterval同源。
7. event loop的顺序,决定了JavaScript代码的执行顺序。
从task开始,script(整体代码)进入第一次循环,就是全局上下文先进入call stack;
直到call stack清空(只剩全局),然后执行所有的jobs,执行完毕后第一次循环结束;
然后js引擎检查task queue,如果为空,则继续检查;
如果不为空,再从task开始,然后再执行所有的jobs,如此一直循环下去。
8. 其中每一个任务的执行,都是借助call stack来完成的。
示例
console.log('global')
setTimeout(function () {
console.log('timeout')
new Promise(function (resolve) {
console.log('timeout_promise')
resolve()
}).then(function () {
console.log('timeout_then')
})
},2000)
for (var i = 1;i <= 5;i ++) {
setTimeout(function() {
console.log(i)
},i*1000)
console.log(i)
}
new Promise(function (resolve) {
console.log('promise')
resolve()
}).then(function () {
console.log('then')
})
console.log('end')
分析以上代码:
代码整体是一个task,也就是全局上下文入执行栈,开始一行行执行代码,所以先打印'global';
遇到定时两秒的setTimeout,属于宏任务。此时,会创建一个timer类型的宏任务队列,将这个setTimeout的回调函数push到宏任务队列中,等待2秒后再通过call stack执行这个回调函数;
接下来执行for循环,连续创建5个时间间隔为1秒的timer,并push到timer类型的宏任务队列中。并打印'1 2 3 4 5';
下面开始执行promise里面的代码,先直接打印'promise',调用回调函数resolve()表示异步操作成功,然后调用then方法,Promise.then属于微任务,那么引擎会创建该类型的微任务队列,then方法的回调函数会被push到微任务队列中,等待当前task执行完毕后调用;也就是会先打印'end',再打印'then'。至此,第一轮循环结束;
js引擎在代码整体执行完毕后,会不断检索task queue,如果为空,则继续检索;如果存在task,则通过call stack调用,此时开始执行新一轮循环。
假设执行完第一轮循环只花了100ms,而timer中延时最短的是1s,那剩下的900ms,可以理解为js引擎的自嗨(检索)时间;直到1s的timer延迟结束,该timer的回调函数被call stack调用(或理解为压入执行栈),打印 '6',至此第二轮循环结束;
继续走下去,又过了漫长的1秒...发现task queue中存在两个延时相同的timer,此时js引擎会严格按照加入队列的顺序来执行,也就是会先打印'timeout','timeout_promise', 'timeout_then',最后才打印'6';由这里我们可以看出,call stack是先执行task,再执行当前task中的jobs,再执行另一个task;
最后剩下的3个timer,每间隔1s打印一个'6',得到666~;