blog
blog copied to clipboard
从event loop规范探究javaScript异步及浏览器更新渲染时机
作者:杨敬卓
转载请注明出处
异步的思考
event loops隐藏得比较深,很多人对它很陌生。但提起异步,相信每个人都知道。异步背后的“靠山”就是event loops。这里的异步准确的说应该叫浏览器的event loops或者说是javaScript运行环境的event loops,因为ECMAScript中没有event loops,event loops是在HTML Standard定义的。
event loops规范中定义了浏览器何时进行渲染更新,了解它有助于性能优化。
思考下边的代码运行顺序:
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队列就可以解答这个问题。
很多框架和库都会使用类似下面函数:
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的变化的吗,这样凭空造出一个节点来反复修改它的内容,来触发观察的回调函数有何意义?
答案就是使用Mutation事件
可以异步执行操作(例子中的flush函数),一是可以尽快响应变化,二是可以去除重复的计算。但是setTimeout(flush, 0)
同样也可以执行异步操作,要知道其中的差异和选择哪种异步方法,就得了解event loop。
定义
先看看它们在规范中的定义。
Note:本文的引用部分,就是对规范的翻译,有的部分会概括或者省略的翻译,有误请指正。
event loop
event loop翻译出来就是事件循环,可以理解为实现异步的一种方式,我们来看看event loop在HTML Standard中的定义章节:
第一句话:
为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的
event loop
。
事件,用户交互,脚本,渲染,网络这些都是我们所熟悉的东西,他们都是由event loop协调的。触发一个click
事件,进行一次ajax
请求,背后都有event loop
在运作。
task
一个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任务源:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
microtask
每一个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任务源有:
- process.nextTick
- promises
- Object.observe
- MutationObserver
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
。
每一个用户代理必须至少有一个浏览器上下文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
,这样它们之间就可以相互通信。
event loop的处理过程(Processing model)
在规范的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的频率更新视图)
microtasks检查点(microtask checkpoint)
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
呢?
- 当上下文执行栈为空时,执行一个microtask checkpoint。
- 在event loop的第六步(Microtasks: Perform a microtask checkpoint)执行checkpoint,也就是在运行task之后,更新渲染之前。
执行栈(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(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
举个简单的例子:
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:
- 【task队列:script ;microtask队列:】
- 从task队列中取出script任务,推入栈中执行。
- promise1列为microtask,setTimeout1列为task,setTimeout2列为task。
- 【task队列:setTimeout1 setTimeout2;microtask队列:promise1】
- script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise1执行。
循环2:
- 【task队列:setTimeout1 setTimeout2;microtask队列:】
- 从task队列中取出setTimeout1,推入栈中执行,将promise2列为microtask。
- 【task队列:setTimeout2;microtask队列:promise2】
- 执行microtask checkpoint,取出microtask队列的promise2执行。
循环3:
- 【task队列:setTimeout2;microtask队列:】
- 从task队列中取出setTimeout2,推入栈中执行。
- setTimeout2任务执行完毕,执行microtask checkpoint。
- 【task队列:;microtask队列:】
event loop中的Update the rendering(更新渲染)
这是event loop中很重要部分,在第7步会进行Update the rendering(更新渲染),规范允许浏览器自己选择是否更新视图。也就是说可能不是每轮事件循环都去更新视图,只在有必要的时候才更新视图。
https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork 这篇文章较详细的讲解了渲染机制。
渲染的基本流程:
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树, 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
Note: 可以看到渲染树的一个重要组成部分是CSSOM树,绘制会等待css样式全部加载完成才进行,所以css样式加载的快慢是首屏呈现快慢的关键点。
下面讨论一下渲染的时机。规范定义在一次循环中,Update the rendering会在第六步Microtasks: Perform a microtask checkpoint 后运行。
验证更新渲染(Update the rendering)的时机
不同机子测试可能会得到不同的结果,这取决于浏览器,cpu、gpu性能以及它们当时的状态。
例子1
我们做一个简单的测试
<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运行完后才进行的。
例子2
现在换成一个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同样是在他们之后完成。
标准就是那么定义的,答案似乎显而易见,我们把例子变得稍微复杂一些。
例子3
<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,分别是click1、setTimeout1、setTimeout2,所以会分别在3次event loop中进行。 下面截取的是setTimeout1、setTimeout2的部分。
我们修改了两次textContent,奇怪的是setTimeout1、setTimeout2之间没有paint,浏览器只绘制了textContent=1,难道setTimeout1、setTimeout2在同一次event loop中吗?
例子4
在两个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中可以看出来,setTimeout1、setTimeout2应该运行在两次event loop中,textContent = 0的修改被跳过了。
setTimeout1、setTimeout2的运行间隔很短,在setTimeout1完成之后,setTimeout2马上就开始执行了,我们知道浏览器会尽量保持每秒60帧的刷新频率(大约16.7ms每帧),是不是只有两次event loop间隔大于16.7ms才会进行绘制呢?
例子5
将时间间隔加大一些。
<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的间隔很短,浏览器就不会去更新渲染了呢?继续我们的实验
例子6
我们在同一时间执行多个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的间隔很短同样会进行绘制。
例子7
有说法是一轮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>
总体的timeline:
可以看到一大块黄色区域,上半部分有一根绿线就是点击后的第一次绘制,脚本的运行耗费大量的时间,并且阻塞了渲染。
看看setTimeout2的运行情况。
可以看到setTimeout2这轮event loop没有run microtasks,microtasks在setTimeout1被全部执行完了。
25000个microtasks不能说明event loop对microtasks数量没有限制,有可能这个限制数很高,远超25000,但日常使用基本不会使用那么多了。
对microtasks增加数量限制,一个很大的作用是防止脚本运行时间过长,阻塞渲染。
例子8
使用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。
例子9
验证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中多次修改同一dom,只有最后一次会进行绘制。
- 渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
- 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。
应用
event loop的大致循环过程,可以用下边的图表示:
假设现在执行到currently running task,我们对批量的dom进行异步修改,我们将此任务插进task:
此任务插进microtasks:
可以看到如果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涉及到的东西很多,本文有误的地方请指正。
references
- https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- https://promisesaplus.com/#notes
- https://developers.google.cn/web/fundamentals/performance/critical-rendering-path/render-tree-construction
- http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/#first-article
- http://stackoverflow.com/questions/2035645/when-is-javascript-synchronous
- https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork
- https://vimeo.com/96425312
- https://html.spec.whatwg.org/multipage/webappapis.html#event-loop
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 个人理解,
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as:
Parsing The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.
Callbacks Calling a callback is often done by a dedicated task.
提到了 HTML parser是一个典型的task,当我们解析和执行完
可以做个测试,在文档中间插入一个执行的脚本,使用Developer tools里的performance(原timeline)看到结果是parser HTML -> 执行脚本 -> parser HTML ..... , 执行脚本是会中断文档解析的,因为脚本可能会修改dom trees,所以最初的文档解析和脚本执行应该是一个连续的过程,所以
@aooy 恩恩 有道理 这里有个问题想请教下 call stack 和 execution context stack 是一回事么 在网上看视频 Philip Roberts- Help I'm stuck in an event-loop.-Mobile
他是拿 call stack 来讲的 event loop 这个call stack 和 execution context stack 感觉有相似之处啊 是一回事么
楼主看这个
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
@fulvaz 我看过啊 这个我懂了。 我的问题是 call stack 和 execution context stack 的区别啊
很棒的文章 我最近也在写这个原理 学习了很多之前比较模糊的点
请教博主一个问题,烦请指教!当我反复查看html规范时候
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as:
- Events Dispatching an Event object at a particular EventTarget object is often done by a dedicated task. 2.Parsing The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task. ....
规范里面说到event loop有多个task queues.一个task queue就是任务的列表,它们负责以下工作。 看里很久有两个疑惑,恳请解答 1.dispatching an event 不是浏览器有专门的线程监听各种事件吗?然后相应的event handler 会进入task queues.为什么规范里说:是任务队列Dispatching an Event。因为任务队列里的任务都会被JavaScript engines执行,也就是说是JavaScript engines在触发事件? 2.html解析不是通过渲染引擎来解析的吗,怎么这里又说是JavaScript engines解析?在一次event loop中,如果页面需要更新。这时候会停止JavaScript引擎,启动渲染引擎来更新页面吗? 谢谢了 @aooy
学习了 十分感谢
十分感谢分享!有个疑问想请教下,应用一节提到 “可以看到如果task队列如果有大量的任务等待执行时,将dom的变动作为microtasks而不是task能更快的将变化呈现给用户。” 这里是将dom的变动操作放在microtasks中执行,那么如果要获得最新的DOM是在哪步执行呢? Vuejs的nextTick回调函数里说是可以获得最新DOM,可是UI rendering是在microtasks之后执行,那么是怎么获取到最新DOM的呢? 这点实在困惑
@WinnieFE 当时我和你有一样的疑惑,写点我现在的理解!也欢迎一起来讨论
首先明确 Task -> MicroTask -> UI Render 顺序是一定的,Vue中对于异步更新的运用主要是维护异步队列dom更新合并,以及nextTick。
而nextTick的实质也是MicroTask,只是它会在执行时立刻追加到异步队列后面,而当你依次执行队列时,UI虽然没渲染,但是DOM其实已经更新了,注意:DOM更新是及时的,但是更新是异步的。
我相信你已经理解了!如果有异议也可以一起讨论下!
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)
这个例子在 node.js 是运行的不一样的结果
promise1
setTimeout1
setTimeout2
promise2
➜ ~ node -v
v6.11.3
@caiyongmin node里的event loop可以说和浏览器的event loop完全不是一个东西,node没有微宏任务的概念,规范只是约束了浏览器厂商。不要混淆nodejs和浏览器中的event loop这里有一篇文章可以看看,setTimeout1、setTimeout2是在timers阶段运行的,该阶段结束后才会执行promise2
楼主,我刚试了一下第三个例子,好像是渲染了两次:
我用的chrome 67.0.3396.99
而且你的截图好像只截了setTimeout1,没有setTimeout2?
我基本肯定第三个例子是错误的 在chrome中,setTimeout1和setTimeout2不是属于一个event loop;但在node中,是属于同一个event loop 因为node是执行完一个setTimeout队列(里面可能会有多个setTimeout callback),才会执行micro task;但chrome是执行完一个setTimeout callback,就会执行micro task
setTimeout(()=> {
console.log('setTimeout1')
new Promise((r) => {
r()
})
.then(() => {
console.log('timeout1-promise1')
})
}, 0)
setTimeout(()=> {
console.log('setTimeout2')
new Promise((r) => {
r()
})
.then(() => {
console.log('timeout2-promise1')
})
}, 0)
在node和chrome分别执行下这段js,应该就比较明朗了
这个翻译的是W3c的标准吧,不是whatwg的标准,我最近也从规范入手理解Event loop呢
@WinnieFE 在Vue中更新一个注册到data中的属性,此时引用该值的地方,比如temlate中是不会立即执行的,所以直接获取对应的DOM节点得到的也还是原来的值,这是因为在更新该属性之后,Vue需要依次去执行注册到该属性上的监听函数去更新对应的应用,只有这一系列监听函数执行完成之后,DOM才是最新的,而我们手动注册的$nextTick方法会在这之后执行,所以可以获得更新后的DOM, 以上是个人理解,如果有误,请指正一下
楼主,我刚试了一下第三个例子,好像是渲染了两次:
我用的chrome 67.0.3396.99 而且你的截图好像只截了setTimeout1,没有setTimeout2?
是的,我测试的时候也是两次
setTimeout(() => console.log('timeout'))
Promise.resolve().then(() => {
console.log('promise1')
Promise.resolve().then(() => console.log('promise2'))
Promise.resolve().then(() => console.log('promise3'))
}).then(() => console.log('promise4'))
console.log('normal')
楼主这个代码的运行结果为什么会是 1,2, 3,4
而不是1,4,2,3
,难道在执行microTask
时产生的microTask
优先级更高?