【JavaScript】event-loop事件循环

176 阅读5分钟

前置知识

进程线程

  • 进程:CPU 在运行指令和保存上下文所需要的时间

打开微信,系统在执行打开指令,到加载微信上下文环境,直到彻底关闭微信前,都是一个进程

  • 线程:是进程中的一个更小的单位,指的是执行一段指令所需的时间

打开微信聊天界面,就需要一个渲染线程, 同时获取到最新的消息,需要一个网络线程

小例子

浏览器新开一个不同域名的tab页面,就是一个新的进程。其中有很多线程相互配合工作

这个过程涉及到的线程有:

  • http线程
  • js引擎线程
  • 渲染线程
关于执行中的线程:

主线程:也就是js引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行

工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件(异步接下来咱们细细介绍)

所以:js引擎线程和渲染线程是互斥的,而通常其他线程之间通常是是可以同时工作的。这就导致只有js代码执行完毕后,渲染进程才会工作。会使在js代码量比较庞大时,会出现页面白屏。

异步

js是单线程的 ---v8在执行js代码时,默认只开启一个线程工作

为什么是单线程?

js天然可以操作DOM元素。

js为多线程,想象一下:一个线程要删除某个DOM元素,另一个线程要修改它,那么到底应该以哪个线程的操作为准呢?为了避免这种情况发生,JavaScript从诞生之初就被设计为单线程

考虑到执行效率,v8会先执行同步代码,遇到异步代码时,会将异步代码存放到任务队列,等待 js引擎线程空闲时,再从任务队列中取出异步代码执行。

先来个代码感受一下:

let a = 1

setTimeout(() => {
  a = 2
  console.log(a)

}, 1000)

console.log(a)

运行过程:马上输出1,等一秒后输出2

异步代码示例

事件循环-Event-Loop

JavaScript是单线程语言,这意味着它同时只能执行一个任务。为了处理异步操作,如网络请求、文件读写等,JavaScript使用事件循环机制。

JavaScript运行时包含以下几个主要部分:

  • 调用栈(Call Stack) :这是代码执行的地方,所有的同步代码都会在这里执行。当调用一个函数时,它会被压入调用栈并开始执行。函数执行完毕后,它会被弹出调用栈。
  • 事件队列(Event Queue) :异步操作完成后,相关的回调函数会被放入事件队列中等待执行。例如,当定时器(setTimeout)到时,其回调函数会被放入事件队列。
  • 事件循环(Event Loop) :事件循环会监视调用栈和事件队列。如果调用栈为空,它会从事件队列中取出一个事件,并将其对应的回调函数放入调用栈中执行。这个过程会不断重复,形成事件循环。

js代码中有同步和异步之分,异步还被分为宏任务和微任务

微任务

promise.then, process.nextTick, MutationObserver

宏任务

setTimeout, setInterval, ajax, setImmediate, I/O, UI rendering

宏任务:有明确的异步任务需要执行和回调;需要其他异步线程支持

微任务:没有明确的异步任务需要执行,只有回调;不需要其他异步任务支持

执行顺序

  1. 先执行同步代码(这属于宏任务),这个过程中遇到异步,就分类存入任务队列
  2. 同步执行完毕后,先执行微任务队列中的代码
  3. 微任务全部执行完毕后,有需要的情况下渲染页面
  4. 渲染完毕后,执行宏任务队列中的代码 (开启了下一次的事件循环)

图解

事件循环

代码案例

console.log('同步代码1');

setTimeout(() => {

    console.log('setTimeout')

}, 0)

new Promise((resolve) => {

  console.log('同步代码2')
  resolve()

}).then(() => {

    console.log('promise.then')

})

console.log('同步代码3');

最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"

解释:

  • 首先,打印同步代码1
  • setTimeout的回调函数放入宏任务队列
  • 打印同步任务new Promise中的同步代码2
  • new Promise的回调函数promise.then放入微任务队列
  • 打印同步任务同步代码3
  • 执行微任务队列,打印promise.then
  • 执行宏任务队列,打印setTimeout

图解

拓展: await

  1. 会将后续的代码挤入微任务队列
  2. 浏览器将 await 的执行时间提前了 (await 后面的代码当成同步来看待)
  3. 它会暂停async函数的执行,直到await后面的Promise对象状态变为resolvedrejected

小测验

console.log('script start')

async function async1 () {
  await async2()
  console.log('async1 end')
}

async function async2 () {
  console.log('async2 end')
}

async1()

setTimeout(() => {
  console.log('setTimeout')
}, 0)

new Promise((resolve, reject) => {
  console.log('promise')
  resolve()
}).then(() => {
  console.log('then1')
}).then(() => {
  console.log('then2')
})

console.log('script end')

输出结果:

"script start"、"promise"、"script end"、"async2 end"、"async1 end"、"then1"、"then2"、"setTimeout"

小结

掌握JavaScript的事件循环机制对于前端开发者来说至关重要。它不仅影响代码的执行顺序,还关系到性能优化和异步编程的能力。 通过清晰地解释事件循环的概念、宏任务和微任务的区别,以及提供具体的示例和解答常见问题,你将能够展示出自己在JavaScript异步编程方面的深厚功底。