JavaScript是单线程的,而浏览器是多线程的

什么是事件循环

事件循环又称EventLoop,JavaScript在设计之初便是单线程,这是因为该语言是为了操作DOM而设计的语言,如果设计成多线程,线程A进行删除DOM,而这时线程B又进行了添加DOM,那么最终结果就无法预测。而将该语言设计成单线程语言,那么就不可避免的就会出现阻塞的问题。

为了避免单线程出现阻塞而设计了事件循环机制,将任务划分为同步任务异步任务

同步任务会存在阻塞页面渲染或交互,而异步任务则是由任务队列的方式进行执行。

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

什么是任务队列

事件循环由三部分组成,分别是调用栈消息队列(也成宏任务队列)、微任务队列。顾名思义其作用如下:

  • 调用栈:存放要执行的函数
  • 消息队列:存放宏任务的函数
  • 微任务队列:存放微任务的函数

调用栈

调用栈也称为执行栈,它是一个存储函数调用的栈结构,因此遵循先进后出的原则。

即没遇到一个函数则向内push该函数,当函数执行完时则会进行pop操作

471a44561ba64443b489f18adec04839

消息队列(宏任务)和微任务队列(微任务)

异步任务队列遵循先进先出的原则,常见的异步任务如下:

  • 宏任务(消息会在调用栈清空后,被压入到调用栈中执行)

    • AJAX

    • 事件回调

    • setTimeout中的回调函数

    • setInterval中的回调函数

    • I/O

    • UI

  • 微任务(在调用栈被清空后立即执行(在宏任务前执行),且执行过程中产生的新的微任务会在此次循环中一同执行。)

    • Promise.then的回调函数

    • async await等

其执行顺序如图:

img

例如代码:

console.log('start')

setTimeout(function() {
	console.log('setTimeout')
}, 0)

Promise.resolve().then(function() {
	console.log('promise1')
}).then(function() {
	console.log('promise2')
})

console.log('end')


// start end promise1 promise2 setTimeout

img

浏览器执行顺序

image-20230218181201844

上面的流程图很好的能解释浏览器中事件循环的运行机制:

  1. (循环开始时)在一次循环中,浏览器优先将所有代码加入到宏任务队列中,接下来事件循环首先检查宏任务队列。

  2. (执行宏任务)如果宏任务队列存在任务,则立即依次执行宏任务(即队列的第一个任务),直到该任务完成(一个宏任务)(或者队列为空)

    初始运行时,浏览器解析完所有的代码,开始执行所有的同步代码,并将产生的异步任务加入到对应的任务队列

    注意:如果宏任务队列不为空,则此时并不是指执行完所有的宏任务,而是取出队列最顶端的一个来执行,执行完该任务后则进入下一阶段:微任务

  3. (执行微任务)执行完一个宏任务后,则依次执行微任务队列中的任务,直到微任务队列中的任务全被执行完毕

    注意:此过程中如果产生新的任务,则也会执行。

  4. (渲染页面)此时,当前循环中已经执行完该阶段的宏任务和微任务,事件循环会检查是否需要更新UI(页面渲染),如果需要渲染则会渲染页面。

  5. 回到第一步:再一次进行该循环

宏任务与微任务的最大区别就在于:单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的所有微任务都会被处理,即使在单次循环中再次产生新的微任务

从代码中实践事件循环

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
  new Promise((resolve)=>{
    console.log('promise4')
    resolve()
  }).then(()=>{
    console.log('promise5')
  })
}

async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
  new Promise(resolve=>{
    console.log('promise6')
    resolve()
  }).then(()=>{
    console.log('promise3')
  })
}, 0);

setTimeout(function () {
  console.log("setTimeout2");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});

console.log("script end");

await可以理解为Promise的一个语法糖,例如如上await async2();可以改写为:

new Promise( resolve =>{
  async2()
  resolve()
}).then( () => {
  console.log("async1 end");
  new Promise((resolve)=>{
    console.log('promise4')
    resolve()
  }).then(()=>{
    console.log('promise5')
  })
})

通过上文所描述的事件循环运行机制首先可以在心里想象一下代码执行流程:浏览器解析javascript(宏任务)->执行所有同步代码->执行所有微任务->宏任务->…

在这过程中如果遇到同步代码,则会直接执行同步代码,而不会推入到任务队列(任务队列只针对异步任务)。

  1. 浏览器首次运行,加载所有代码(宏任务),并执行所有同步代码

    宏任务队列:[]

    微任务队列:[]

  2. 依次执行同步代码

    console.log("script start");和两个setTimeout

    这里执行的setTimeout并不是指其回调函数,而是将setTimeout中的回调函数加入到宏任务队列中。

    宏任务队列:[ setTimeout1 , setTimeout2 ]

    微任务队列:[]

    遇到同步代码:执行函数async1();,进入该函数后,会执行同步代码(console.log("async1 start");console.log("async2");),并将异步任务推送到对应的任务队列。

    宏任务队列:[ setTimeout1 , setTimeout2 ]

    微任务队列:[ console.log("async1 end"), Promise]

    Promise的构造函数中的内容为同步代码,因此此时的console.log("promise1");也属于同步代码。并将其产生的微任务推送到对应的队列

    宏任务队列:[ setTimeout1 , setTimeout2 ]

    微任务队列:[ console.log("async1 end"), Promise, console.log("promise2"), ]

    执行最后一个同步代码console.log("script end");

    至此,同步代码已全部执行完毕,此时同步代码中输出的内容为:

    script start
    async1 start
    async2
    promise1
    script end
  3. 执行微任务队列

    依次执行上一步中微任务队列中的任务,首先输出是一个async1 end。接下来则是执行Promise中的同步部分,即promise4该微任务产生了新的微任务,此时将新的微任务推送到队列:

    宏任务队列:[ setTimeout1 , setTimeout2 ]

    微任务队列:[console.log("promise2"), console.log('promise5') ]

    此时在依次执行微任务队列:promise2promise5

    该部分浏览器输出为:

    async1 end
    promise4
    promise2
    promise5
  4. 执行宏任务队列

    此时已执行完所有的微任务,即任务队列只剩下宏任务:

    宏任务队列:[ setTimeout1 , setTimeout2 ]

    微任务队列:[]

    两个定时器的等待时间相同,因此我们从上到下依次看即可。

    首先执行第一个宏任务setTimeout1,输出setTimeout1Promise中同步代码部分promise6,并将异步任务推入微任务队列。此时我们的任务队列则变成了:

    宏任务队列:[ setTimeout2 ]

    微任务队列:[ console.log('promise3') ]

    由事件循环机制我们知道:一次循环只处理一个宏任务,因此下一步则是检查微任务队列是否存在任务。

    该阶段浏览器输出为:

    setTimeout1
    promise6
  5. 执行微任务队列

    此时微任务队列只有一个任务,并不会产生新的任务,

    此时浏览器只输出一个promise3

  6. 执行宏任务队列

    宏任务队列:[ setTimeout2 ]

    微任务队列:[]

    第二个定时器也比较简单,只输出一个setTimeout2

至此完成的输出为:

image-20230218175321205

/**

1. 执行所有同步代码,这属于宏任务
script start
async1 start
async2
promise1
script end

2. 执行所有微任务(如果产生新的微任务则继续执行新的微任务)
async1 end
promise4
promise2
promise5 -> 新的微任务

3. 执行所有宏任务
setTimeout1
promise6

4. 执行所有微任务
promise3

5. 执行所有宏任务
setTimeout2

 */

参考

此文虽然有参考,但还是有些内容无法从参考文中得到明确的答案,因此部分结论为我个人根据资料推测得出。

如有错误,还望大佬指正。