十六. 事件循环_ 面试题
16.1. 浏览器的事件循环
如果在执行JavaScript代码的过程中,异步操作如何执行的呢?
-
中间我们插入了一个setTimeout的函数调用;
- setTimeout()本身不是一个异步的操作,传入的回调函数才是异步的
-
这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行
16.2. 宏任务和微任务
但是事件循环中并非只维护着一个队列,事实上是有两个队列:
- 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
- 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
那么事件循环对于两个队列的优先级是怎么样的呢?
-
1.main script中的代码优先执行(编写的顶层script代码);
-
2.在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的;
- 如果不为空,那么就优先执行微任务队列中的任务(回调);
16.3. Promise面试题
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
queueMicrotask(() => {
console.log("queueMicrotask1")
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2
16.4. promise async await面试题
async function bar() {
console.log('222');
return new Promise((resolve) => {
resolve()
})
}
async function foo() {
// await和generator一样,会先执行第一段代码,然后执行excutor(),
// 然后把then后面的内容加入到微任务,即'333'加到微任务
console.log('111');
await bar()
console.log('333');
}
foo()
console.log('4444');
// 111 222 444 333
async function async1 () {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise (function (resolve) {
console.log('promise1')
resolve();
}).then (function () {
console.log('promise2')
})
console.log('script end')
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
16.5. Promise面试题(困难);掘金上某厂面试题
Promise.resolve().then(() => {
console.log(0);
// return Promise
// 不是普通的值, 多加一次微任务
// Promise.resolve(4), 多加一次微任务
// 一共多加两次微任务
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
演变一:return 普通值
Promise.resolve().then(() => {
console.log(0);
// 直接return一个值 相当于resolve(4)
return 4
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
// 这里的回调函数执行时,会把clg(2)回调函数加入微任务
console.log(1);
}).then(() => {
// clg(2)加入微任务后,不是马上添加clg(3)回调函数,而是去执行微任务,
// 等clg(2)执行时,才会添加clg(3)的微任务
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})
// 打印顺序: 0142356
演变二:return thenable对象
按理是没有区别,但是结果和直接return 4却不一样,原生Promise在执行thenable对象时,往后面推迟了一个微任务
- 注:不能用promise A+规范去套执行结果,ES6的Promise遵循A+,但也做了一些功能拓展
结果不一样的原因:
- 如果不是return一个普通的值,比如thenable对象,then函数不是直接执行的,而是推到下一个微任务后面去执行
问:为什么要推迟到下一个微任务?
- 答:then是一个函数,如果里面包含了大量的计算代码,然后才会确定Promise的状态,那么会阻塞后面的微任务clg(2)
问:那推迟到下一个微任务,不照样会阻塞后面的clg(3)微任务吗?
- 答:我们这里只是写了个特殊的案例去讨论执行流程,一般真实开发不会嵌套这么多微任务,有可能clg(2)执行完就没有微任务了,
大概是出于这样的一个考虑,做了一些优化,所以把then函数往后推了一个微任务
Promise.resolve().then(() => {
console.log(0);
return {
then: function () {
//函数内有大量的计算代码
resolve(4)
}
}
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
})
// 打印顺序: 0124356
16.6. node的事件循环
浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。
事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:
- 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
- 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;
- 早期js是应用在浏览器的,为什么现在也可以用在服务器?因为可以进行IO操作,Input(输入)/Output(输出), js做不了的事情,就会交给libuv去完成,libuv完成后把结果和回调放到队列里,js再从事件循环里取出来
但是一次完整的事件循环Tick分成很多个阶段:
- 定时器(Timers) :本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- 待定回调(Pending Callback) :对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到 ECONNREFUSED。
- idle, prepare:仅系统内部使用。
- 轮询(Poll) :检索新的 I/O 事件;执行与 I/O 相关的回调;
- 检测(check) :setImmediate() 回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。
16.7. Node事件循环的阶段图解
16.8. Node的宏任务和微任务
我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
- 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
- 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
但是,Node中的事件循环不只是 微任务队列和 宏任务队列:
-
微任务队列:(若队列里两种微任务都存在,则按下面排列顺序执行)
- next tick queue:process.nextTick;
- other queue:Promise的then回调、queueMicrotask;
-
宏任务队列: (在一次Tick,会按下面排列顺序执行宏任务)
- timer queue:setTimeout、setInterval;
- poll queue:IO事件;
- check queue:setImmediate;
- close queue:close事件;
所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:
- next tick microtask queue;
- other microtask queue;
- timer queue;
- poll queue;
- check queue;
- close queue;
16.9. node面试题(困难)
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
// 注意:等到300ms后才会加入到timer queue,也就是所有的宏任务都执行了,才会执行这个回调
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nexttick1
// nexttick2
// async1 end
// promise3
// settimetout0
// setImmediate
// setTimeout2