摘要: 理解JS的执行顺序。

思考下面 JavaScript 代码:

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");

控制台打印的顺序是怎样的?

答案

正确的答案是:script start, script end, promise1, promise2, setTimeout,但是由于浏览器实现支持不同导致结果也不一致。

Microsoft Edge、Firefox 40、iOS Safari和桌面Safari 8.0.8 打印promise1promise2之前会先打印 setTimeout —— 这似乎是浏览器厂商相互竞争导致的实现不同。这真的很奇怪,因为 Firefox 39 和 Safari 8.0.7 结果总是正确的。

为什么会这样

要理解这一点,需要了解事件循环如何处理任务和微任务。

每个“线程”都有自己的事件循环,因此每个 web worker 都有自己的事件循环,因此可以独立执行,而来自同域的所有窗口共享一个事件循环,所以它们可以同步地通信。

事件循环持续运行,直到清空 Tasks 队列的任务。一个事件循环有多个任务源,这些任务源保证了该源中的执行顺序(比如IndexedDB定义了它们自己的规范),但是浏览器可以在每次循环中选择哪个源来执行任务。这允许浏览器优先选择性能敏感的任务,比如用户输入等。

Tasks 被放到任务源中,这样浏览器就可以从内部进入JavaScript/DOM领域,并确保这些操作按顺序进行。在Tasks 执行期间,浏览器可能更新渲染。从鼠标点击到事件回调需要调度一个任务,解析超文本标记语言也是如此。

setTimeout迟给定的时间,然后为它的回调调度一个新任务。这就是为什么setTimeout在打印script end之后打印,因为打印script end是第一个任务的一部分,而setTimeout在一个单独的任务中。

微任务通常是针对当前执行脚本之后应该立即发生的事情进行调度的,比如对一批操作进行响应,或者在不影响整个新任务的情况下进行异步处理。

只要没有其他JavaScript处于执行中期,并且在每个任务的末尾,微任务队列就在回调之后处理。在微任务期间排队的任何其他微任务都会被添加到队列的末尾并进行处理。微任务 包括 MutationObserver callbacks。例如上面的例子中的 promisecallback

一个settled状态的promise 或者已经变成settled状态(异步请求被settled)的promise,会立刻将它的callback(then)放到微任务队列里面。

这确保了 promise 回调是异步的,即便promise已经变为settled状态。因此一个已settledpromise调用.then(yey,nay)时将立即把一个微任务加入微任务队列中。

这就是为什么promise1promise2会在script end后打印,因为当前运行的脚本必须在处理微任务之前完成。promise1promise2setTimeout之前打印,因为微任务总是在下一个任务之前发生。

好,一步一步的运行:

浏览器之间会有什么不同?

一些浏览器的打印的顺序是 script start, script end, setTimeout, promise1, promise2。它们在setTimeout之后运行promise回调。很可能他们调用promise回调是作为新任务的一部分,而不是作为一个微任务。

这也是可以理解的,因为promise来自 ECMAScript 而不是 HTML。ECMAScript 有“作业”的概念,类似于微任务,但是除了模糊的邮件列表讨论之外,这种关系并不明确。然而,普遍的共识是,promise应该是微任务队列的一部分并且有充足的理由。

promise 看作任务会导致性能问题,因为回调没有必要因为任务相关的事(比如渲染)而延迟执行。它还会由于与其他任务源的交互而导致非确定性,并可能中断与其他api的交互,稍后将详细介绍。

这里有一条 Edge 反馈,它错误地将 promises 当作 任务。WebKit nightly 做对了,所以我认为 Safari 最终会修复,而 Firefox 43 似乎已经修复。

如何判断某些东西是否使用任务或微任务

动手试一试是一种办法,查看相对于promisesetTimeout如何打印,尽管这取决于实现是否正确。

一种方法是查看规范: 将一个任务加入队列: step 14 of setTimeout

将 microtask 加入队列:step 5 of queuing a mutation record

如上所述,ECMAScript 将微任务称为作业: 调用 EnqueueJob 将一个 微任务加入队列:step 8.a of PerformPromiseThen

等级一 boss打怪

下面是一段html代码:

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

给出下面的JS代码,如果点击div.inner将会打印出什么呢?

// 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
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);

在偷看答案前先试一试

试一试

和你猜想的有不同吗?如果是,你得到的结果可能也是正确的。不幸的是,浏览器实现并不统一,下面是各个浏览器下测试结果:

谁是正确的?

调度'click'事件是一项任务。 Mutation observer 和 promise 回调被列为微任务。 setTimeout 回调列为任务。 因此运行过程如下:

所以 Chrome 是对的。对我来说新发现是,微任务在回调之后运行(只要没有其它的 Javascript 在运行),我原以为它只能在一个任务的末尾执行。

浏览器出了什么问题?

对于 mutation callbacks,Firefox 和 Safari 都正确地在内部区域和外部区域单击事件之间执行完毕,清空了微任务队列,但是 promises 列队的处理看起来和chrome不一样。这多少情有可原,因为作业和微任务的关系不清楚,但是我仍然期望在事件回调之间处理 Firefox ticket. Safari ticket.

对于 Edge,我们已经看到它错误的将 promises 当作任务,它也没有在单击回调之间清空微任务队列,而是在所有单击回调执行完之后清空,于是总共只有一个 mutate 在两个 click 之后打印。

等级一 boss打怪升级

仍然使用上面的例子,假如我们运行下面代码会怎么样:

inner.click();

跟之前一样,它会触发 click 事件,但这次是通过 JS 调用的。

试一试

下面是各个浏览器的运行情况:

我发誓我一直在从Chrome中得到不同的结果,我已经更新了这张图表很多次了,我以为我在错误地测试Canary。如果你在Chrome中得到不同的结果,请在评论中告诉我是哪个版本。

为什么不同?

应该是这样的:

所以正确的顺序是:click, click, promise, mutate, promise, timeout, timeout,似乎 Chrome 是对的。

以前,这意味着微任务在侦听器回调之间运行,但.click()会导致事件同步调度,因此调用.click()的脚本仍然在回调之间的堆栈中。 上述规则确保微任务不会中断执行中期的JavaScript。 这意味着我们不处理侦听器回调之间的微任务队列,它们在两个侦听器之后处理。

总结

任务按顺序执行,浏览器可以在它们之间进行渲染:

微任务按顺序执行,并执行:

  • 在每个回调之后,只要没有其它代码正在运行。
  • 在每个任务的末尾。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!

详解JavaScript的任务、微任务、队列以及代码执行顺序的更多相关文章

  1. 详解JavaScript调用栈、尾递归和手动优化

    调用栈(Call Stack) 调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧. 栈帧是指为一个函数调用单独分配的那部分栈空间. 当运行的程序从当前函数调用另外一个函数时 ...

  2. 详解javascript的类

    前言 生活有度,人生添寿. 原文地址:详解javascript的类 博主博客地址:Damonare的个人博客 Javascript从当初的一个"弹窗语言",一步步发展成为现在前后端 ...

  3. 详解Javascript的继承实现(二)

    上文<详解Javascript的继承实现>介绍了一个通用的继承库,基于该库,可以快速构建带继承关系和静态成员的javascript类,好使用也好理解,额外的好处是,如果所有类都用这种库来构 ...

  4. 【转】详解JavaScript中的this

    ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...

  5. js对象详解(JavaScript对象深度剖析,深度理解js对象)

    js对象详解(JavaScript对象深度剖析,深度理解js对象) 这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕 ...

  6. 详解 javascript中offsetleft属性的用法(转)

    详解 javascript中offsetleft属性的用法 转载  2015-11-11   投稿:mrr    我要评论 本章节通过代码实例介绍一下offsetleft属性的用法,需要的朋友可以做一 ...

  7. 详解javascript中的this对象

    详解javascript中的this对象 前言 Javascript是一门基于对象的动态语言,也就是说,所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象.Javascript可以通过一定的 ...

  8. (转载)详解Javascript中prototype属性(推荐)

    在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例.但是在Javascript语言体系中,是不存在类(Class)的概念的,javascript中不 ...

  9. 详解JavaScript数组过滤相同元素的5种方法

    详解JavaScript数组过滤相同元素的5种方法:https://www.jb51.net/article/114490.htm

随机推荐

  1. oracle 死锁 锁

    [zhuan]今天看群里在讨论数据库死锁的问题,也一起研究了下,查了些资料在这里总结下. 所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将 ...

  2. C学习笔记(8)--- 文件读写

    1.C 文件读写: 一个文件,无论它是文本文件还是二进制文件,都是代表了一系列的字节.C 语言不仅提供了访问顶层的函数,也提供了底层(OS)调用来处理存储设备上的文件. a.打开文件: FILE *f ...

  3. mean shift聚类算法的MATLAB程序

    mean shift聚类算法的MATLAB程序 凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 1. mean shift 简介 mean shift, 写的 ...

  4. eslint代码规范检测

    1.如果在 vue init webpack projectname 时选择了eslint(Yes),则   npm uninstall eslint 2.在webpack.base.conf.js里 ...

  5. 剑指Offer-24.二叉树中和为某一值的路径(C++/Java)

    题目: 输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径.路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径.(注意: 在返回值的list中,数组长度大的 ...

  6. 44 dlib鼠标指定目标跟踪

    dlib提供了dlib.correlation_tracker()类用于跟踪目标.官方文档入口:http://dlib.net/python/index.html#dlib.correlation_t ...

  7. 【2019.8.6 慈溪模拟赛 T3】集合(set)(线段树上DP)

    线段树上\(DP\) 首先发现,每个数肯定是向自己的前驱或后继连边的. 则我们开一棵权值线段树,其中每一个节点记录一个\(f_{0/1,0/1}\),表示在这个区间左.右端点是否连过边的情况下,使这个 ...

  8. 【2019.8.12 慈溪模拟赛 T2】汪哥图(wang)(前缀和)

    森林 考虑到题目中给出条件两点间至多只有一条路径. 就可以发现,这是一个森林. 而森林有一个很有用的性质. 考虑对于一棵树,点数-边数=\(1\). 因此对于一个森林,点数-边数=连通块个数. 所以, ...

  9. 58同城笔试题:数组去重;分饼干(分糖果);最小路径和(leetcode64)

    1. 数组去重 题目描述 /** * 有序数组去重 * 输出最终的数字个数 * 输入:1,2,2 * 输出:2 * @author Turing * */ 代码 import java.util.*; ...

  10. decodeURIComponent 解码函数

    想象一个场景,你朋友发一个链接让你打开,但链接是下面其中之一,你会不会想锤死他 1. \u0068\u0074\u0074\u0070\u003a\u002f\u002f\u0062\u0069\u0 ...