JS基础系列(二)同/异步任务、宏/微任务的执行顺序

​ 由于这两天面试有遇到相关的问题,以及在维护外包项目时遇到的种种相关的奇葩异步乱用的问题,决定好好捋捋这几个名词在实际中的应用。 一、队列类型 ​ 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 异步任务: ​ 先执行微任务: 从上面执行顺序可知现在有微任务队列1/2/3,按理是按照数字顺序执行的,但既然这节讨论的是async awiat,它的作用就是把异步函数当成同步处理,也就是说等当前的异步执行完之后,才会继续向下执行,所以在这里是先执行微任务2,才会执行微任务1,最后执行微任务3; 至于第二层链式then,因为没有async await语法让它提前执行,所以放在第二梯队的微任务里。 所以第一次微任务--输出then async1-end promise2; 上面执行完第一梯队的微任务,接着执行第二梯队微任务,也就是第二层then语句,所以按顺序输出--then2 promise3; 执行完微任务,最后执行宏任务: 宏任务只有一个setTimeout,所以最终输出--setTimeout。 执行完毕。 最后,如果把await async2()中的await去掉,又会发生什么?请自行验证...... 总结 ​ 所以综上所述,简单总结一句话就是:同步任务结束后,先处理微观任,然务后处理宏观任务,宏观任务内部处理重复上述动作。 ​ 以上内容个人实践总结,如有不对欢迎拍砖?,希望能帮到大家?。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):