事件循环机制(Eventloop)

前言

在JavaScript的执行环境中,事件循环(Event Loop)是实现非阻塞异步执行的关键机制。它由主线程(也称为调用栈)和任务队列(Event Queue)组成,这两个组件协同工作以确保同步和异步任务的有序执行。文章结尾附上真题解析

  • 主线程(Call Stack):这是JavaScript引擎执行同步代码的场所。当代码被执行时,同步任务会被压入调用栈中,按照后进先出(LIFO)的原则依次执行。
  • 任务队列(EventQueue):这是一个先进先出(FIFO)的数据结构,用于存储异步任务的回调函数。当异步操作(如网络请求、定时器等)完成时,它们的回调函数会被放入任务队列中等待执行。
  • 事件循环检查(Event LoopCheck):主线程在执行完当前的同步任务后,会进入事件循环检查阶段。在这个阶段,主线程会不断检查任务队列,查看是否有待处理的事件。
  • 任务队列处理:如果任务队列中有待处理的事件,主线程会从队列中取出第一个事件,将其回调函数推入调用栈中执行。这个过程是连续的,主线程会不断地从任务队列中取出事件并执行,直到队列为空。
  • 事件循环的持续性:事件循环是一个持续的过程,它会不断地检查任务队列,确保异步任务能够在适当的时机被执行。这种机制保证了JavaScript的执行环境能够在单线程中高效地处理同步和异步任务,避免了多线程环境下可能出现的竞态条件和锁的问题。

事件循环的步骤

  1. 执行同步代码:当执行JavaScript代码时,同步任务会被推入调用栈中执行。
  2. 执行异步回调:当异步操作(如setTimeout, Promise, XMLHttpRequest等)完成时,它们的回调函数会被推入事件队列。
  3. 事件循环检查:事件循环会检查调用栈是否为空。如果为空,它会从事件队列中取出第一个任务,推入调用栈执行。
  4. 重复循环:事件循环会不断重复上述过程。

同步任务

同步任务指的是任务按照程序中的顺序,一个接一个地执行,并且当前任务必须等待前一个任务完成后才能开始。在同步执行中,程序的执行流程是线性的,每个操作都必须等待前一个操作完成后才能进行。

console.log('Step 1');
let result = add(2, 3);
console.log(result);
console.log('Step 2');

function add(a, b) {
  return a + b;
}

在上面的例子中,console.log(‘Step 1’) 执行完毕后才会执行函数调用 add(2, 3),并等待 add 函数返回结果后才会继续执行后续代码。

异步任务

异步任务则允许程序在等待某些操作完成时继续执行其他任务。这意味着可以启动一个任务,然后程序继续执行,而不需要等待该任务完成。当异步任务完成时,通常会通过回调函数、事件、或者Promise等方式通知程序。包括:

  • 回调函数 callback
  • Promise/async await
  • Generator
  • 事件监听
  • 发布/订阅
  • 计时器(setTimeout,setInterval)
  • requestAnimationFrame
  • MutationObserver
  • process.nextTick
  • I/O操作
console.log('Start');

setTimeout(() => {
  console.log('Timeout callback');
}, 1000);

console.log('End');

在上述例子中,setTimeout 是一个异步任务,它会在1秒后将回调函数推入任务队列,而主线程不会等待这个1秒,而是继续执行后面的 console.log(‘End’)。当主线程的同步任务执行完成后,它会检查任务队列,将异步任务的回调函数推入执行栈,最终输出 ‘Timeout callback’。

任务队列

任务队列分为宏任务队列(macrotask queue)和微任务队列(microtask queue)两种。JavaScript 引擎遵循事件循环的机制,在执行完当前宏任务后,会检查微任务队列,执行其中的微任务,然后再取下一个宏任务执行。这个过程不断循环,形成事件循环。

1、宏任务包括:

  • 所有同步任务
  • I/O操作,如文件读写、数据库数据读写等
  • setTimeout、setInterval
  • setImmediate(Node.js环境)
  • requestAnimationFrame
  • 事件监听回调函数等

2、微任务包括:

  • Promise的then、catch、finally
  • async/await中的代码
  • Generator函数
  • MutationObserver
  • process.nextTick(Node.js 环境)

在这里插入图片描述

Node.js中的事件循环

Node.js的事件循环比浏览器中的复杂,因为它需要处理I/O操作、网络请求等。Node.js的事件循环由以下几个阶段组成:

  1. timers:执行setTimeout和setInterval的回调。
  2. I/O callbacks:执行除了close事件外的所有I/O操作的回调。
  3. idle, prepare:Node.js内部使用,与libuv的事件循环有关。
  4. poll:等待新的I/O事件,如果timers阶段执行时间太长,poll阶段会被推迟。
  5. check:执行setImmediate的回调。
  6. close callbacks:执行关闭事件的回调,如socket.on(‘close’, …).

多版本Node.js的事件循环

Node.js的事件循环机制在不同版本中有所变化。例如:

  1. Node.js v0.10:使用了较为简单的事件循环模型。
  2. Node.js v0.12:引入了setImmediate,允许在当前事件循环的末尾执行回调。
  3. Node.js 4.x:引入了kPromiseResolve阶段,用于处理Promise的resolve回调。
  4. Node.js 7.x:引入了async_id和async_hooks模块,用于跟踪异步操作。

结尾

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

分析这段代码:

  1. 执行代码,输出script start。
  2. 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。
  3. 遇到setTimeout,产生一个宏任务
  4. 执行Promise,输出Promise。遇到then,产生第一个微任务
  5. 继续执行代码,输出script end
  6. 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  7. 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
  8. 执行await,实际上会产生一个promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
  1. 执行完成,执行await后面的语句,输出async1 end

  2. 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

  • 10
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值