一、EventLoop
javascript是一门单线程语言,既然是单线程的,就是同一时间只能做一件事情。那么问题来了,我们访问一个页面,这个页面的初始化代码运行时间很长,比如有很多图片、视频、外部资源等等,难道我们也要一直在那等着吗?答案当然是 不能
所以就出现了两类任务:同步任务和异步任务
1.同步和异步任务分别进入不同的 ‘‘场所’’ 执行。所有同步任务都在主线程上执行,形成一个执行栈;而异步任务进入Event Table并注册回调函数。
2.当这个异步任务有了运行结果,Event Table会将这个回调函数移入Event Queue,进入等待状态。
3.当主线程内同步任务执行完成,会去Event Queue读取对应的函数,并结束它的等待状态,进入主线程执行。
4.主线程不断重复上面3个步骤,也就是常说的Event Loop(事件循环)。
- 那怎么知道主线程执行栈为空啊?
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
异步任务又分为宏任务和微任务
宏任务:整体代码script、setTimeout、setInterval、I/O、UI交互事件,可以理解是每次执行栈执行的代码就是一个宏任务。
微任务:Promise,process.nextTick,且process.nextTick优先级大于promise.then。可以理解是在当前 task 执行结束后立即执行的任务;
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
如下举例:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log(),立即执行。
- 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- 第一轮事件循环结束了,我们开始第二轮循环,从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
- 结束。
二、下面来具体介绍一下宏任务和微任务
1、setTimeout
setTimeout的秒数意思是经过指定时间后,把要执行的任务加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于指定的时间。举例说明:
setTimeout(() => {
myFn()
},2000)
bigFn(10000000)
- 遇到setTimeout,将
myFn()
函数注册到Event table中, - 执行
bigFn(10000000)
函数,假设这个函数很慢,仍在计时中 - 2000ms过去了,要将
myFn()
放入到Event queue中,bigFn(10000000)
函数依然没有执行完。等待… - 当
bigFn(10000000)
函数执行完毕,将myFn()
从Event queue中拉出来放入到主线程中执行。
补充:setTimeout(fn,0)
的意思是,当主线程空闲下来,立刻执行fn()
2、setInterval
对于setInterval(fn,ms)
来说,不是每过ms会执行一次fn,而是每过ms,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
3、Promise
Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的,then后面的回调函数放在了微任务队列中。
举例说明:
new Promise(function(resolve) {
console.log('10');
resolve();
}).then(function() {
console.log('11');
});
console.log('12');
// 10 12 11
new Promise
方法体中的函数是被当做同步代码立即执行的,所以先输出10then
被分配到微任务队列中。- 遇到
console.log('12');
,执行。主线程清空,接下来执行微任务队列,输出11。
4、async和await
async函数返回的是一个Promise对象,所以它里面的方法体也是立即执行函数。
当async函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。也就是说,一旦遇到await,就让出了线程(跳出async函数体)然后继续执行后面的脚本的。
举例:
async function async1(){
console.log('1')
await async2()
console.log('2')
}
async function async2(){
console.log('3')
}
console.log('4')
async1();
// 4 1 3 2
async
为立即执行函数,所以输出1- 遇到
await async2()
,因为async2()
也是async函数,所以立即执行,输出3,但是由于有await
,所以让出线程,将async1()
后面未执行的语句放入到微任务队列中,继续执行下面的函数。 - 遇到
console.log('4')
,输出4,此时主线程空闲,将微任务中的任务拉到主线程中执行。 - 输出2。
资料参考:
https://segmentfault.com/a/1190000019494012
https://juejin.cn/post/6844903512845860872#heading-5
https://segmentfault.com/a/1190000015057278