由于这两天面试有遇到相关的问题,以及在维护外包项目时遇到的种种相关的奇葩异步乱用的问题,决定好好捋捋这几个名词在实际中的应用。
一、队列类型
js是单线程编程语言,所以js的执行顺序是按语句的顺序去排列的。
js的执行任务可以分为两类:
- 同步任务:就是在主线程上的任务,顺序到达后马上执行;
- 异步任务:在主线程上异步执行的任务,顺序到达后并不会马上执行,但会被排在任务队列里,执行完同步任务后按队列执行异步任务。
二、执行
不管理没理解,不废话,直接刚:
1、下面先看同步任务:A
console.log('start');
function task() {
console.log('task');
}
task();
console.log('end');
在控制台可以看到输出start task end
,这就是同步任务,只要顺序到达马上执行。
2、接着看异步任务
异步任务有ES5的settimeout、setinterval
以及ES6的promise
。
settimeout
接着上面的代码,先看前者:B
console.log('start');
setTimeout(() => {
console.log('s1');
});
function task() {
console.log('task');
}
task();
console.log('end');
可以看到,尽管settimeout在task函数的前面,但s1在最后输出,表明settimeout是异步任务,排在主线程之外的队列中执行。
为了进一步验证,我们增加难度,看下面代码:C
console.log('start');
setTimeout(() => {
console.log('s1');
});
function task() {
console.log('task');
setTimeout(() => {
console.log('s2');
});
}
task();
setTimeout(() => {
console.log('s3');
});
console.log('end');
刷新浏览器,可以看到控制台在最后按顺序输出s1 s2 s3
,这表明所有的settimeout
事件在同一队列里,所以队列里的settimeout按顺序执行。
在日常开发过程中我们经常会遇到异步嵌套异步,如果同个队列内部都有异步,这时候的执行又是怎样的呢?接下来继续增加难度:D
console.log('start');
setTimeout(() => {
console.log('s1');
setTimeout(() => {
console.log('s4');
});
});
function task() {
console.log('task');
setTimeout(() => {
console.log('s2');
});
}
task();
setTimeout(() => {
console.log('s3');
setTimeout(() => {
console.log('s5');
});
});
console.log('end');
上面我们在两个settimeout里分别新增了一个settimeout,这时候的执行顺序会不会有什么不同呢?
继续刷新浏览器,在控制台看输出...end s1...s5
,怎么样,有没有觉得很奇怪?
如果不理解js的任务队列执行顺序问题,会对上面的代码执行结果表示一脸萌,起码当初的我就是这种表情。
所以接着C的思路在D的体现:主线程任务先执行,异步任务推入任务队列,主线程任务执行完成之后按顺序继续执行任务队列的任务;在任务队列里有二维异步任务,推入第二条队列,执行完第一队列后,继续执行第二队列;
看到这里,应该对js的任务队列有一定的理解了吧,如果还不理解,就按照上面的例子换着法子使劲折腾就对了,实践出真知,在学习编程的时候是最最真的道理了。
看完settimeout
的例子,接下来我们继续看 promise
,至于setinterval
就暂时不讨论了。
promise
为了循序渐进,我们接着B的例子一点点增加难度,继续:E
console.log('start');
setTimeout(() => {
console.log('s1');
});
function task() {
console.log('task');
}
new Promise(resolve => {
console.log('p1');
resolve(true);
});
task();
console.log('end');
刷新浏览器可以看到输出start p1 task end s1
,为啥?
原因所在,就要引申出宏任务和微任务的概念了。
如果对js的事件循环机制理解不深,到这里或许就要懵了,既有同步任务异步任务还有任务队列,现在又多了了宏观任务和微观任务,这咋判断?接下来我就讲讲这几个概念的...我也讲不清楚,所以我就找了一段网上我觉得描述的比较好的:
js是单线程语言,对于异步操作只能先把它放在一边,按照某种规则按先后顺序放进一个容器(其实就是存入宏观任务和微观任务队列中),先处理同步任务,再处理异步任务。异步任务分为 [ 宏观任务队列、微观任务队列 ]
按照规定,能发起宏观任务的方法有:
script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境);
微观任务的方法有:
Promise.then、MutaionObserver、process.nextTick(Node.js 环境),async/await实际上是promise+generator的语法糖,也就是promise,也就是微观任务;
有promise就少不了then,所以在E基础上我们加上then,再看输出:F
console.log('start');
setTimeout(() => {
console.log('s1');
});
function task() {
console.log('task');
}
new Promise(resolve => {
console.log('p1');
resolve(true);
}).then(() => {
console.log('then');
});
task();
console.log('end');
之前的输出是start p1 task end s1
,加了then之后输出start p1 task end then s1
,then在s1之前输出。
按照上面对几个概念的描述,promise的执行属于微任务,settimeout属于宏任务,而F的输出表明微任务先于宏任务执行。可是,这是绝对的吗?下面我们继续作:G
console.log('start');
setTimeout(() => {
console.log('s1');
new Promise(resolve => {
console.log('p2');
resolve(true);
}).then(() => {
console.log('then2');
});
});
function task() {
console.log('task');
}
new Promise(resolve => {
console.log('p1');
resolve(true);
}).then(() => {
console.log('then');
});
task();
console.log('end');
可以看到新添加的微任务的结果p2 then2
在最后输出,这是不是跟上面“微任务先于宏任务执行”有冲突?仔细一想,其实不然,在上面G的代码执行顺序来看,可以分为几个执行步骤:
先执行同步任务:
start、p1、task、end
,为何p1会先执行?因为这时候的p1其实还在同步任务里,then之后的操作才在异步任务队列中;接着执行异步任务,而异步任务分为宏任务和微任务,微任务先于宏任务执行,所以第二步执行:
then、s1
;-
最后执行宏任务内部的异步,也就是微任务:
p2、then2
。 所以从上面可以总结:先执行主线程的同步任务,这是第一梯队;若有异步,先执行异步里的微任务也就是then内部的操作,这是第二梯队;然后执行宏任务也就是settimeout内部的操作,这是第三梯队;如果第三梯队中又有微任务,继续执行,这是第四梯队。
为了验证,我们继续作:H
console.log('123');
setTimeout(() => {
console.log('s1');
new Promise(resolve => {
console.log('res3');
resolve(true);
}).then(() => {
console.log('then3');
});
});
setTimeout(() => {
console.log('s2');
}, 0);
function task() {
console.log('task');
setTimeout(() => {
console.log('s3');
});
new Promise(resolve => {
console.log('res2');
resolve(true);
}).then(() => {
console.log('then2');
});
}
new Promise(resolve => {
console.log('res');
setTimeout(() => {
console.log('ps');
});
resolve(true);
console.log('after');
}).then(() => {
console.log('then');
});
console.log('456');
task();
console.log('end');
看完上面的代码,先别管结果如何,有没有想吐槽:变态!谁会写这样的代码!你别不信,其实这是我在接外包项目debug的时候经常遇到的事。因为他们的代码往往是以完成任务为目标而得出来的,至于完成的过程是怎样的,他们完全不会关注,这就导致代码内部会出现如宏观任务嵌套微任务,微任务又与宏任务以及微任务相互并行的现象,至于中间会出现什么问题,They don't care...扯远了,想知道上面的结果是啥,自己看。
async...await
最后看看promise的语法糖async await在队列中又有什么不同, 下面继续刚:I
async function async1() {
console.log('async1-start');
await async2();
console.log('async1-end');
}
async function async2() {
console.log('async2');
new Promise(function(resolve) {
console.log('resolve1');
resolve();
})
.then(() => {
console.log('then');
})
.then(() => {
console.log('then2');
});
}
console.log('script-start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
})
.then(function() {
console.log('promise2');
})
.then(function() {
console.log('promise3');
});
console.log('script-end');
上面代码最后输出是script-start async1-start async2 resolve1 promise1 script-end then async1-end promise2 then2 promise3 setTimeout
。
如果不看结果,自己预想一遍,能得到正确的输出吗?如果理解了代码从A~H的意思,基本上是能得到正确答案的,下面我们梳理一下执行顺序:
-
同步任务:
-
script-start先执行--输出
script-start
; - 接着执行asyn1()函数,函数内部的async1-start在函数的同步序列中,紧接着被输出,由于async的缘故,后面的语句会被放到微任务队列-1--输出
async1-start
; - 然后到async2()函数,首先会输出async2,接着由于await的原因使得当前函数变为同步,所以resolve1虽然在promise内部,但还在主线程上,所以也马上被输出,then放在微任务队列-2中--输出
async2 resolve1
; - 执行完asyn1()函数的同步任务,继续向下执行,遇到
new Promise
,内部的promise1
在主线程上,马上被输出,then放在微任务队列-3中--输出promise1
; - 最后执行输出
script-end
-
script-start先执行--输出
-
异步任务:
先执行微任务:
-
从上面执行顺序可知现在有微任务队列1/2/3,按理是按照数字顺序执行的,但既然这节讨论的是async awiat,它的作用就是把异步函数当成同步处理,也就是说等当前的异步执行完之后,才会继续向下执行,所以在这里是先执行微任务2,才会执行微任务1,最后执行微任务3;
至于第二层链式then,因为没有async await语法让它提前执行,所以放在第二梯队的微任务里。
所以第一次微任务--输出
then async1-end promise2
; -
上面执行完第一梯队的微任务,接着执行第二梯队微任务,也就是第二层then语句,所以按顺序输出--
then2 promise3
;执行完微任务,最后执行宏任务:
-
宏任务只有一个setTimeout,所以最终输出--
setTimeout
。执行完毕。
最后,如果把
await async2()
中的await
去掉,又会发生什么?请自行验证...... -
总结
所以综上所述,简单总结一句话就是:同步任务结束后,先处理微观任务后处理宏观任务,宏观任务内部处理重复上述动作。
以上内容个人实践总结,如有不对欢迎拍砖