事件轮询中的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完成会浪费大量的时间.有几种方法可以处理这种性能上的影响: 同步:每次处理一个请求,依次处理.优点:简单:缺点:任何一个请求都 ...
随机推荐
- 关于在jsp页面中使用/struts-tags标签库的迭代器时,从ValueStack获取参数使用EL的问题
情况复原下: 通过valuestack存了个user,然后页面里可以${user.sex}拿到男.${user.sex=='男'}拿到ture 但是在使用 <s:if text="${ ...
- Netty4 学习笔记之一:客户端与服务端通信 demo
前言 因为以前在项目中使用过Mina框架,感受到了该框架的强大之处.于是在业余时间也学习了一下Netty.因为Netty的主要版本是Netty3和Netty4(Netty5已经被取消了),所以我就直接 ...
- 【个人笔记】《知了堂》node.js简介及创建应用
Node.js是什么? Node.js是建立在谷歌Chrome的JavaScript引擎(V8引擎)的Web应用程序框架.Node.js自带运行时环境可在Javascript脚本的基础上可以解释和执行 ...
- oracle pl/sql 基础
一.pl/sql developer开发工具pl/sql developer是用于开发pl/sql块的集成开发环境(ide),它是一个独立的产品,而不是oracle的一个附带品. 二.pl/sql介绍 ...
- 解决linux下部署科大讯飞时的版本过低问题
在将项目部 署到阿里云服务器之后,需要将科大讯飞SDK下的libmsc64.so依赖库文件上传至/usr/java/jdk1.8.0_121/jre/lib/amd64路径下 在Windo ...
- C++Builder中MessageBox的基本用法
C++Builder中MessageBox的基本用法 返回值:IDYES=Application->MessageBox("","",MBYESNO) i ...
- Free Goodies UVA - 12260 贪心
Free Goodies Time Limit: 3000MS Memory Limit: Unknown 64bit IO Format: %lld & %llu [Submit ...
- 1289 大鱼吃小鱼 1305 Pairwise Sum and Divide 1344 走格子 1347 旋转字符串 1381 硬币游戏
1289 大鱼吃小鱼 有N条鱼每条鱼的位置及大小均不同,他们沿着X轴游动,有的向左,有的向右.游动的速度是一样的,两条鱼相遇大鱼会吃掉小鱼.从左到右给出每条鱼的大小和游动的方向(0表示向左,1表示向右 ...
- 57、Bootstrap中文文档
给大家介绍一个前端框架让你从此写起前端代码与之先前相比如有神助般的效果拉就是Bootstrap. 一.Bootstrap的下载 Bootstrap,由Twitter的设计师Mark Otto和Jaco ...
- Parallel.Invoke 并行的使用
Parallel类 在System.Threading.Tasks 命名空间下 下面有几个方法,这里讲一下Invoke的用法 下面我们定义几个方法方便测试 先自定义Response 防止并行的时候占 ...