一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
首先先来看下这道面试题:
// 今日头条面试题
async function asyncFunction1() {
console.log('asyncFunction1 start')
await asyncFunction2()
console.log('asyncFunction1 end')
}
async function asyncFunction2() {
console.log('asyncFunction2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
asyncFunction1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
这道面试题的本质,其实就是要考察setTimeout、promise、async await的实现及执行顺序,以及JS的事件循环的相关问题。
这道题的最终的答案是:
script start
asyncFunction1 start
asyncFunction2
promise1
script end
asyncFunction1 end
promise2
settimeout
这道题涉及到的就是Microtasks(微任务)、Macrotasks(宏任务)、event loop(事件循环) 以及 JS 的异步运行机制;
一、 event loop 事件循环机制
JS主线程不断的循环往复的从任务队列中读取任务,执行任务,其中运行机制称为事件循环(event loop)
二、 Microtasks(微任务)、Macrotasks(宏任务)
- 在异步模式下,创建异步任务主要分为宏任务与微任务两种。ES6规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。
- 宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
微任务的优先级要高于宏任务。- 宏任务 用于处理 I/O 和计时器等事件,每次执行一个。
- 微任务 为
async/await和 Promise 实现延迟执行,并在每个 宏任务 结束时执行。 - 在每一个事件循环之前,微任务 队列总是被清空(执行)。
microtasks包含的api:
- process.nextTick
- promise
- Object.observe (废弃)
- MutationObserver
macrotasks包含的api:
- setTimeout
- setImmediate
- setInterval
- I/O
- UI 渲染
- MessageChannel
注意:
- 每一个 event loop 都有一个 microtask queue
- 每个 event loop 会有一个或多个macrotask queue ( 也可以称为task queue )
- 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue中
- 每一次event loop,会首先执行 microtask queue, 执行完成后,会提取 macrotask queue 的一个任务加入 microtask queue, 接着继续执行microtask queue,依次执行下去直至所有任务执行结束。
三、 JS异步执行机制
JS 主线程拥有一个 执行栈(同步任务) 和 一个 任务队列(microtasks queue) ,主线程会依次执行代码,
- 当遇到函数(同步)时,会先将函数入栈,函数运行结束后再将该函数出栈;
- 当遇到 task 任务(异步)时,这些 task 会返回一个值,让主线程不在此阻塞,使主线程继续执行下去,而真正的 task 任务将交给 浏览器内核 执行,浏览器内核执行结束后,会将该任务事先定义好的回调函数加入相应的任务队列(microtasks queue/ macrotasks queue) 中。
- 当JS主线程清空执行栈之后,会按先入先出的顺序读取microtasks queue中的回调函数,并将该函数入栈,继续运行执行栈,直到清空执行栈,再去读取任务队列。
- 当microtasks queue中的任务执行完成后,会提取 macrotask queue 的一个任务加入 microtask queue, 接着继续执行microtask queue,依次执行下去直至所有任务执行结束。
这就是 JS的异步执行机制
四、 async await、Promise和setTimeout
async await
async function async1(){
console.log('async1 start'); // 3. 打印 async1 start
await async2(); // 4. 执行async2并等待
console.log('async1 end') // 7. 打印 saync1 end
}
async function async2(){
console.log('async2') // 5. 打印 async2
}
console.log('script start'); // 1. 打印 script start
async1(); // 2. 调用async1方法
console.log('script end') // 6. 打印script end
// 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果( Promise 对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
Promise
Promise本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。
console.log('script start') //1. 打印 script start
let promise1 = new Promise(function (resolve) {
console.log('promise1') //3. 打印 promise1
resolve()
console.log('promise1 end')//4. 打印 promise1 end
}).then(function () { //5.promise1 是 **resolved 或 rejected** :那这个 task 就会放入**当前事件循环**回合的 microtask queue
console.log('promise2')//8. 打印 promise2
})// 2. 执行Promise对象
setTimeout(function(){
console.log('settimeout') //9. 打印 settimeout
})// 6. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') // 7.打印 script end
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
setTimeout
console.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script end
// 输出顺序:script start->script end->settimeout