基本概念
在研究浏览器的事件循环机制之前,先了解几个关键词。
执行栈
同步任务都在主线程上执行,形成一个执行栈,可以认为是一个存储函数调用的栈结构,遵循后进先出的原则。
任务队列
只要异步任务有了运行结果,就在任务队列中放置一个事件。异步任务分为宏任务macro-task和微任务micro-task,在es6中宏任务被称为task,微任务被称为jobs。宏任务会放置在宏任务队列,微任务会放置在微任务队列。
宏任务
浏览器常见的宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、UI render。
微任务
浏览器常见的微任务:Promise.then、Async/Await、MutationObserver(h5新特性)。
事件循环过程
- 开始的时候执行栈和微任务队列为空,宏任务队列有且只有一个script脚本(整体代码);
- 执行栈中同步任务执行完毕后,系统会读取任务队列。只有宏任务队列有一个script脚本(整体代码)的异步任务,将该宏任务推入执行栈;
- 执行过程中,遇到同步代码直接执行,生成宏任务添加到宏任务队列,生成微任务添加到微任务队列。等同步代码执行玩后,script脚本被移除宏任务队列,这个就是宏任务的执行和出队列的过程;
- 执行完一个宏任务之后,接下来就是处理上一个宏任务执行过程中产生的微任务队列,逐个执行微任务并将任务出队,直到队列被清空。需要注意的是宏任务的执行和出队是一个一个执行的,而微任务的执行和出队是一队一队的;
- 执行渲染操作,更新界面;
- 上面过程循环往复,知道宏任务队列和微任务队列都清空。
举几个🌰
宏任务和微任务执行顺序
1 | Promise.resolve().then(()=>{ |
执行流程分析:
整体代码是一个宏任务,将宏任务压入执行栈。执行过程中先生成了一个微任务简称为Promise1添加到微任务队列,后面又生成一个宏任务简称setTimeout1添加到宏任务队列。执行完这个整体代码的宏任务之后,将这个宏任务出队列。
接下来就是执行上一个宏任务(整体代码)生成的微任务队列,开始执行Promise1。执行微任务p1的时候生成一个宏任务setTimeout2,将宏任务setTimeout2添加到宏任务队列,此时宏任务队列有setTimeout1和setTimeout2。
微任务队列执行完成,执行宏任务队列最前面的宏任务setTimeout1,因为队列的原则是先进先出。在执行宏任务setTimeout1的时候,生成了微任务Promise2。执行完宏任务setTimeout1,将这个宏任务出队列。
接下来就是执行上一个宏任务(setTimeout1)生成的微任务队列,开始执行Promise2。执行完微任务列队,再来执行宏任务里的唯一的一个宏任务setTimeout2。执行完宏任务setTimeout2,将这个宏任务出队列。此时宏任务和微任务队列都已清空,结束当前事件循环。
⚠️微任务执行完也是会出列的。
Async/Await执行顺序
科普知识:await下面的代码怎么执行?
如果await后面跟同步函数的调用。相当于直接将await下面的代码注册为一个微任务,可简单理解为promise.then(await下面的代码)。然后跳出async函数,执行其他代码。
如果await后面跟一个异步函数的调用,当await之后的函数中同步任务被执行,异步任务被添加到任务队列之后,直接跳出async函数,执行剩下代码,等剩下代码同步任务被执行,异步代码被添加到任务队列之后,再来将await下面的代码注册为一个微任务。
await后面跟同步函数的调用
1 | console.log('script start') |
执行流程分析:
- 整体代码是个宏任务压入执行栈执行,执行同步任务打印’script start’;
- 执行async1时,await后面的函数可以当作同步任务执行,打印’async2 end’;
- 因为async1中的await后面的函数并不是异步函数,所以可以直接将await下面的代码生成为一个微任务,并添加到微任务队列;
- 代码往下执行生成一个setTimeout宏任务,将宏任务添加到宏任务队列;
- 代码往下执行创建一个Promise实例。注意创建Promise实例时,参数是一个是以同步的方式执行的函数,直接打印’Promise’;
- 代码往下执行生成promise1和promise2两个微任务,并按顺序加入微任务队列;
- 执行同步任务打印’script end’,将全部代码这个宏任务出列;
- 开始执行上一个宏任务生成的微任务队列,此时微任务队列里是三个微任务,分别是await下面的代码生成的微任务、promise1和promise2,依次执行出队列。结果是按顺序打印’async1 end’、’promise1’、 ‘promise2’;
- 执行完微任务队列后,此时宏任务队列只有一个setTimeout宏任务;
- 执行setTimeout宏任务,打印’setTimeout’,结束宏任务并将宏任务出列。此时宏任务和微任务队列都已清空,结束当前事件循环。
await后面跟异步函数的调用
1 | console.log('script start') |
执行流程分析(跟上个🌰的流程差别,主要是await后面的代码执行时机,具体是步骤2、3、8):
- 整体代码是个宏任务压入执行栈执行,执行同步任务打印’script start’;
- 执行async1时,await后面的函数可以当作同步任务执行。async2函数执行打印’async2 end’,生成一个微任务简称为Promise async2;
- 因为async1中的await后面的函数是异步函数,所以直接跳出了async1函数;
- 代码往下执行生成一个setTimeout宏任务,将宏任务添加到宏任务队列;
- 代码往下执行创建一个Promise实例。注意创建Promise实例时,参数是一个是以同步的方式执行的函数,直接打印’Promise’;
- 代码往下执行生成promise1和promise2两个微任务,并按顺序加入微任务队列;
- 执行同步任务打印’script end’,再回到async1函数中将await下面的代码生成为一个微任务,并添加到微任务队列。此时本轮宏任务执行就执行完了,将全部代码这个宏任务出列;
- 开始执行上一个宏任务生成的微任务队列,此时微任务队列里是三个微任务,分别是Promise async2、promise1、promise2和await下面的代码生成的微任务,依次执行出队列。结果是按顺序打印’async2 end1’、’promise1’、 ‘promise2’、’async1 end’;
- 执行完微任务队列后,此时宏任务队列只有一个setTimeout宏任务;
- 执行setTimeout宏任务,打印’setTimeout’,结束宏任务并将宏任务出列。此时宏任务和微任务队列都已清空,结束当前事件循环。