JS的事件循环 Event Loop
javascript是单线程,当遇到异步任务的时候,它会将异步任务放到事件队列中去,先去执行后面的代码,当同步任务执行完成以后,再回来处理事件队列中的回调函数。
如果一个异步任务中又嵌套着另一个异步任务,又会重复上述的过程,我们将这个循环往复的过程称之为事件循环(event loop)。
那么这篇要学的知识点有这些
- 浏览器的线程和js的单线程
- js的执行栈
- 事件队列的异步任务:宏任务,微任务
- 事件循环的具体过程
浏览器的线程
浏览器是多线程的,这区别于js是单线程的,浏览器在运行的时候,会有很多线程,其中就包括JS的执行线程,但是浏览器的多线程中,GUI渲染线程和JS执行线程是互斥的。当JS执行的时候渲染线程就会挂起。至于为什么会挂起,原因就是js可以修改dom节点,如果继续渲染,可能会渲染出来没被修改前的dom节点,或者产生一些意想不到的错误。
浏览器线程主要有以下几种
- GPU进程
- 网络进程
- 插件进程
- 浏览器主进程
- 渲染进程
- GUI渲染线程
- js执行线程
- 事件触发线程
- 定时器线程
- 异步请求线程
JS的执行栈
js执行线程执行任务的时候,会创建js执行栈,同步任务直接推入执行栈中执行,异步任务放入事件队列中,并注册回调函数。当执行栈空闲的的时候,js会读取事件队列中的函数到执行栈中执行。
执行栈遵循先进后出,后进先出原则。执行栈中涉及到函数的执行上下文。这里简单说明一下,与本次的事件循环关系不大。
- js的所有代码都是排队执行的
- 一开始浏览器执行全局代码的时候,会创建全局执行上下文,压入栈中。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部,当前函数执行完成后,当前执行上下文出栈,并等待垃圾回收。
- 浏览器js执行引擎总是访问栈顶的执行上下文
备注:这里的执行上下文,可以简单的理解为变量的作用域
事件队列中的异步任务划分
异步任务分为宏任务和微任务
- 宏任务: script, setTimeout,ajax请求,I/O操作,等等
- 微任务: Promsie.then,MutationObserver,process.nextTick()等等
整个js代码被当做一个宏任务,也就是上面分类的script,也是一轮循环的起点,一轮循环的终点是所有微队列里面的微任务全部执行完毕。下一轮的起点又是一个宏任务被推入执行栈。
事件循环的具体过程
上面说了,js执行的时候会创建执行栈,同时也会有一个事件队列,队列里面分宏队列和微队列(我是这样理解的)。js的代码都在在执行栈中执行的,异步任务会放到事件队列中。那么具体执行流程如下:
- 整个script代码被当做一个宏任务,放到执行栈中执行,当遇到宏任务的时候,直接放到宏队列当中,遇到微任务的时候,会放到微队列当中。
- 当执行栈中的代码执行完毕,js首先会将微任务队列的任务推到执行栈中执行,由于微任务中可能又有宏任务和微任务,所以继续将宏任务和微任务推入不同的队列。
- 当所以微队列的微任务都被执行完了以后,会将第一个推入宏队列的任务,取出执行。(此时就是一个新的事件循环的开始,事件循环的结束,就是所以微任务被执行完毕)
代码示例
示例一
console.log('开始');
setTimeout(() => {
console.log(1);
})
let p = new Promise((resolve, reject) => {
console.log(2);
resolve(3);
});
p.then((value) => {
console.log(value);
});
console.log(4);
// 输出结果;
开始
2
4
3
1
解析:
- 首先一整个代码被当做一个宏任务推入执行栈中执行
- 当遇到console.log('开始'); 打印开始
- setTimeout是个宏任务里面的函数被推入宏队列
- 执行到Promise时,Promise里面传入的是一个立即执行函数,所以它会立马执行,打印出2,并且将promise的状态改成了成功。
- p.then是个微任务,被放入微队列
- 执行console.log(4);打印出4
- 第一个宏任务执行完毕,执行微队列里面的微任务,所以执行p.then里面的函数,打印出来3,
- 微队列执行完毕,开始下一轮循环,将宏队列里面的第一个推入执行栈执行,也就是执行setTimeout里面的函数console.log(1);打印出来1。整个代码执行完毕。
备注:这里Promise立即执行函数和回调,我已经在上一篇说过了,不会的看这个链接的文章。 juejin.cn/post/709530…
示例二
console.log('开始');
setTimeout(() => {
console.log(1);
let p1 = new Promise((resolve) => {
console.log(2);
resolve(3);
});
p1.then((value) => {
console.log(value);
});
});
let p2 = new Promise((resolve, reject) => {
console.log(4);
});
p2.then(() => {
console.log(5);
setTimeout(() => {
console.log(6);
})
});
console.log(7);
// 输出结果:
开始
4
7
1
2
3
解析:
- 执行到console.log('开始'); 打印开始
- setTimeout为宏任务,直接放入宏队列
- p2的new Promise立即执行函数会被执行,打印出来4,但是它没有改变状态,所以p2.then不会被放入微队列。
- 执行console.log(7);打印7
- 微队列无任务,直接进入下一轮循环,执行setTimeout里面的函数,打印1
- p1的new Promise立即执行函数执行,打印出来2,并且改变状态,p1.then里面的函数放入微队列,执行栈空闲,取微队列里面任务,打印出来3.结束。
下面的实例自己去研究吧,我就不一一解释了,有问题评论区
示例三
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4')
})
setTimeout(function() {
console.log('5');
new Promise(function(resolve) {
console.log('6');
resolve();
}).then(function() {
console.log('7')
})
})
console.log('14');
})
new Promise(function(resolve) {
console.log('8');
resolve();
}).then(function() {
console.log('9')
})
setTimeout(function() {
console.log('10');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
console.log('13')
// 输出结果
// 1 8 13 9 2 3 14 4 10 11 12 5 6 7
示例四
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
console.log('13')
// 输出结果
// 1 7 13 6 8 2 4 3 5 9 11 10 12
写在最后
通过这篇文章的学习,应该是完全能应对,面试题中的事件循环了吧,其实很简单,只要理解一次循环的过程,和能区分宏任务和微任务,就好了,剩下的都是同步代码的执行,都很简单。只要习题能做出来,问题应该不大。
嗯,写到这里,我发现用process.nextTick是微任务,哈哈哈,我上一章在promise里面用setTimeout去模仿微任务,真的太蠢拉。我要去在评论区给自己纠正一下。