event loops隐藏得比较深,不少人对它很陌生。但提起异步,相信每一个人都知道。异步背后的“靠山”就是event loops。这里的异步准确的说应该叫浏览器的event loops或者说是javaScript运行环境的event loops,由于ECMAScript中没有event loops,event loops是在HTML Standard定义的。css
event loops规范中定义了浏览器什么时候进行渲染更新,了解它有助于性能优化。html
思考下边的代码运行顺序:html5
console.log('start') setTimeout( function () { console.log('setTimeout') }, 0 ) Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('end') // start // end // promise1 // promise2 // setTimeout
上面的顺序是在chrome运行得出的,有趣的是在safari 9.1.2中测试,promise1 promise2会在setTimeout的后边,而在safari 10.0.1中获得了和chrome同样的结果。为什么浏览器有不一样的表现,了解tasks, microtasks队列就能够解答这个问题。java
不少框架和库都会使用相似下面函数:node
function flush() { ... } function useMutationObserver() { var iterations = 0; var observer = new MutationObserver(flush); var node = document.createTextNode(''); observer.observe(node, { characterData: true }); return function () { node.data = iterations = ++iterations % 2; }; }
初次看这个useMutationObserver函数总会颇有疑惑,MutationObserver
不是用来观察dom的变化的吗,这样凭空造出一个节点来反复修改它的内容,来触发观察的回调函数有何意义?git
答案就是使用Mutation事件
能够异步执行操做(例子中的flush函数),一是能够尽快响应变化,二是能够去除重复的计算。可是setTimeout(flush, 0)
一样也能够执行异步操做,要知道其中的差别和选择哪一种异步方法,就得了解event loop。github
先看看它们在规范中的定义。web
Note:本文的引用部分,就是对规范的翻译,有的部分会归纳或者省略的翻译,有误请指正。ajax
event loop翻译出来就是事件循环,能够理解为实现异步的一种方式,咱们来看看event loop在HTML Standard中的定义章节:chrome
第一句话:
为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的
event loop
。
事件,用户交互,脚本,渲染,网络这些都是咱们所熟悉的东西,他们都是由event loop协调的。触发一个click
事件,进行一次ajax
请求,背后都有event loop
在运做。
一个event loop有一个或者多个task队列。
当用户代理安排一个任务,必须将该任务增长到相应的event loop的一个tsak队列中。
每个task都来源于指定的任务源,好比能够为鼠标、键盘事件提供一个task队列,其余事件又是一个单独的队列。能够为鼠标、键盘事件分配更多的时间,保证交互的流畅。
task也被称为macrotask,task队列仍是比较好理解的,就是一个先进先出的队列,由指定的任务源去提供任务。
哪些是task任务源呢?
规范在Generic task sources中有说起:
DOM操做任务源:
此任务源被用来相应dom操做,例如一个元素以非阻塞的方式插入文档。
用户交互任务源:
此任务源用于对用户交互做出反应,例如键盘或鼠标输入。响应用户操做的事件(例如click)必须使用task队列。
网络任务源:
网络任务源被用来响应网络活动。
history traversal任务源:
当调用history.back()等相似的api时,将任务插进task队列。
task任务源很是宽泛,好比ajax
的onload
,click
事件,基本上咱们常常绑定的各类事件都是task任务源,还有数据库操做(IndexedDB ),须要注意的是setTimeout
、setInterval
、setImmediate
也是task任务源。总结来讲task任务源:
每个event loop都有一个microtask队列,一个microtask会被排进microtask队列而不是task队列。
有两种microtasks:分别是solitary callback microtasks和compound microtasks。规范值只覆盖solitary callback microtasks。
若是在初期执行时,spin the event loop,microtasks有可能被移动到常规的task队列,在这种状况下,microtasks任务源会被task任务源所用。一般状况,task任务源和microtasks是不相关的。
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 loop,可是每一个单元的类似源浏览器上下文至多有一个event loop。
event loop 老是具备至少一个浏览器上下文,当一个event loop的浏览器上下文全都销毁的时候,event loop也会销毁。一个浏览器上下文总有一个event loop去协调它的活动。
Worker的event loop相对简单一些,一个worker对应一个event loop,worker进程模型管理event loop的生命周期。
反复提到的一个词是browsing contexts(浏览器上下文)。
浏览器上下文是一个将 Document 对象呈现给用户的环境。在一个 Web 浏览器内,一个标签页或窗口常包含一个浏览上下文,如一个 iframe 或一个 frameset 内的若干 frame。
结合一些资料,对上边规范给出一些理解(有误请指正):
event loop
。event loop
,browsing contexts
和web workers
就是相互独立的。browsing contexts
能够共用event loop
,这样它们之间就能够相互通讯。在规范的Processing model定义了event loop
的循环过程:
一个event loop只要存在,就会不断执行下边的步骤:
1.在tasks队列中选择最老的一个task,用户代理能够选择任何task队列,若是没有可选的任务,则跳到下边的microtasks步骤。
2.将上边选择的task设置为正在运行的task。
3.Run: 运行被选择的task。
4.将event loop的currently running task变为null。
5.从task队列里移除前边运行的task。
6.Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
7.更新渲染(Update the rendering)...
8.若是这是一个worker event loop,可是没有任务在task队列中,而且WorkerGlobalScope对象的closing标识为true,则销毁event loop,停止这些步骤,而后进行定义在Web workers章节的run a worker。
9.返回到第一步。
event loop会不断循环上面的步骤,归纳说来:
event loop
会不断循环的去取tasks
队列的中最老的一个任务推入栈中执行,并在当次循环里依次执行并清空microtask
队列里的任务。microtask
队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧之内的屡次dom变更浏览器不会当即响应,而是会积攒变更以最高60HZ的频率更新视图)event loop
运行的第6步,执行了一个microtask checkpoint
,看看规范如何描述microtask checkpoint
:
当用户代理去执行一个microtask checkpoint,若是microtask checkpoint的flag(标识)为false,用户代理必须运行下面的步骤:
1.将microtask checkpoint的flag设为true。
2.Microtask queue handling: 若是event loop的microtask队列为空,直接跳到第八步(Done)。
3.在microtask队列中选择最老的一个任务。
4.将上一步选择的任务设为event loop的currently running task。
5.运行选择的任务。
6.将event loop的currently running task变为null。
7.将前面运行的microtask从microtask队列中删除,而后返回到第二步(Microtask queue handling)。
8.Done: 每个environment settings object它们的 responsible event loop就是当前的event loop,会给environment settings object发一个 rejected promises 的通知。
9.清理IndexedDB的事务。
10.将microtask checkpoint的flag设为flase。
microtask checkpoint
所作的就是执行microtask队列里的任务。何时会调用microtask checkpoint
呢?
task和microtask都是推入栈中执行的,要完整了解event loops还须要认识JavaScript execution context stack,它的规范位于https://tc39.github.io/ecma262/#execution-context-stack。
javaScript是单线程,也就是说只有一个主线程,主线程有一个栈,每个函数执行的时候,都会生成新的execution context(执行上下文)
,执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中, running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
举个简单的例子:
function bar() { console.log('bar'); } function foo() { console.log('foo'); bar(); } foo();
规范晦涩难懂,作一个形象的比喻:
主线程相似一个加工厂,它只有一条流水线,待执行的任务就是流水线上的原料,只有前一个加工完,后一个才能进行。event loops就是把原料放上流水线的工人。只要已经放在流水线上的,它们会被依次处理,称为同步任务。一些待处理的原料,工人会按照它们的种类排序,在适当的时机放上流水线,这些称为异步任务。
过程图:
举个简单的例子,假设一个script标签的代码以下:
Promise.resolve().then(function promise1 () { console.log('promise1'); }) setTimeout(function setTimeout1 (){ console.log('setTimeout1') Promise.resolve().then(function promise2 () { console.log('promise2'); }) }, 0) setTimeout(function setTimeout2 (){ console.log('setTimeout2') }, 0)
运行过程:
script里的代码被列为一个task,放入task队列。
循环1:
循环2:
循环3:
这是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 后运行。
不一样机子测试可能会获得不一样的结果,这取决于浏览器,cpu、gpu性能以及它们当时的状态。
咱们作一个简单的测试
<div id='con'>this is con</div> <script> var t = 0; var con = document.getElementById('con'); con.onclick = function () { setTimeout(function setTimeout1 () { con.textContent = t; }, 0) }; </script>
用chrome的Developer tools的Timeline查看各部分运行的时间点。
当咱们点击这个div的时候,下图截取了部分时间线,黄色部分是脚本运行,紫色部分是更新render树、计算布局,绿色部分是绘制。
绿色和紫色部分能够认为是Update the rendering。
在这一轮事件循环中,setTimeout1是做为task运行的,能够看到paint确实是在task运行完后才进行的。
如今换成一个microtask任务,看看有什么变化
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function () { Promise.resolve().then(function Promise1 () { con.textContext = 0; }) }; </script>
和上一个例子很像,不一样的是这一轮事件循环的task是click的回调函数,Promise1则是microtask,paint一样是在他们以后完成。
标准就是那么定义的,答案彷佛显而易见,咱们把例子变得稍微复杂一些。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function click1() { setTimeout(function setTimeout1() { con.textContent = 0; }, 0) setTimeout(function setTimeout2() { con.textContent = 1; }, 0) }; </script>
当点击后,一共产生3个task,分别是click一、setTimeout一、setTimeout2,因此会分别在3次event loop中进行。
下面截取的是setTimeout一、setTimeout2的部分。
咱们修改了两次textContent,奇怪的是setTimeout一、setTimeout2之间没有paint,浏览器只绘制了textContent=1,难道setTimeout一、setTimeout2在同一次event loop中吗?
在两个setTimeout中增长microtask。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function () { setTimeout(function setTimeout1() { con.textContent = 0; Promise.resolve().then(function Promise1 () { console.log('Promise1') }) }, 0) setTimeout(function setTimeout2() { con.textContent = 1; Promise.resolve().then(function Promise2 () { console.log('Promise2') }) }, 0) }; </script>
从run microtasks中能够看出来,setTimeout一、setTimeout2应该运行在两次event loop中,textContent = 0的修改被跳过了。
setTimeout一、setTimeout2的运行间隔很短,在setTimeout1完成以后,setTimeout2立刻就开始执行了,咱们知道浏览器会尽可能保持每秒60帧的刷新频率(大约16.7ms每帧),是否是只有两次event loop间隔大于16.7ms才会进行绘制呢?
将时间间隔加大一些。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function () { setTimeout(function setTimeout1() { con.textContent = 0; }, 0); setTimeout(function setTimeout2() { con.textContent = 1; }, 16.7); }; </script>
两块黄色的区域就是 setTimeout,在1224ms处绿色部分,浏览器对con.textContent = 0的变更进行了绘制。在1234ms处绿色部分,绘制了con.textContent = 1。
能否认为相邻的两次event loop的间隔很短,浏览器就不会去更新渲染了呢?继续咱们的实验
咱们在同一时间执行多个setTimeout来模拟执行间隔很短的task。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function () { setTimeout(function(){ con.textContent = 0; },0) setTimeout(function(){ con.textContent = 1; },0) setTimeout(function(){ con.textContent = 2; },0) setTimeout(function(){ con.textContent = 3; },0) setTimeout(function(){ con.textContent = 4; },0) setTimeout(function(){ con.textContent = 5; },0) setTimeout(function(){ con.textContent = 6; },0) }; </script>
图中一共绘制了两帧,第一帧4.4ms,第二帧9.3ms,都远远高于每秒60HZ(16.7ms)的频率,第一帧绘制的是con.textContent = 4,第二帧绘制的是 con.textContent = 6。因此两次event loop的间隔很短一样会进行绘制。
有说法是一轮event loop执行的microtask有数量限制(多是1000),多余的microtask会放到下一轮执行。下面例子将microtask的数量增长到25000。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); con.onclick = function () { setTimeout(function setTimeout1() { con.textContent = 'task1'; for(var i = 0; i < 250000; i++){ Promise.resolve().then(function(){ con.textContent = i; }); } }, 0); setTimeout(function setTimeout2() { con.textContent = 'task2'; }, 0); }; </script>
能够看到一大块黄色区域,上半部分有一根绿线就是点击后的第一次绘制,脚本的运行耗费大量的时间,而且阻塞了渲染。
看看setTimeout2的运行状况。
能够看到setTimeout2这轮event loop没有run microtasks,microtasks在setTimeout1被所有执行完了。
25000个microtasks不能说明event loop对microtasks数量没有限制,有可能这个限制数很高,远超25000,但平常使用基本不会使用那么多了。
对microtasks增长数量限制,一个很大的做用是防止脚本运行时间过长,阻塞渲染。
使用requestAnimationFrame。
<div id='con'>this is con</div> <script> var con = document.getElementById('con'); var i = 0; var raf = function(){ requestAnimationFrame(function() { con.textContent = i; Promise.resolve().then(function(){ i++; if(i < 3) raf(); }); }); } con.onclick = function () { raf(); }; </script>
整体的Timeline:
点击后绘制了3帧,把每次变更都绘制了。
看看单个 requestAnimationFrame的Timeline:
和setTimeout很类似,能够看出requestAnimationFrame也是一个task,在它完成以后会运行run microtasks。
验证postMessage是不是task
setTimeout(function setTimeout1(){ console.log('setTimeout1') }, 0) var channel = new MessageChannel(); channel.port1.onmessage = function onmessage1 (){ console.log('postMessage') Promise.resolve().then(function promise1 (){ console.log('promise1') }) }; channel.port2.postMessage(0); setTimeout(function setTimeout2(){ console.log('setTimeout2') }, 0) console.log('sync') }
执行顺序:
sync
postMessage
promise1
setTimeout1
setTimeout2
timelime:
第一个黄块是onmessage1,第二个是setTimeout1,第三个是setTimeout2。显而易见,postMessage属于task,由于setTimeout的4ms标准化了,因此这里的postMessage会优先setTimeout运行。
上边的例子能够得出一些结论:
event loop的大体循环过程,能够用下边的图表示:
假设如今执行到currently running task,咱们对批量的dom进行异步修改,咱们将此任务插进task:
能够看到若是task队列若是有大量的任务等待执行时,将dom的变更做为microtasks而不是task能更快的将变化呈现给用户。
对于一些简单的场景,同步彻底能够胜任,若是得对dom反复修改或者进行大量计算时,使用异步能够做为缓冲,优化性能。
举个小例子:
如今有一个简单的元素,用它展现咱们的计算结果:
<div id='result'>this is result</div>
有一个计算平方的函数,而且会将结果响应到对应的元素
function bar (num, id) { var product = num * num; var resultEle = document.getElementById( id ); resultEle.textContent = product; }
如今咱们制造些问题,假设如今不少同步函数引用了bar,在一轮event loop里,可能bar会被调用屡次,而且其中有几个是对id='result'的元素进行操做。就像下边同样:
...
bar( 2, 'result' ) ... bar( 4, 'result' ) ... bar( 5, 'result' ) ...
彷佛这样的问题也不大,可是当计算变得复杂,操做不少dom的时候,这个问题就不容忽视了。
用咱们上边讲的event loop知识,修改一下bar。
var store = {}, flag = false; function bar (num, id) { store[ id ] = num; if(!flag){ Promise.resolve().then(function () { for( var k in store ){ var num = store[k]; var product = num * num; var resultEle = document.getElementById( k ); resultEle.textContent = product; } }); flag = true; } }
如今咱们用一个store去存储参数,统一在microtasks阶段执行,过滤了多余的计算,即便同步过程当中屡次对一个元素修改,也只会响应最后一次。
写了个简单插件asyncHelper,能够帮助咱们异步的插入task和microtask。
例如:
//生成task var myTask = asyncHelper.task(function () { console.log('this is task') }); //生成microtask var myMicrotask = asyncHelper.mtask(function () { console.log('this is microtask') }); //插入task myTask() //插入microtask myMicrotask();
对以前的例子的使用asyncHelper:
var store = {}; //生成一个microtask var foo = asyncHelper.mtask(function () { for( var k in store ){ var num = store[k]; var product = num * num; var resultEle = document.getElementById( k ); resultEle.textContent = product; } }, {callMode: 'last'}); function bar (num, id) { store[ id ] = num; foo(); }
若是不支持microtask将回退成task。
event loop涉及到的东西不少,本文有误的地方请指正。