Open aooy opened 7 years ago
microtask 是个好东西
用户代理可以理解为浏览器实现吗
@Hubu 我感觉可以这么理解
感觉理解浏览器运行机制又深刻了一些,谢谢你的好文!
运行microtask的条件是 上下文执行盏为空的时候,也就是在运行task之后,更新渲染之前。
settimeout属于task,promise属于microtask 照这样 settimeout 0秒要早promise执行了,我整体这么理解下来 有些懵,求解答 不知道可否加个微信或者qq,等你闲了 我请教你
@webkonglong 文章涉及到规范,所以有些生涩,建议可以先看一些通熟易懂的介绍task和microtask的文章。 我在文中有一个实例,列出了每次循环task队列和microtask的变化,你所说的这个问题要弄清楚,首先得理解这轮event loop的task是哪个任务,其次setTimeout它只是一个task任务源,并不会立即执行,它只是将一个setTimeout任务插进task队列,得排到它,它里面的函数才会执行。Promise.then是microtask任务源,会将任务插进microtask队列。
Promise.resolve().then(function promise1 () {
console.log('promise1');
})
setTimeout(function setTimeout1 (){
console.log('setTimeout1')
}, 0)
以上面的例子来说,这两个api仅仅是将相应的任务插进他们各自的队列中,此次event loop执行的task并不是setTimeout里的任务,setTimeout的任务排在后边了,还没轮到它。microtask队列的任务是会在当轮清空的,所以会看到promise1先于setTimeout1执行。
好文!想问一下浏览器为何要区分task和microtask呢?
@zhanba 感觉选择eventloop这个异步模型,自然就得分出两种task才合理,一种是在当轮eventloop执行的,一种是往后某轮才执行的。如果没有microtask就没法在当轮eventloop里添加异步操作了,有点像人的左右手吧,少了一个就不完整了。
讲的太好了。。看过讲event loop最舒服的文章 尤其是用了大量的例子去验证浏览器渲染,一直想看这部分,今天终于如愿以偿 感谢作者
请问将任务放到队列的操作是在哪里完成的?
疑问 or Bug
关于 文中 一句 script里的代码被列为一个task,放入task队列。
不知道这句是根据什么推测来的, 难道 script 也是一个 task 源?
说下个人的理解,不当之处请指正
HTML-Standard 中 8.1.3.4 一节 (calling-scripts)[https://html.spec.whatwg.org/#calling-scripts] 根据第九步 clean up after running script 当执行栈为空时, perform a microtask checkpoint,
我认为步骤应该是这样: tick1: 1、 queue a microtask promise1 task queue [], microtask queue [promise1] 2、 queue a task setTimeout1 task queue [setTimeout1], microtask queue [promise1] 3、 queue a task setTimeout2 task queue [setTimeout1, setTimeout2], microtask queue [promise1]
此刻 execution context stack 为空 则 perform a microtask checkpoint 执行所有 microtask queue 里的microtask , 也就是 执行 microtask promise1, 执行完毕后 queue 是这样的 task queue [setTimeout1, setTimeout2], microtask queue []
tick2: 执行 task queue 中 oldest task, 也就是 setTimeout1, 同时 queue a microtask promise2, 执行完毕后 task queue [setTimeout2], microtask queue [promise2] 接着 perform a microtask checkpoint, 执行 microtask queue 中的所有microtask , 此时 也就是要执行 promise2 , 执行完毕后 task queue [setTimeout2], microtask queue []
tick3: 执行 task queue 中 oldest task, 也就是 setTimeout2 接着perform a microtask checkpoint 此时 microtask queue 为空。 执行完毕后
task queue [] microtask queue []
很棒的文章。纠正一点
我们都知道javaScript是单线程,渲染计算和脚本运行共用同一线程(网络请求会有其他线程),导致脚本运行会阻塞渲染。
渲染计算应该是浏览器GUI渲染线程负责,是由浏览器用c++编写的模块负责的。GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
@jimczj 学习了,谢谢指正。
@dreamdevil00 个人理解, Githubissues.
作者:杨敬卓
转载请注明出处
异步的思考
event loops隐藏得比较深,很多人对它很陌生。但提起异步,相信每个人都知道。异步背后的“靠山”就是event loops。这里的异步准确的说应该叫浏览器的event loops或者说是javaScript运行环境的event loops,因为ECMAScript中没有event loops,event loops是在HTML Standard定义的。
event loops规范中定义了浏览器何时进行渲染更新,了解它有助于性能优化。
思考下边的代码运行顺序:
上面的顺序是在chrome运行得出的,有趣的是在safari 9.1.2中测试,promise1 promise2会在setTimeout的后边,而在safari 10.0.1中得到了和chrome一样的结果。为何浏览器有不同的表现,了解tasks, microtasks队列就可以解答这个问题。
很多框架和库都会使用类似下面函数:
初次看这个useMutationObserver函数总会很有疑惑,
MutationObserver
不是用来观察dom的变化的吗,这样凭空造出一个节点来反复修改它的内容,来触发观察的回调函数有何意义?答案就是使用
Mutation事件
可以异步执行操作(例子中的flush函数),一是可以尽快响应变化,二是可以去除重复的计算。但是setTimeout(flush, 0)
同样也可以执行异步操作,要知道其中的差异和选择哪种异步方法,就得了解event loop。定义
先看看它们在规范中的定义。
Note:本文的引用部分,就是对规范的翻译,有的部分会概括或者省略的翻译,有误请指正。
event loop
event loop翻译出来就是事件循环,可以理解为实现异步的一种方式,我们来看看event loop在HTML Standard中的定义章节:
第一句话:
事件,用户交互,脚本,渲染,网络这些都是我们所熟悉的东西,他们都是由event loop协调的。触发一个
click
事件,进行一次ajax
请求,背后都有event loop
在运作。task
task也被称为macrotask,task队列还是比较好理解的,就是一个先进先出的队列,由指定的任务源去提供任务。
哪些是task任务源呢?
规范在Generic task sources中有提及:
task任务源非常宽泛,比如
ajax
的onload
,click
事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作(IndexedDB ),需要注意的是setTimeout
、setInterval
、setImmediate
也是task任务源。总结来说task任务源:microtask
microtask 队列和task 队列有些相似,都是先进先出的队列,由指定的任务源去提供任务,不同的是一个 event loop里只有一个microtask 队列。
HTML Standard没有具体指明哪些是microtask任务源,通常认为是microtask任务源有:
NOTE: Promise的定义在 ECMAScript规范而不是在HTML规范中,但是ECMAScript规范中有一个jobs的概念和microtasks很相似。在Promises/A+规范的Notes 3.1中提及了promise的then方法可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。所以开头提及的promise在不同浏览器的差异正源于此,有的浏览器将
then
放入了macro-task队列,有的放入了micro-task 队列。在jake的博文Tasks, microtasks, queues and schedules中提及了一个讨论vague mailing list discussions,一个普遍的共识是promises属于microtasks队列。进一步了解event loops
知道了
event loops
大致做什么的,我们再深入了解下event loops
。反复提到的一个词是browsing contexts(浏览器上下文)。
结合一些资料,对上边规范给出一些理解(有误请指正):
event loop
。event loop
,browsing contexts
和web workers
就是相互独立的。browsing contexts
可以共用event loop
,这样它们之间就可以相互通信。event loop的处理过程(Processing model)
在规范的Processing model定义了
event loop
的循环过程:event loop会不断循环上面的步骤,概括说来:
event loop
会不断循环的去取tasks
队列的中最老的一个任务推入栈中执行,并在当次循环里依次执行并清空microtask
队列里的任务。microtask
队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ的频率更新视图)microtasks检查点(microtask checkpoint)
event loop
运行的第6步,执行了一个microtask checkpoint
,看看规范如何描述microtask checkpoint
:microtask checkpoint
所做的就是执行microtask队列里的任务。什么时候会调用microtask checkpoint
呢?执行栈(JavaScript execution context stack)
task和microtask都是推入栈中执行的,要完整了解event loops还需要认识JavaScript execution context stack,它的规范位于https://tc39.github.io/ecma262/#execution-context-stack。
javaScript是单线程,也就是说只有一个主线程,主线程有一个栈,每一个函数执行的时候,都会生成新的
execution context(执行上下文)
,执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中, running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。举个简单的例子:
执行过程中栈的变化:
完整异步过程
规范晦涩难懂,做一个形象的比喻: 主线程类似一个加工厂,它只有一条流水线,待执行的任务就是流水线上的原料,只有前一个加工完,后一个才能进行。event loops就是把原料放上流水线的工人。只要已经放在流水线上的,它们会被依次处理,称为同步任务。一些待处理的原料,工人会按照它们的种类排序,在适当的时机放上流水线,这些称为异步任务。
过程图:
举个简单的例子,假设一个script标签的代码如下:
运行过程:
script里的代码被列为一个task,放入task队列。
循环1:
循环2:
循环3:
event loop中的Update the rendering(更新渲染)
这是event loop中很重要部分,在第7步会进行Update the rendering(更新渲染),规范允许浏览器自己选择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图。
https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork 这篇文章较详细的讲解了渲染机制。
渲染的基本流程:
Note: 可以看到渲染树的一个重要组成部分是CSSOM树,绘制会等待css样式全部加载完成才进行,所以css样式加载的快慢是首屏呈现快慢的关键点。
下面讨论一下渲染的时机。规范定义在一次循环中,Update the rendering会在第六步Microtasks: Perform a microtask checkpoint 后运行。
验证更新渲染(Update the rendering)的时机
不同机子测试可能会得到不同的结果,这取决于浏览器,cpu、gpu性能以及它们当时的状态。
例子1
我们做一个简单的测试
用chrome的Developer tools的Timeline查看各部分运行的时间点。 当我们点击这个div的时候,下图截取了部分时间线,黄色部分是脚本运行,紫色部分是更新render树、计算布局,绿色部分是绘制。
绿色和紫色部分可以认为是Update the rendering。
在这一轮事件循环中,setTimeout1是作为task运行的,可以看到paint确实是在task运行完后才进行的。
例子2
现在换成一个microtask任务,看看有什么变化
和上一个例子很像,不同的是这一轮事件循环的task是click的回调函数,Promise1则是microtask,paint同样是在他们之后完成。
标准就是那么定义的,答案似乎显而易见,我们把例子变得稍微复杂一些。
例子3
当点击后,一共产生3个task,分别是click1、setTimeout1、setTimeout2,所以会分别在3次event loop中进行。 下面截取的是setTimeout1、setTimeout2的部分。
我们修改了两次textContent,奇怪的是setTimeout1、setTimeout2之间没有paint,浏览器只绘制了textContent=1,难道setTimeout1、setTimeout2在同一次event loop中吗?
例子4
在两个setTimeout中增加microtask。
从run microtasks中可以看出来,setTimeout1、setTimeout2应该运行在两次event loop中,textContent = 0的修改被跳过了。
setTimeout1、setTimeout2的运行间隔很短,在setTimeout1完成之后,setTimeout2马上就开始执行了,我们知道浏览器会尽量保持每秒60帧的刷新频率(大约16.7ms每帧),是不是只有两次event loop间隔大于16.7ms才会进行绘制呢?
例子5
将时间间隔加大一些。
两块黄色的区域就是 setTimeout,在1224ms处绿色部分,浏览器对con.textContent = 0的变动进行了绘制。在1234ms处绿色部分,绘制了con.textContent = 1。
可否认为相邻的两次event loop的间隔很短,浏览器就不会去更新渲染了呢?继续我们的实验
例子6
我们在同一时间执行多个setTimeout来模拟执行间隔很短的task。
图中一共绘制了两帧,第一帧4.4ms,第二帧9.3ms,都远远高于每秒60HZ(16.7ms)的频率,第一帧绘制的是con.textContent = 4,第二帧绘制的是 con.textContent = 6。所以两次event loop的间隔很短同样会进行绘制。
例子7
有说法是一轮event loop执行的microtask有数量限制(可能是1000),多余的microtask会放到下一轮执行。下面例子将microtask的数量增加到25000。
总体的timeline:
可以看到一大块黄色区域,上半部分有一根绿线就是点击后的第一次绘制,脚本的运行耗费大量的时间,并且阻塞了渲染。
看看setTimeout2的运行情况。 可以看到setTimeout2这轮event loop没有run microtasks,microtasks在setTimeout1被全部执行完了。
25000个microtasks不能说明event loop对microtasks数量没有限制,有可能这个限制数很高,远超25000,但日常使用基本不会使用那么多了。
对microtasks增加数量限制,一个很大的作用是防止脚本运行时间过长,阻塞渲染。
例子8
使用requestAnimationFrame。
总体的Timeline: 点击后绘制了3帧,把每次变动都绘制了。
看看单个 requestAnimationFrame的Timeline:
和setTimeout很相似,可以看出requestAnimationFrame也是一个task,在它完成之后会运行run microtasks。
例子9
验证postMessage是否是task
执行顺序:
timelime:
第一个黄块是onmessage1,第二个是setTimeout1,第三个是setTimeout2。显而易见,postMessage属于task,因为setTimeout的4ms标准化了,所以这里的postMessage会优先setTimeout运行。
小结
上边的例子可以得出一些结论:
应用
event loop的大致循环过程,可以用下边的图表示:
假设现在执行到currently running task,我们对批量的dom进行异步修改,我们将此任务插进task:
此任务插进microtasks:
可以看到如果task队列如果有大量的任务等待执行时,将dom的变动作为microtasks而不是task能更快的将变化呈现给用户。
同步简简单单就可以完成了,为啥要异步去做这些事?
对于一些简单的场景,同步完全可以胜任,如果得对dom反复修改或者进行大量计算时,使用异步可以作为缓冲,优化性能。
举个小例子:
现在有一个简单的元素,用它展示我们的计算结果:
有一个计算平方的函数,并且会将结果响应到对应的元素
现在我们制造些问题,假设现在很多同步函数引用了bar,在一轮event loop里,可能bar会被调用多次,并且其中有几个是对id='result'的元素进行操作。就像下边一样:
似乎这样的问题也不大,但是当计算变得复杂,操作很多dom的时候,这个问题就不容忽视了。
用我们上边讲的event loop知识,修改一下bar。
现在我们用一个store去存储参数,统一在microtasks阶段执行,过滤了多余的计算,即使同步过程中多次对一个元素修改,也只会响应最后一次。
写了个简单插件asyncHelper,可以帮助我们异步的插入task和microtask。
例如:
对之前的例子的使用asyncHelper:
如果不支持microtask将回退成task。
结语
event loop涉及到的东西很多,本文有误的地方请指正。
references