Fork me on GitHub

js 代码运行机制,宏任务、微任务

0. 关于 JavaScript

JavaScript 是单线程语言,其他所谓的“多线程”都是模拟出来的。

1. JavaScript 事件循环

  • 为了解决 js 单线程在执行大量耗时代码时的问题,程序员将 js 的任务分为两大类:
    • 同步任务
      • 进入主线程执行
    • 异步任务
      • 进入Event Table执行
      • 当指定的事件完成时,Event Table会将这个回调函数移入Event Queue
      • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行
      • 上述过程会不断重复,也就是常说的Event Loop(事件循环)

2 macro-task(宏任务)、micro-task(微任务)

  • 除了广义的同步任务和异步任务,其实对任务还有更细致的划分
    • macro-task(宏任务):包括整体代码 scriptsetTimeoutsetInterval
    • micro-task(微任务):Promiseprocess.nextTick

js 事件循环机制,决定了代码执行顺序。

第一步js 解释器识别所有 js 代码,将同步的代码放到主线程执行;异步的代码放到Event Table执行。这也是第一次宏任务执行完毕!

第二步:接下来执行所有的微任务。

之后一直循环一、二步骤

3. 举个栗子 :chestnut:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
setTimeout(() => console.log('setTimeout-1'), 0)

async function todo1 (params) {
console.log('todo1-await-above')
await Promise.resolve(99)
console.log('todo1-await-under')
}

todo1()

new Promise((resolve, reject) => {
console.log('promise-1')
resolve()
}).then(data => {
console.log('promise-then-1')
})

console.log('end')
  • 这段代码作为宏任务,进入主线程
  • 先遇到 setTimeout , 等待 0 ms 后,将其回调函数注入到宏任务Event Queue
  • 接下来遇到 todo1 函数,没调用,就当看不到 :bowtie:
  • 调用 todo1 函数
  • 遇到 console.log('todo1-await-above') 立即执行并输出
  • 遇到 await promise 将等待 promise 执行结束后再继续执行,这里将执行权交给todo1函数外部继续执行
  • 遇到 new Promise 立即执行 console.log('promise-1') 并输出,之后执行 resolve(),将then 的回调函数注入到微任务Event Queue
  • 遇到 console.log('end') ,立即执行并输出
  • 注意代码还有console.log('todo1-await-under')没有执行,在这里执行并放到微任务Event Queue【作者译:因为await后面跟着状态不确定的promise
  • 好了,整体代码<script>作为第一轮的宏任务执行结束,接下来按照先进先出原则,执行微任务队列事件。
  • 执行并输出promise-then-1
  • 执行并输出todo1-await-under
  • 检查宏任务队列,这时还有setTimeout回调函数需要执行
  • 执行并输出setTimeout-1
  • 最后再次检查微任务队列,没有啦。再检查宏任务队列,也没啦。
  • 到此结束!
1
2
3
4
5
6
7
8
9
// 输出:
// todo1-await-above
// promise-1
// end

// promise-then-1
// todo1-await-under

// setTimeout-1

接下来我们稍微改变一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTimeout(() => console.log('setTimeout-1'))

async function todo1 (params) {
console.log('todo1-await-above')
// await Promise.resolve(99)
await 123 // 改变啦
console.log('todo1-await-under')
}

todo1()

new Promise((resolve, reject) => {
console.log('promise-1')
resolve()
}).then(data => {
console.log('promise-then-1')
})

console.log('end')

上面这段代码做了稍微的改动,将todo1函数中的await Promise.resolve(99)更改为await 123

  • 老规矩,这段代码作为宏任务,进入主线程
  • 先遇到 setTimeout , 等待 0 ms 后,将其回调函数注入到宏任务Event Queue
  • 接下来遇到 todo1 函数,没调用,就当看不到 :bowtie:
  • 调用 todo1 函数
  • 遇到 console.log('todo1-await-above') 立即执行并输出
  • 遇到 await 123 因为这里await一个具体值,状态是明确的,所以继续向下执行,将console.log('todo1-await-under')放到微任务队列
  • 遇到 new Promise 立即执行 console.log('promise-1') 并输出,之后执行 resolve(),将 then 的回调函数注入到微任务Event Queue
  • 遇到 console.log('end') ,立即执行并输出
  • 好了,整体代码<script>作为第一轮的宏任务执行结束,接下来按照先进先出原则,先执行微任务队列事件。
  • 执行并输出todo1-await-under
  • 执行并输出promise-then-1
  • 检查宏任务队列,这时还有setTimeout回调函数需要执行
  • 执行并输出setTimeout-1
  • 最后再次检查微任务队列,没有啦。再检查宏任务队列,也没啦。
  • 到此结束!
1
2
3
4
5
6
7
8
9
// 输出:
// todo1-await-above
// promise-1
// end

// todo1-await-under
// promise-then-1

// setTimeout-1

声明:以上结果在 Google Chrome, 70.0.3538.110 版本 中运行得出

我的眼界有限,有什么不对的地方欢迎指正,谢谢!

参考

这一次,彻底弄懂 JavaScript 执行机制

理解 JavaScript 的 async/await

-------------我是有底线哒-------------