事件轮询中的task与microtask
event loop
网上看到的一篇文章,关于介绍task和Tasks, microtasks, queues and schedules,尝试简单翻译一下写进来吧!
原文地址:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
当我跟我同事Matt Gaunt讲,我要写一篇关于microtask和浏览器事件轮询的文章的时候,他说:“你尽管写,反正我不看。”好吧,不看就算了,但我还是要写,总有人会看的。
事实上, Philip Roberts已经对这方面的知识做了一个很完整的介绍,尽管没有包含microtasks,但是其他的基本上都有。好了,我要开始我的表演了!
考虑下面的代码:
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 and desktop Safari 8.0.8中,setTimeout可能会在promise1和promise2之前打印-看起来就像是在竞争。看起来很奇怪,一般都是正确打印的。
这是为啥呢?
想要理解这个,必须先了解事件轮询中的tasks与microtasks。这里面包含不少知识,第一次接触这个可能会让你脑阔疼,请深呼吸:
每一个‘线程’都有它独立的事件轮询,所以每个页面都可以各自工作,执行它们自己的代码。所有一个来源的窗口都共享同一个事件轮询,彼此之间同步交流信息。事件轮询不断的运转,执行所有的任务队列。一个事件轮询中的任务可能来源于多个地方,需要保证所有任务按正确的顺序执行并不简单,但是浏览器会帮忙选择如何执行这些任务。这样一来,浏览器可以对一些影响性能的操作(如用户输入)做特殊处理。跟上!
Tasks已经被提前排好序,保证了浏览器可以持续从内部取出它们并弄到JS/DOM中执行。在两个任务的执行空隙,浏览器可能会重新渲染视图。在解析HTML页面的时候,鼠标点击事件与对应回调函数会产生一个新的task,同时会产生事件序列的还有上面的例子:setTimeout。
setTimeout会延迟指定的时间,然后将回调函数加入任务序列中。这就是为什么setTimeout会在script end后面打印。script end的打印属于第一个任务序列的一部分,而setTimeout则在下一个任务序列中被打印。OK,这里基本上没问题了,希望下一个环节你还能坚持住……
Microtasks通常在JS当前主任务执行完后直接执行,比如说对一些特殊事件作出响应,或者在不影响主线程情况下异步执行某些事件。一旦没有其他JS代码在执行中,microtask队列会立即执行,执行过程中如果有microtask插入,也同时会被执行。microtas包括mutation observer回调,与上面例子中的promise回调。
一旦一个promise被决议,在决议后就会形成一个microtask来响应回调函数。这个可以保证promise的即使被决议,回调函数也会被异步执行。因此,调用then(rel,rej)方法后会立即生成一个microtask队列。这也就是为什么promise1和promise2在script end后面打印,microtask必须在当前JS代码运转完后才会被操作。promise1和promise2在setTimeout之前打印,也就是microtask永远在下一个task之前执行。
这样上面的例子就很清晰了:
//执行JS主代码
console.log('script start');
//等待下一轮task
setTimeout(function() {
console.log('setTimeout');
}, 0);
//then方法产生microtask
Promise.resolve().then(function() {
console.log('promise1');
//又插入一个microtask 立即执行
//执行完后进行下一轮task
}).then(function() {
console.log('promise2');
});
//JS主代码 第一轮task执行完会执行microtask
console.log('script end');
如同注释所说的那样,一步一步得到了最后的结果。
为什么不同浏览器会出现差异?
有些浏览器会打印script start,start end,setTimeout,promise1,promise2。promise的回调函数在setTimeout之后执行,看起来似乎将promise当成下一轮task而不是microtask。
某种程度上可以理解这件事,promise来自于ECMA标准而不是HTML。ECMA标准中有'jobs'的概念,跟microtasks很相似,然而,仅仅通过一些类似邮件的讨论,这两者的区别并不是那么清晰。但是一般来说,都公认promise应该是microtask的一部分,而且确实比较好。
promise一般用来解决性能问题,有些回调函数可能会因为渲染之类的事件导致延迟执行。(后面的没太看懂)
如何判定这是一个task还是一个microtask
测试是一种方式。看看setTimeout和promise的打印顺序,当然保证结果是正确。
比较稳妥的方式是看说明文档。(举的例子会跳转到一个新页面)
稍微提一下,在ECMA标准中,microtask被叫做‘jobs’。在step 8.a of PerformPromiseThen
中,排入队列被称为生成一个microtask序列。
现在来看一个更加复杂的案例:
Lv1 BOSS
写这部分之前,我先给点易错的案例。
看看下面这一段html:
<div class="outer">
<div class="inner"></div>
</div>
JS代码如下:
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0); Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
如果点击div.inner,会打印出什么呢?
在查看答案之前自己分析一下。(提示:有些东西会不止打印一次)
答案不一样?或许你是对的,因为不同浏览器打印的不一样。
Chrome:click,promise,mutate,click,promise,mutate,timeoue,timeoue
Firefox:click,mutate,click,mutate,timeoue,promise,promise,timeoue
Safari:click,mutate,click,mutate,promise,promise,timeout,timeout
IE:click,click,mutate,timeout,promise,timeout,promise
哪一个是对的?
触发的click事件是一个task。Mutation observer和promise的回调函数是microtask。setTimeout是另外一个task。所以顺序这样是这样的;
new MutationObserver(function() {
//紧跟在promise后面的microtask
console.log('mutate');
}).observe(outer, {
attributes: true
});
function onClick() {
//click第一个task
console.log('click');
//第二个task
setTimeout(function() {
console.log('timeout');
}, 0);
//promise产生一个microtask
Promise.resolve().then(function() {
console.log('promise');
});
//这句代码也会产生一个microtask
outer.setAttribute('data-random', Math.random());
}
过程大概是这样的:点击div.inner,click(第一个task)->timeout(第二个task)->promise(microtask)->mutate(microtask)。
按照之前所描述的顺序:task->microtask->task,可以得到click,promise,mutate,timeout。但是由于冒泡的关系,外层div也会触发一遍上面的流程,所以最终结果是click,promise,mutate,click,promise,mutae,timeout,timeout。
因此,Chrome是正确的。有一个地方对我来说很新鲜,microtask的回调会在没有其余运行中JS代码后执行,我理解为task的尾部。下面是HTML文档中对回调的说明:
如果栈中JS环境对象为空,会执行microtask队列的检查。
—HTML:Cleaning up after a callback
microtask的检查包含:遍历microtask队列直到全部被执行。
ECMA标准把这个称为jobs:
只有当前环境没有任何东西在执行并且执行环境栈为空,job才能开始被执行。
—ECMAScript:Jobs and Job Queues
在HTML环境中,'能'变成了'必须'。
为什么浏览器会出错?
Firefox和Safari可以正确的区别microtask与click事件,比如mutation的回调函数,但是promise的处理不太一样。这个顺序会出现问题也是情有可原的,因为关于job和microtask之间区别非常模糊,我认为这两个在事件回调之间执行比较合理。 Firefox ticket. Safari ticket(这里是两个相关bug讨论链接)
至于Edge,它对promise的处理错的一塌糊涂,同时也未在两个监听事件之间执行microtask队列,等监听事件都完事了才调用microtask,并且两个click事件只打印了一次mutate。Bug ticket
Lv1 BOSS愤怒的哥哥
代码跟上面的一样,但是执行的代码变成了:
inner.click();
这个也会触发同样的事件,但是方式不是通过点击,而是直接用JS代码执行。
答案如下:
Chrome:click,click,promise,mutate,promise,timeout,timeout
Firefox:click,click,mutate,timeout,promise,promise,timeout
Safari:click,click,muate,promise,promise,timeout,timeout
IE:click,click,mutate,timeout,promise,timeout,promise
Chrome每次都会出现不同的结果,我专门弄了一个表来记录我测试出来的错误。如果你在Chrome中得到不一样的结果,在评论中告诉我版本号。
为啥不一样?
来梳理一下流程。
首先这里有一个不一样的地方,即之前提到的:这里是执行JS代码触发函数,不是事件触发。所以这里的顺序是task(执行JS代码)->task(onClick函数)->打印click->timeout(第二个task)->promise(microtask)->mutate(microtask)->打印click(冒泡)->timeout(第三个task)->promise(microtask),mutate只会触发一次(不太懂原理),主要区别在于在冒泡的时候,JS代码仍在执行,所以说microtask不会执行,必须等到第二个click打印才会触发。最后正确的结果是click,click,promise,mutate,promise,timeout,timeout,看起来Chrome又对了。
microtask在两个事件监听触发后被调用。
之前,microtask在监听回调之间执行,但是通过JS代码的函数调用,导致事件同步执行了,第一个回调结束后,JS主线程依旧在栈中。上述规则保证了microtask不打断JS主线程执行。这意味着这种情况下,microtask不能在监听回调之间执行,而需要在之后。
这没问题吗?
可能你会在一些地方还存在疑惑。我曾经在试图创建a simple wrapper library for IndexedDB that uses promises遇到过这个问题,它比IDBRequest对象还要奇怪。这同时也让IDB变得好玩起来。almost makes IDB fun to use.
当IDB成功执行一个事件,相关的事务对象变得没那么活跃了(transaction object becomes inactive after dispatching没看懂)。如果在事件执行期间创建了一个promise,该回调会在step4(?)之前执行,此时相关事件仍然在执行,这个现象只会在Chrome中出现,对渲染库有一点没用。
在Firefox中你可以变通解决这个问题,因为promise的polyfill是用mutation observers实现的,正确的实现了microtask。Safari对这两个microtask一直处于纠结状态。不幸的是,IE/Edge中也会有问题,mutation不会在回调后执行。
希望能从这些问题中找到一些共同点。
额
总结一下:
· task按顺序执行,浏览器可能在周期间隙里渲染视图
· microtask也是按顺序执行,遵循下列规则:
1.JS主线程没有程序执行
2.主程序的尾部
希望现在你能明白关于事件轮询的相关概念,至少会分析执行顺序。
有人在看么。。。(作者原话)
鸣谢A,B,C……
事件轮询中的task与microtask的更多相关文章
- JS中的异步以及事件轮询机制
一.JS为何是单线程的? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊.(在JAVA和c#中的异步 ...
- node.js中的事件轮询Event Loop
任务队列/事件队列 "任务队列"是一个事件的队列,IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈" ...
- 理解JavaScript中的事件轮询
原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 为什么JavaScript是单线程 JavaScript语言的一大特点就是单线程,也 ...
- nodejs事件轮询详述
目录 概述 nodejs特点 事件轮询 关于异步方法 概述 关于nodejs的介绍网上资料非常多,最近由于在整理一些函数式编程的资料时,多次遇到nodejs有关的内容.所以就打算专门写一篇文章总结一下 ...
- node.js事件轮询(1)
事件轮询(引用) 事件轮询是node的核心内容.一个系统(或者说一个程序)中必须至少包含一个大的循环结构(我称之为"泵"),它是维持系统持续运行的前提.nodejs中一样包含这样的 ...
- 理解Node.js的事件轮询
前言 总括 : 原文地址:理解Node.js的事件轮询 Node小应用:Node-sample 智者阅读群书,亦阅历人生 正文 Node.js的两个基本概念 Node.js的第一个基本概念就是I/O操 ...
- 对Node.JS的事件轮询(Event Loop)的理解
title: Node.JS的事件轮询(event loop)的理解 categories: 理解 tags: Node JS 机制 当我们知道I/O操作和创建新线程的开销是巨大的! 网站延迟的开销 ...
- Node.js的异步IO和事件轮询
想象一下,以前我们在写程序时, 如果程序在I/O上阻塞了,当有更多请求过来时,服务器会怎么处理呢?在这种情景中通常会用多线程的方式.一种常见的实现是给每个连接分配一个线程,并为那些连接设置一个线程池 ...
- 【译】理解node.js事件轮询
Node.js的第一个基本论点是I/O开销很大. 当前编程技术中等待I/O完成会浪费大量的时间.有几种方法可以处理这种性能上的影响: 同步:每次处理一个请求,依次处理.优点:简单:缺点:任何一个请求都 ...
随机推荐
- 由throw new Error() 引发的探讨
问题复现 在工作时遇到了需要抛出异常并且需要自己捕获处理的地方,于是在抛出的地方写下 function parseExcel(con) { try { // doSomething } catch ( ...
- GitHub新手使用教学(从安装到使用)
一.安装Git for Windows(又名msysgit) 下载地址: https://git-for-windows.github.io/ 在官方下载完后,安装到Windows Explore ...
- 微信开发(2)–获取access_token
一.access_token access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token.开发者需要进行妥善保存.access_token的存储至少要保 ...
- 【转】开源中国上看到的一个vim的自动配置的好东西,分享下
https://www.oschina.net/p/onekey-to-vim-ide 变量有高亮,竖行上有直线定位,对python的支持效果更佳,从事C/C++开发的程序员使用也不错.
- Tree--RedBlackTree详解(2 - 3 - 4Tree)(红黑树)
#topics h2 { background: #2B6695; color: #FFFFFF; font-family: "微软雅黑", "宋体", &qu ...
- Redis介绍——Linux环境Redis安装全过程和遇到的问题及解决方案
一:redis的入门介绍: 首先贴出官网; 英文:https://redis.io/ 中文:http://www.redis.cn/ 1.是什么 --REmote DIctionary Server( ...
- 详解MySQL基准测试和sysbench工具
前言 作为一名后台开发,对数据库进行基准测试,以掌握数据库的性能情况是非常必要的.本文介绍了MySQL基准测试的基本概念,以及使用sysbench对MySQL进行基准测试的详细方法. 文章有疏漏之处, ...
- GBDT(MART)概念简介
GBDT(Gradient Boosting Decision Tree) 又叫 MART(Multiple Additive Regression Tree),是一种用于回归的机器学习算法,该算法由 ...
- selenium webdriver 启动三大浏览器Firefox,Chrome,IE
selenium webdriver 启动三大浏览器Firefox,Chrome,IE 1.安装selenium 在联网的情况下,在Windows命令行(cmd)输入pip install selen ...
- Java总结篇:Java多线程
Java总结篇系列:Java多线程 多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: ...