知识铺垫
单线程模型:由于JavaScript被设计为用在浏览器环境,而该环境下存在大量可能发生冲突的DOM操作,为了避免进行复杂的冲突处理(可能存在的冲突数量几乎不可预测),JavaScript的设计者舍弃了java的多线程模型(该模型下,执行引擎同时可以做几件事,但要进行线程同步),将其设计成了一门单线程语言(执行引擎在同一时间只做一件事)。
注意:这里的单线程是指JavaScript的主线程只有一个。除了这个主线程,JavaScript还有一个I/O线程,通过事件循环来处理I/O问题,但两者之间相对独立,不需要进行状态同步,因此我们仍然可以把JavaScript看成一门单线程语言。
任务队列:所谓任务队列,就是用于存储等待执行的任务的队列。由于JavaScript是一门单线程语言,如果当前有一个任务需要执行,但JavaScript引擎正在执行其他任务,那么这个任务就需要放进一个队列中进行等待。等到线程空闲时,就可以从这个队列中取出最早加入的任务进行执行
(单线程相当于说这家银行只有一个服务窗口,一次只能为一个人服务,后面到的就需要排队,而任务队列就是排队区,先到的就优先服务)。
注意:如果当前线程空闲,并且队列为空,那每次加入队列的函数将立即执行。
事件循环机制(Event Loop)
含义:事件循环分为两种,分别是浏览器事件循环和node.js事件循环
JS的主要运行环境有两个,浏览器和Node.js
事件循环又分为同步任务和异步任务
同步任务
含义:在主线程上排队执行的任务,只有一个任务执行完毕,才能执行后一个任务
异步任务
含义:不进入主线程,而进入“任务队列(task queue)”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
分类:异步任务又分为宏任务和微任务。
所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈),而异步则先放到任务队列(task queue)里,任务队列又分为宏任务(macro-task)与微任务(micro-task)。
宏任务
宏任务包括:script(整体代码)、setTimout、setInterval、setImmediate(node.js环境)、I/O、UI交互事件
微任务
微任务包括:promise.then(new promise构造函数是同步)、async/await。
若同时存在promise和nextTick,则先执行nextTick
执行过程
1.所有同步任务都在主线程上执行,形成一个执行栈(调用栈)
2.主线程之外,还存在一个‘任务队列’(task queue),浏览器中的各种 Web API 为异步的代码提供了一个单独的运行空间,当异步的代码运行完毕以后,会将代码中的回调送入到 任务队列中(队列遵循先进先出得原则)
3.一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将队列中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行
执行顺序
先执行同步代码,
遇到异步宏任务则将异步宏任务放入宏任务队列中,
遇到异步微任务则将异步微任务放入微任务队列中,
当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,
微任务执行完毕后再将异步宏任务从队列中调入主线程执行,
一直循环直至所有任务执行完毕。
注意:当宏任务和微任务都处于 任务队列(Task Queue) 中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务;
下面是详细的示例讲解
难度逐个增加
示例1
//遇到setTimout,异步宏任务,放入宏任务队列中;
setTimeout(function(){
//从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空,结束~
console.log('1');
});
//遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2;
new Promise(function(resolve){
console.log('2');
resolve();
// 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
}).then(function(){
//从微任务队列中取出任务到主线程中,输出3,微任务队列为空
console.log('3');
});
//遇到同步任务console.log(‘4’);输出4;主线程中同步任务执行完
console.log('4');
//输出顺序:2 4 3 1
遇到setTimout,异步宏任务,放入宏任务队列中;
遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2;
而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
遇到同步任务console.log(‘4’);输出4;主线程中同步任务执行完
从微任务队列中取出任务到主线程中,输出3,微任务队列为空
从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空,结束~
注意宏任务内部嵌套了一个微任务的情况,或者微任务内部嵌套微任务 ,只有外层的异步任务在主线程执行完毕后,才能执行判断内部的微任务/宏任务,并放入任务队列中。
示例2
//遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中
setTimeout(()=>{
new Promise(resolve =>{
resolve();
}).then(()=>{
//此时由于回调还未执行,所以无法将setTimeout内部的 console.log('test')放入微任务队列中
//异步微任务d
console.log('test');
});
//异步宏任务
console.log(4);
});
new Promise(resolve => {
resolve();
//同步任务 输出1
console.log(1)
}).then( () => {
//异步微任务,放入队列中
console.log(3);
//由于是在.then回调之后才进行进行,因此等待微任务队列继续执行
Promise.resolve().then(() => {
//异步微任务a
console.log('before timeout');
//此时又有一个.then,这个是异步微任务b
}).then(() => {
Promise.resolve().then(() => {
//异步微任务c
console.log('also before timeout')
})
})
})
//同步任务 输出2
console.log(2);
//输出:1 2 3 before timeout also before timeout 4 test
遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中,此时由于回调还未执行,所以无法将setTimeout内部的 console.log('test');放入微任务队列中
遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中,同理,内部的 console.log('before timeout')与 console.log('also before timeout')都尚未放入微任务队列中
遇到同步任务console.log(2),输出2;主线程中同步任务执行完
从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
从微任务队列中取出任务a到主线程中,输出 before timeout;
从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空,结束
示例3
async function async1() {
//同步任务
console.log('async1 start');
await async2();
//执行完async2()后才执行这里
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function(resolve) {
//同步任务
console.log('promise1');
resolve();
}).then(function() {
//异步微任务
console.log('promise2');
});
}
//同步任务
console.log('script start');
setTimeout(function() {
//异步宏任务
console.log('setTimeout');
}, 0)
//执行async1()
async1();
new Promise(function(resolve) {
//同步任务
console.log('promise3');
resolve();
}).then(function() {
//异步微任务
console.log('promise4');
});
//同步任务
console.log('script end');
// script start
// async1 start
// promise1
// promise3
// script end
// promise2
// async1 end
// promise4
// setTimeout
script start
同步任务直接执行,此时遇到settimeout推入异步宏任务,继续遇到async1()
async1 start
async1同步任务直接执行,await是异步,下面的console.log推入异步微任务,执行async2()
promise1
promise的声明是同步任务,直接执行,“.then”后面的内容是异步,推入异步微任务,继续向下寻找是否还有同步任务
promise3
promise的声明是同步任务,直接执行,“.then”后面的内容是异步,推入异步微任务,继续向下寻找是否还有同步任务
script end
所有的同步任务执行完毕,按“先进先出”顺序从微任务队列中执行
promise2
async1 end
promise4
setTimeout
可能有些人对await的这个执行逻辑有点懵,这样转换一下可能就好理解一些,关于异步制的讲解笔者也有讲过 文章链接: http://t.csdn.cn/rCYVT
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等价于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}