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

​ 由于这两天面试有遇到相关的问题,以及在维护外包项目时遇到的种种相关的奇葩异步乱用的问题,决定好好捋捋这几个名词在实际中的应用。

一、队列类型

​ js是单线程编程语言,所以js的执行顺序是按语句的顺序去排列的。

js的执行任务可以分为两类:

  1. 同步任务:就是在主线程上的任务,顺序到达后马上执行;
  2. 异步任务:在主线程上异步执行的任务,顺序到达后并不会马上执行,但会被排在任务队列里,执行完同步任务后按队列执行异步任务。

二、执行

不管理没理解,不废话,直接刚:

1、下面先看同步任务A

console.log('start');
function task() {
  console.log('task');
}
task();
console.log('end');

在控制台可以看到输出start task end,这就是同步任务,只要顺序到达马上执行。

2、接着看异步任务

异步任务有ES5settimeout、setinterval以及ES6promise

settimeout

接着上面的代码,先看前者:B

console.log('start');
setTimeout(() => {
  console.log('s1');
});
function task() {
  console.log('task');
}
task();
console.log('end');

可以看到,尽管settimeouttask函数的前面,但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 s1thens1之前输出。

​ 按照上面对几个概念的描述,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的代码执行顺序来看,可以分为几个执行步骤:

  1. 先执行同步任务:start、p1、task、end,为何p1会先执行?因为这时候的p1其实还在同步任务里,then之后的操作才在异步任务队列中;

  2. 接着执行异步任务,而异步任务分为宏任务和微任务,微任务先于宏任务执行,所以第二步执行:then、s1

  3. 最后执行宏任务内部的异步,也就是微任务: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的意思,基本上是能得到正确答案的,下面我们梳理一下执行顺序:

  1. 同步任务:

    1. script-start先执行--输出script-start
    2. 接着执行asyn1()函数,函数内部的async1-start在函数的同步序列中,紧接着被输出,由于async的缘故,后面的语句会被放到微任务队列-1--输出async1-start
    3. 然后到async2()函数,首先会输出async2,接着由于await的原因使得当前函数变为同步,所以resolve1虽然在promise内部,但还在主线程上,所以也马上被输出,then放在微任务队列-2中--输出async2 resolve1
    4. 执行完asyn1()函数的同步任务,继续向下执行,遇到new Promise,内部的promise1在主线程上,马上被输出,then放在微任务队列-3中--输出promise1
    5. 最后执行输出script-end
  2. 异步任务:

    ​ 先执行微任务

    1. 从上面执行顺序可知现在有微任务队列1/2/3,按理是按照数字顺序执行的,但既然这节讨论的是async awiat,它的作用就是把异步函数当成同步处理,也就是说等当前的异步执行完之后,才会继续向下执行,所以在这里是先执行微任务2,才会执行微任务1,最后执行微任务3

      至于第二层链式then,因为没有async await语法让它提前执行,所以放在第二梯队的微任务里。

      所以第一次微任务--输出then async1-end promise2

    2. 上面执行完第一梯队的微任务,接着执行第二梯队微任务,也就是第二层then语句,所以按顺序输出--then2 promise3

      执行完微任务,最后执行宏任务

    3. 宏任务只有一个setTimeout,所以最终输出--setTimeout

      执行完毕。

    最后,如果把await async2()中的await去掉,又会发生什么?请自行验证......

总结

​ 所以综上所述,简单总结一句话就是:同步任务结束后,先处理微观任务后处理宏观任务,宏观任务内部处理重复上述动作。

​ 以上内容个人实践总结,如有不对欢迎拍砖

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345