1、JS 运行机制
JS 的执行是单线程的,所谓的单线程就是事件任务要排队执行,前一个任务结束,才会执行后一个任务,这就是同步任务,为了避免前一个任务执行了很长时间还没结束,那下一个任务就不能执行的情况,引入了异步任务的概念。JS 运行机制简单来说可以按以下几个步骤。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,会把其回调函数作为一个任务添加到任务队列中。
- 一旦执行栈中的所有同步任务执行完毕,就会读取任务队列,看看里面有那些任务,将其添加到执行栈,开始执行。
主线程不断重复上面的第三步。也就是常说的事件循环(Event Loop)。
异步任务的类型
异步任务的执行结果,会把其回调函数添加到任务队列,可任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
macro-task大概包括:
script(整体代码) setTimeout setInterval setImmediate I/O UI render
micro-task大概包括:
process.nextTick Promise Async/Await(实际就是promise) MutationObserver(html5新特性)
经典的例子
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
// 但是新版的结果为
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
那为什么会有这个现象呢?我们先来看 async 和 await
async/await执行顺序
我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来上边例子的讲解
- 执行代码,输出script start。
- 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。
- 遇到setTimeout,产生一个宏任务
- 执行Promise,输出Promise。遇到then,产生第一个微任务
- 继续执行代码,输出script end
- 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
- 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
- 执行await,实际上会产生一个promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)}
-
执行完成,执行await后面的语句,输出async1 end
-
最后,执行下一个宏任务,即执行setTimeout,输出setTimeout
注意
新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为: // script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
但是这种做法其实是违法了规范的,这是 V8 团队的一个 PR (链接:github.com/tc39/ecma26…
所以我们可以分情况理解
- 如果await 返回的是一个非异步的,比如一个变量或常量,比如 await 1,这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。
- 如果await后面跟的是一个异步函数的调用,如下例子:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
return Promise.resolve().then(()=>{
console.log('async2 end1')
})
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 输出为
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
此时执行完awit并不先把await后面的代码注册到微任务队列中去,而是执行完await之后,直接跳出async1函数,执行其他代码。然后遇到promise的时候,把promise.then注册为微任务。其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end). 可以理解为,这种情况下,await 后面的代码会在本轮循环的最后被执行. 浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。