js中的微任务与宏任务

导论

先看题目:

question:试着写出下面程序的输出结果:

  console.log(1);
  
  setTimeout(() => {
    console.log(2);
    new Promise((resolve) => {
      console.log(3);
      resolve();
    }).then(res => {
      console.log(4);
    })
    console.log(5);
  }, 0);
  
  new Promise((resolve) => {
    console.log(6);
    resolve();
  }).then(res => {
    console.log(7);
  })
  
  setTimeout(() => {
    console.log(8);
    new Promise((resolve) => {
      console.log(9);
      resolve();
    }).then(res => {
      console.log(10);
    }, 0)
  });
  
  console.log(11);

answer:1 -> 6 -> 11 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8 -> 9 -> 10

宏任务与微任务

在javascript事件循环中,==异步**任务是通过队列(queue)来存储的。流程如下:

javascript任务执行顺序

而异步任务一共分为两种:微任务与宏任务。它们的执行顺序如下:

javascript宏任务与微任务执行顺序

需要注意的是,微任务与宏任务是先注册,再执行。而不是读取到就立即执行。

常见的宏任务(按优先级排列):整体代码script > setImmediate > setTimeout/setInterval

常见的微任务(按优先级排列):process.nextTickNodejs中的内容) > 原生Promise > MutationObserver

回到刚才的那个题目:

  // 主代码块
  console.log(1);
  
  // 注册了一个宏任务
  setTimeout(() => {
    // ...
  }, 0);
  
  // {↓标记↓}
  new Promise((resolve) => {
    // 立即执行部分,其实是同步任务
    console.log(6);
    resolve();
  }).then(res => {
    // 这才是微任务
    console.log(7);
  })
  
  // 注册了一个宏任务
  setTimeout(() => {
    // ...
  });
  // 主代码块
  console.log(11);

所以一开始先执行主代码块,输出1611,请注意,Promise构造函数的参数中的代码是同步任务,与主代码块同步进行。

执行完主代码块后,会寻找微任务队列中的微任务并执行,此时的微任务只有标记的Promise.then()(其他Promsie所在的代码块并未被执行,因此尚未被注册),输出7

微任务执行完后,寻找下一个宏任务 — 第一个SetTimeOut

  setTimeout(() => {
    console.log(2);
    new Promise((resolve) => {
      console.log(3);
      resolve();
    }).then(res => {
      console.log(4);
    })
    console.log(5);
  }, 0);

同理可得:将输出235,并注册一个微任务(Promise.then())。在执行完后执行微任务,输出4

然后再是最后一个宏任务:

 setTimeout(() => {
    console.log(8);
    new Promise((resolve) => {
      console.log(9);
      resolve();
    }).then(res => {
      console.log(10);
    }, 0)
  });

8910,没什么问题了吧,别问,问就同理可得。

西江月·证明
即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。
反之亦然同理,推论自然成立,略去过程Q.E.D ,由上可知证毕。

其实这个题还差了点意思,换做我就这样出:

  console.log(1);
  
  setTimeout(() => {
    console.log(2);
    new Promise((resolve) => {
      console.log(3);
      resolve();
    }).then(res => {
      console.log(4);
    })
    console.log(5);
  }, 0);
  
  new Promise((resolve) => {
    console.log(6);
    resolve();
  }).then(res => {
    console.log(7)
    setTimeout(() => {
      console.log(8);
    });
  })
  
  console.log(9);

猜一下答案输出顺序是什么?
...
...
...
...
...
...
...
...
...
答案是 1 -> 6 -> 9 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8。

拓展

常见的宏任务除了上面提到的那些之外,还有一个不曾提到但非常常见的:IO(输入输出流)。你可以简单理解为事件监听(最常见的表现就是事件监听,不过不仅仅包括事件监听)。

上代码:

/* css */
div {
  border: 1px solid black;
}

#outer {
  padding: 25px;
  width: 50px;
  background-color: aqua;
}

#inner {
  width: 50px;
  height: 50px;
  background-color: green;
}
<!--html-->
<div id="outer">
  <div id="inner"></div>
</div>
// js
let $outer = document.getElementById('outer');

let $inner = document.getElementById('inner');

function handler() {
  console.log('click') // 直接输出
  // 注册微任务
  Promise.resolve().then(_ => console.log('promise'))
  // 注册宏任务
  setTimeout(_ => console.log('timeout'))
  // 注册宏任务
  requestAnimationFrame(_ => console.log('animationFrame'));
  // DOM属性修改,触发微任务
  $outer.setAttribute('data-random', Math.random());
}

// 微任务
new MutationObserver(_ => {
  console.log('observer')
}).observe($outer, {
  attributes: true
})

$inner.addEventListener('click', handler);
$outer.addEventListener('click', handler);
效果图

点击div#inner,输出结果: 'click' -> 'promise' -> 'observer' -> 'click' -> 'promise' -> 'observer' -> 'animationFrame' -> 'animationFrame' -> 'timeout' -> 'timeout'。

让我们来捋一下:

点击时通过事件冒泡触发了宏任务$inner.click(),输出'click'。该任务触发了冒泡事件并注册了宏任务$outer.click()。并在事件处理函数handler中注册了两个宏任务(requestAnimationFramesetTimeout)和一个微任务(Promise),并触发了一个微任务MutationObserver

该宏任务执行完后,微任务队列中有两个微任务,按优先级先执行Promise,所以依次输出'promise','observer'。

下一个宏任务,即$outer.click()开始执行,重复了$inner.click()的事件,区别是它没有冒泡并注册新任务了。依次输出'click','promise','observer'。

至此,只剩下几个宏任务对线了。

值得注意的是,因为requestAnimationFrame会触发页面重绘(不是优先级哦),进而会导致setTimeout重置,所以前者会比后者先输出。

后记

祝你学习愉快。

参考文章

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

推荐阅读更多精彩内容