![](https://img.haomeiwen.com/i599584/24a39ffd9677c4be.png)
JavaScript的学习零散而庞杂,因此很多时候我们学到了一些东西,但是却没办法感受到自己的进步,甚至过了不久,就把学到的东西给忘了。为了解决自己的这个困扰,在学习的过程中,我一直试图在寻找一条核心的线索,只要我根据这条线索,我就能够一点一点的进步。
前端基础进阶正是围绕这条线索慢慢展开,而事件循环机制(Event Loop),则是这条线索的最关键的知识点。所以,我就马不停蹄的去深入的学习了事件循环机制,并总结出了这篇文章跟大家分享。
事件循环机制从整体上的告诉了我们所写的JavaScript代码的执行顺序。但是在我学习的过程中,找到的许多国内博客文章对于它的讲解浅尝辄止,不得其法,很多文章在图中画个圈就表示循环了,看了之后也没感觉明白了多少。但是他又如此重要,以致于当我们想要面试中高级岗位时,事件循环机制总是绕不开的话题。特别是ES6中正式加入了Promise对象之后,对于新标准中事件循环机制的理解就变得更加重要。这就很尴尬了。
最近有两篇比较火的文章也表达了这个问题的重要性。
但是很遗憾的是,大神们告诉了大家这个知识点很重要,却并没有告诉大家为什么会这样。所以当我们在面试时遇到这样的问题时,就算你知道了结果,面试官再进一步问一下,我们依然懵逼。
在学习事件循环机制之前,我默认你已经懂得了如下概念,如果仍然有疑问,可以回过头去看看我以前的文章。
- 执行上下文(Execution context)
- 函数调用栈(call stack)
- 队列数据结构(queue)
- Promise(我会在下一篇文章专门总结Promise的详细使用)
因为chrome浏览器中新标准中的事件循环机制与nodejs类似,因此此处就整合nodejs一起来理解,其中会介绍到几个nodejs有,但是浏览器中没有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate
OK,那我就先抛出结论,然后以例子与图示详细给大家演示事件循环机制。
- 我们知道JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。
当然新标准中的web worker涉及到了多线程,我对它了解也不多,这里就不讨论了。
- JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
![](https://img.haomeiwen.com/i599584/15f617d44cdb990d.png)
-
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
-
任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
-
macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
-
micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
-
setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行
-
来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
-
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
-
其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
纯文字表述确实有点干涩,因此,这里我们通过2个例子,来逐步理解事件循环的具体顺序。
// demo01 出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。
![](https://img.haomeiwen.com/i599584/92fc0827aa39e325.png)
第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。
setTimeout(function() {
console.log('timeout1');
})
![](https://img.haomeiwen.com/i599584/2a99131c2572f898.png)
第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-task的Promise队列中去。
因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。
![](https://img.haomeiwen.com/i599584/774ec33de48c1d41.png)
![](https://img.haomeiwen.com/i599584/8b5e93798f6c9d52.png)
![](https://img.haomeiwen.com/i599584/521c5da565a35a45.png)
script任务继续往下执行,最后只有一句输出了globa1,然后,全局任务就执行完毕了。
第四步:第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行,也是进入函数调用栈中执行的。
![](https://img.haomeiwen.com/i599584/dd7673edbbe5e687.png)
第五步:当所有的micro-tast执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。
![](https://img.haomeiwen.com/i599584/881e739c134cb6c9.png)
这个时候,我们发现宏任务中,只有在setTimeout队列中还要一个timeout1的任务等待执行。因此就直接执行即可。
![](https://img.haomeiwen.com/i599584/c4ea234b27c5f2f2.png)
这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。
那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复杂一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// demo02
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。
第一步:宏任务script首先执行。全局入栈。glob1输出。
![](https://img.haomeiwen.com/i599584/5ae0b593167e499b.png)
第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
![](https://img.haomeiwen.com/i599584/afded6f26c106326.png)
第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
![](https://img.haomeiwen.com/i599584/c22a5e6567ec25d3.png)
第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。
process.nextTick(function() {
console.log('glob1_nextTick');
})
![](https://img.haomeiwen.com/i599584/8d16de95f6a12b25.png)
第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
![](https://img.haomeiwen.com/i599584/792877853f338494.png)
![](https://img.haomeiwen.com/i599584/b5c548ec48521c87.png)
第六步:执行遇到第二个setTimeout。
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
![](https://img.haomeiwen.com/i599584/0392b96fd8fd2281.png)
第七步:先后遇到nextTick与Promise
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
![](https://img.haomeiwen.com/i599584/7001e3438df47eb0.png)
第八步:再次遇到setImmediate。
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
![](https://img.haomeiwen.com/i599584/eb6742e93ff577cd.png)
这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。
其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。
当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。
这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。
![](https://img.haomeiwen.com/i599584/48cfccebbff92e97.png)
setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。
大家需要注意这里的循环结束的时间节点。
当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。
OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。
当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。
// 用数组模拟一个队列
var tasks = [];
// 模拟一个事件分发器
var addFn1 = function(task) {
tasks.push(task);
}
// 执行所有的任务
var flush = function() {
tasks.map(function(task) {
task();
})
}
// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中
setTimeout(function() {
flush();
})
// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法
var dispatch = function(name) {
tasks.map(function(item) {
if(item.name == name) {
item.handler();
}
})
}
// 当然,我们把任务丢进去的时候,多保存一个name即可。
// 这时候,task的格式就如下
demoTask = {
name: 'demo',
handler: function() {}
}
// 于是,一个订阅-通知的设计模式就这样轻松的被实现了
这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。
需要注意的是,这里的执行顺序,或者执行的优先级在不同的场景里由于实现的不同会导致不同的结果,包括node的不同版本,不同浏览器等都有不同的结果。
[图片上传失败...(image-26761c-1529051594378)]
网友评论
如果是ajax呢?它的回调函数又是怎么样的啊?希望大神能再讲讲
console.log(1);
new Promise(function(resolve,reject){
window.setTimeout(function(){
resolve(false);
},0);
}).then(function(){
console.log(2);
},function(){
console.log(3);
})
setImmediate(function() {
console.log('immediate1'); //2
})
setTimeout(function() {
console.log('timeout1'); //1
})
输出结果:
timeout1
immediate1
结论:是否可以认为setTimeout比setImmediate优先级高?node 8.x
情况二
process.nextTick(function() {
console.log('glob1_nextTick'); //2
})
new Promise(function(resolve) {
console.log('glob1_promise'); //1
resolve();
}).then(function() {
console.log('glob1_then'); //3
})
输出结果:
glob1_promise
glob1_nextTick
glob1_then
结论:是否可以认为process.nextTick比then优先级高?node 8.x
@这波能反杀
推荐下,源码圈 300 胖友的书单整理:http://t.cn/R0Uflld
忿
1. 浏览器标准环境中(比如说谷歌webkit内核),是一个宏任务紧接着所有微任务执行。
2. 在node环境中,则又不一样了,是一个类型宏任务队列执行完,再去执行微任务。
自己运行一下代码,就可以发现了。
不知道这样理解是否正确,还有想问一下作者,为什么执行完setImmediate的所有微任务,才算是第二轮结束,这里觉得比较匪夷所思。
golb1
glob1_promise
glob2_promise
//执行第一轮微任务队列(micro)
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
第一轮事件循环结束
//执行第二轮宏任务中的setTimeout队列(macro)
timeout1
timeout1_promise
timeout2
timeout2_promise
//执行第二轮宏任务setTimeout产生的微任务队列(micro)
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
第二轮事件循环结束
//执行第二轮宏任务中setImmediate队列(macro)
immediate1
immediate1_promise
immediate2
immediate2_promise
//执行第二轮宏任务setImmediate产生的微任务队列(micro)
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
console.log('setImmediate1');
})
setTimeout(function() {
console.log('timeout1');
})
setImmediate(function() {
console.log('setImmediate2');
})
setTimeout(function() {
console.log('timeout2');
})
在chrome 60.0.3112.90版本下,连续执行20次,结果都是setImmediate1、setImmediate2、timeout1、timeout2。说明在不同执行环境,对于两者优先级的策略是不同的,不能死记硬背。
以上,求大神们解答!
这应该可以算是一个合理的解释
例如我们可以直接让setTimeout的事件进入队列,每次循环到该事件时加一个时间判断,如果符合时间了,就执行,不符合就让它过,然后到下一轮循环。
但是这种方式明显不是最优的解法。我们还可以先将setTiemout的事件丢在事件队列中,先让它处于挂起状态,然后通过事件触发的方式,满足了时间条件再丢入setTimeout的队列中。
总之能够实现的方式很多,具体浏览器的js引擎采用的那种方式我不知道,有可能不同的浏览器采取的方式都不一样。
https://vimeo.com/96425312
讲解了整个浏览器的事件的流程,大家可以看一下.就能清楚很多比如事件监听,异步这些了
里面有详细的讲解整个浏览器关于事件循环和事件监听的
文中:
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
注: macro-task 执行一个任务完毕 然后执行完micro-task
但是在demo2 解释中 写到 :
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
这样有矛盾。
demo2 在chrome执行(去除了setImmediate 和 process.nextTick)
code:
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
结果:
golb1
glob1_promise
glob2_promise
glob1_then
glob2_then
imeout1
imeout1_promise
imeout1_then
timeout2
timeout2_promise
timeout2_then
node(v7.7.1 原demo2 没修改) 执行情况
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
请问这怎么解释?
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve((3 * x) - 1);
}, 0);
});
}
function foo (bar, baz) {
var x = bar * baz;
return getY(x)
.then(function (y) {
return [ x, y ];
});
}
setTimeout(function () {
console.log('timeout')
}, 0)
foo(10, 20).then(function (msgs) {
var x = msgs[ 0 ];
var y = msgs[ 1 ];
console.log(x, y);
});
希望博主可以对这个案例分析一下
setImmediate队列在setTimeout队列之后执行是因为setTimeout第一次出现的顺序就在setImmediate之前是吗?
不知道这里有没有文章,如果代码顺序是
......
setImmediate( function( ) {
console.log("immediate1");
......
}
......
setTimeout( function( ) {
console.log("timeout1");
......
}
......
的话,是不是setImmediate队列就会先执行?
console.log('start');
var xhr = new XMLHttpRequest();
var url = 'test.json';
xhr.open('GET', url,true);
xhr.onreadystatechange = function() {
if (xhr.status == 200 && xhr.readyState == 4) {
console.log(xhr.responseText)
}
}
xhr.send();
setTimeout(function() {
console.log('timeout')
}, 0)
for (var i = 0; i < 3000000000; i++) {}
var btn = document.querySelector("#btn");
btn.onclick = function() {
console.log('click')
}
new Promise((reslove, reject) => {
console.log('promise');
reslove();
}).then((res) => {
console.log('promise finish')
})
console.log('end');
start
promise
end
promise finish
click
{
"test":"aa"
}
timeout
这里和我预期的有点不一样,我以为应该是ajax的回调先被执行到的,以为我故意停留了2-3秒来点击那个按钮,按理说ajax率先返回了应该被先加入到队列中,结果是click先先输出。。。这里是不是有优先级的存在?
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
对于这段代码,浏览器和node 的执行结果不一致,
浏览器的结果是:
timeout1->timeout1_promise->timeout1_then->timeout2->timeout2_promise->timeout2_then
而node结果是:
timeout1->timeout1_promise->timeout2->timeout2_promise->timeout1_then->timeout2_then
请问这是什么原因呢
例如下面这个例子
var start = new Date();
setTimeout(function() {
console.log(new Date() - start);
}, 500);
while (new Date() - start <= 1000) {}
结果输出是1000大一点,所以我认为是setTimeout这类定时器在执行到的时候就已经开始计算第二个参数的那个时间了。
表达不好,见谅。
我测试了宏任务队列的执行顺序:script(整体代码)->setTimeout(setInterval同源)->setImmediate
微任务队列的执行顺序:process.nextTick->Promise(then)
还有一点就是我关于循环结束的时间点的理解:宏任务中最后一个任务(该任务所处的队列可能不是最后一个任务队列)执行完,再执行完微任务中的所有可执行的微任务,循环结束。
问题:
1 .宏任务和微任务中的其他的几个任务队列中的任务的执行不知道该如何测试 = =
2. 对于循环结束时间点的理解是否恰当
望波哥指点一哈,感谢。
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
我如果把promise放到nextTick前面,也是会先执行nextTick,请问是不是微任务里面的任务类型有先后呢?
micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
您在这列举的顺序是不是就是他们的权重顺序了?