javaScript 之 蚁人<微任务>

1,266 阅读6分钟

每次我在写技术类文章的时候都喜欢用引用一个神话故事或者一位超级英雄。没错,因为我的中二病很严重,写代码的时候都幻想自己有一对机械手臂帮我在那啪啪啪的调试bug,别想歪了不是那种啪啪啪。 这次我要说的就是 蚁人

好吧,为什么要说蚁人那。如果你看过漫威(虽然我是DC粉)的超级英雄电影你应该知道蚁人的能力。

变小 🐜 --- 变小 🐜 --- 变小 🐜--- 变小 🐜 --- 变小 🐜

变大 🐜 --- 变大 🐜 --- 变大 🐜--- 变大 🐜 --- 变大 🐜


牛叉吹完,开始正题

蚁人的变大我认为就是宏任务

蚁人的变小我认为就是微任务

为什么这么说那?因为你想在<美队3>里蚁人内战之中变得很大,但是问题就来了,变大之后虽然力量有了加成,但是速度变得很慢,我现在不说你应该猜出宏任务的缺点了吧? 同理,蚁人变小之后灵巧了不少,可以跟很多蚂蚁沟通,这些蚂蚁井然有序帮助蚁人,是不是这些小🐜很像微任务,那么微任务的好处你也猜到了吧?

我们来引入谷歌的一位大神 Jake 的文章作为说明,原文标题:《Tasks, microtasks, queues and schedules》原文地址:Tasks, microtasks, queues and schedules

首先看一段代码:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

打印顺序是什么?(ctrl+A查看答案)

正确答案是: script start, script end, promise1, promise2, setTimeout

这是为什么那?

我稍稍解释下,既然我说了,宏任务是变大那么效率肯定低,微任务反之肯定是有序进行效率肯定要高一些,那么我们能看出,SetTimeout肯定是效率低的。

因为SetTimeout等于在开一条多线程干另一件事,那么SetTimeout是宏任务无疑的。与之相反的异步处理方式当然是微任务了。

也就是说Promise对象返回的状态给了then,then就是那些小蚂蚁,这些小蚂蚁会有条不紊的工作下去,.then().then().then().then()的链式调用,只要你愿意就会无穷无尽。

那么这个问题好解了,所以 'script start'肯定会被先打印出来,script end随后打印出来,SetTimeout先往后放因为效率低,然后'promise1'打印,'promise2'打印 两个微任务执行完毕,最后安排完小蚂蚁后蚁人变大 触发SetTimeout最后执行。

为什么会这样那?

因为SetTimeout这种宏任务很容易在触犯的时候关联着DOM元素的变化,SetTimeout不能自己完成需要等待DOM元素修改后的结果,这样每次触发任务之后还要关联DOM元素的变化,效率肯定要低很多。promise就不一样了,每次执行的微任务都被蚂蚁排着队处理了,每个蚂蚁都有条不紊的一个接一个进行处理,这些蚂蚁就形成了一个任务队列。也就是说大家执行的任务都是有顺序的,如果在执行任务的过程中有新的微任务产生就往后排,等待前面的蚂蚁执行完毕,再去执行新的任务。

但是理想是墙上的美女,现实是炕上的媳妇... 总有些东西会无情的打你脸...

理论是这个理论,实践却是另一种实践。有的浏览器打印结果就是不一样,啪啪啪打的响不响?

那为什么那些浏览器打印顺序不一样咧?

有些浏览会会打印出: script start, script end, setTimeout, promise1, promise2。

他们会在setTimeout之后执行promise的回调,就好像这些浏览器会把promise的回调视作一个新的宏任务而不是微任务。

其实无可厚非,因为promises 来自于ECMAScript 的标准而不是HTML标准。 ECMAScript 有个关于jobs的概念和微任务挺类似的,但是否明确具有关联关系却尚未定论(相关讨论)。然而,普遍的观点是promise应该属于微任务。

简单来说就是各个浏览器厂商大哥相互之间达不成共识,结果对宏任务和微任务的理解各有差异,这点不仅浏览器,就连node的打印结果和浏览器之间的版本都不一定想通!

来点官方的说法吧~我是不愿意看啊

如果说把 promise 当做一个新的 task 来执行的话,这将会造成一些性能上的问题,因为 promise 的回调函数可能会被延迟执行,因为在每一个 task 执行结束后浏览器可能会进行一些渲染工作。由于作为一个 task 将会和其他任务来源(task source)相互影响,这也会造成一些不确定性,同时这也将打破一些与其他 API 的交互,这样一来便会造成一系列的问题。

继续看下面的代码 一段容器

<div class="outer">
  <div class="inner"></div>
</div>

接着看触发的任务

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
//监听element属性变化
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

偷看答案前先试一试啊,tips:日志可能出现多次哦。 结果如下:

  • click
  • promise
  • mutate
  • click
  • promise
  • mutate
  • timeout
  • timeout 你猜对了吗。你可能猜对了,但是许多浏览器却不这样觉得。 https://segmentfault.com/img/bVbaQYd?w=610&h=340

哪个是对的? 分发click event是一个宏任务,Mutation observer和promise都会进入微任务队列,setTimeout回调是一个宏任务,建议原文观看 DEMO

所以chrome是对的,我之前也不知道只要执行栈中没有js代码在执行,微任务会在回调后立即执行,我之前认为它只会在宏任务结束后执行(Although we are mid-task,microtasks are processed after callbacks if the stack is empty).这个规则来自于HTML标准中关于回调调用的部分

If the stack of script settings objects is now empty, perform a microtask checkpoint — HTML: Cleaning up after a callback step 3

浏览器哪里出错了?

Firefox和Safari在click监听器回调之间正确执行了mutation 回调的微任务,但promise打印结果却出现在了错误的位置。 无可厚非的是jobs和微任务的关系太含糊不清,不过我仍认为应该在click监听器回调之间执行。 Edge我们早就知道会把promise回调放进错误的队列,但他也也没在click监听器回调之间执行微任务队列,而是在所有监听器回调后执行,这打印click之后只打印了一次muteta,因此这是Edge的一个bug。

然后可以总结了,我个人觉得结果以chrome的结果作为标准就可以。

宏任务

  • 效率低,会按顺序执行,同时会影响界面DOM的渲染。
  • 效率高,所有微任务也按顺序执行,且在以下场景会立即执行所有微任务。
    • 每个回调之后且js执行栈中为空(小蚂蚁的活都分配完了)。
    • 每个宏任务结束后(变大之后又变小了,接着给小蚂蚁分配任务)。

今天的牛逼到此结束,等着换下一个牛逼吧,啪啪啪 👏👏👏