摘要: 理解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. [20191012]组成rowid.txt

    [20191012]组成rowid.txt --//昨天做了拆分rowid的测试,链接http://blog.itpub.net/267265/viewspace-2659613/=>[2019 ...

  2. SQL Server 完整备份遇到的一个不常见的错误

    1. 错误详情 有一次在手动执行数据库完整备份时遇到如下错误: 执行多次都是这个错误信息. 提示无法生成检查点,原因可能是由于系统资源(如磁盘或内存空间)不足或者有时是由于数据库损坏而造成的. 我们检 ...

  3. centos7中python3.6报错ModuleNotFoundError: No module named '_ssl' 或者 Max retries exceeded with url: / (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.",))

    如果在运行爬虫时报此错:requests.exceptions.SSLError: HTTPSConnectionPool(host='www.baidu.com', port=443): Max r ...

  4. Python—网络抓包与解包(pcap、dpkt)

    pcap安装 [root@localhost ~]# pip install pypcap 抓包与解包 # -*- coding:utf-8 -*- import pcap, dpkt import ...

  5. c# 第32节 类的继承

    本节内容: 1:为什么要继承 2:继承特点 3:继承的实现 4:子类传统构造,与base构造 1:为什么要继承 2:继承特点 什么是继承: 继承就是子类包含父类的数据结构和行为方式, 包括字段.属性. ...

  6. pytroch 权重初始化和加载词向量的方法

    1.几种不同的初始化方法 import torch.nn as nn embedding = torch.Tensor(3, 5) #如下6种初始化方法 #正态分布 nn.init.normal_(e ...

  7. day55_9_19模型层的操作

    一.配置settings. 如果是queryset对象 那么可以点query直接查看该queryset的内部sql语句 将以下代码放入settings中.就可以实现当使用orm时查看sql语句: LO ...

  8. git 入门教程之 git bash 竟然不支持 tree 命令

    开门见山 git bash 是 Windows 用户安装 git 时默认安装的命令行工具,不仅界面漂亮功能也不错,大多数情况下可以替代 Windows 原生的 cmd 命令行. 然而,git bash ...

  9. bzoj2115 Xor

    题目链接 problem 考虑一个边权为非负整数的无向连通图,节点编号为\(1\) 到 \(N\),试求出一条从 \(1\) 号节点到 \(N\) 号节点的路径,使得路径上经过的边的权值的 \(XOR ...

  10. AGC008E Next or Nextnext(组合计数,神奇思路)

    神仙题. 排列计数,一种常见的做法是 \(i\) 向 \(p_i\) 连边. 然而这里这个就逼迫我们只能从 \(i\) 向 \(a_i\) 连边. 不过没关系,考虑从 \(i\) 向 \(p_i\) ...