JavaScript 异步编程和事件循环详解

JavaScript 异步编程和事件循环详解

⏲️建议阅读时间: 15min

1. 异步编程的意义

什么是异步编程,解决了什么问题

异步编程是什么呢?

简单说,异步编程就是不需要等待一个任务执行完毕,就可以继续执行其他任务的编程方式。

🌰举个生活中的例子,平时我们做饭需要按顺序一步一步来,必须等一样做完才能做下一样。但是如果让好几个人一起做饭,他们就可以异步进行,同时做多件事,效率更高。(但是也不是所有的任务都可以异步,比如生孩子)

编程也是一样,默认是同步执行,只有前一个任务做完,才能执行后一个任务。异步编程允许我们在等待一个任务完成的同时,继续执行其他任务。

异步编程解决了什么问题呢?

主要解决了程序中一些不必要的等待时间问题。比如程序中有某些任务需要等待网络请求,文件读取,用户输入等情况。这种需要等待的时间对程序来说是"空转"时间。

📖浏览器和 Node.js 中的异步应用场景

在浏览器和 Node.js 中,异步编程很常见,主要应用在以下场景:

  • 网络请求。比如 Ajax 请求,需要等待服务器响应,可以异步执行。
  • 文件操作。读取文件、访问数据库等 IO 操作,都可能需要等待,可以异步处理。
  • 用户输入。比如点击、输入等用户交互,不能假设用户的响应时间,需要异步处理。
  • 定时任务。设置定时器执行某任务,也是典型的异步操作。

通过异步编程,这些需要等待的任务不会造成整个程序的阻塞,可以优化程序执行流程,提高用户体验。

所以异步编程对于今天的网络应用和交互程序来说,是非常重要的编程方式。正确使用异步编程可以让你的程序更高效、更流畅。

2. 事件循环机制简介

💡事件循环的概念和原理

什么是事件循环呢?

事件循环是一种编程模式。它的核心思想是维护一个事件队列,循环检查队列,处理队列中的事件。

我们比较一下同步模型和事件循环模型:

  • 同步模型:程序按顺序依次执行任务,必须等待前一个任务结束才能执行下一个任务。
  • 事件循环模型:程序维护一个待处理事件队列,循环检查队列,找出需要处理的事件并触发回调函数,然后再循环检查队列......这样一个无结束的循环。

事件循环允许我们注册事件和对应的回调函数,然后程序会自动按照发生顺序调用这些回调函数。

举个例子,假设我们注册了点击和滚动事件的监听器函数,然后点击并滚动页面,事件循环内部会自动调用注册的回调函数处理点击和滚动事件。

通过这样的机制,我们就可以采用异步编程方式来响应事件,不需要像同步模型那样等待事件处理完再做其他事情。

🚀事件循环解决的核心问题

  • 可以异步处理并发事件,不需要阻塞程序等待。
  • 可以 registers 多个事件和回调函数,事件循环会自动调用它们。
  • 不同事件可以共享同一个线程,不需要为每个事件创建新线程。
  • 可以将事件队列作为线程间通信的消息通道。

3. JavaScript 中的事件循环

💻浏览器环境下的事件循环(Event Loop)

在浏览器环境下,JavaScript 语言实现了一个 Event Loop 来处理事件循环。

它的工作流程是这样的:

  1. 所有同步任务直接在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

所以浏览器的事件循环是通过任务队列来实现的。异步任务被抛到队列中,等待同步任务结束,然后从队列中读取事件并执行。

🖥️Node.js 环境下的事件循环

而在 Node.js 中,它实现了一个 Event Loop 来处理事件,与浏览器略有不同:

  1. Node 的 Event Loop 只包含 6 个阶段,它们会控制事件的执行。
  2. 每个阶段都有一个先进先出的回调队列,只有回调队列清空后才会进入下一阶段。
  3. 通过这种阶段控制机制,Node 实现了非阻塞 I/O 模型。

🔍两者的区别和联系

区别:

  1. 浏览器的事件循环是单一的,而 Node.js 有多个阶段的事件循环。
  2. 浏览器将异步任务统一放入任务队列,Node.js 每个阶段有独立的回调队列。
  3. 浏览器没有阶段的概念,所有任务都在一个队列里,Node.js 通过不同阶段来控制任务执行。
  4. 浏览器的事件循环是由 HTML5 标准定义,Node.js 的事件循环规范是独立的。

联系:

  1. 它们底层都实现了事件循环机制,可以处理异步任务而不阻塞主线程。
  2. 都维护一个队列来存储待处理事件,然后循环查找新的事件。
  3. 都会在主线程空闲时,从队列中取出事件处理回调函数执行。
  4. 都允许非阻塞 IO 和事件驱动的编程方式。
  5. 都需要避免长时间运行的任务 blocking 主线程。
  6. 它们帮助 JavaScript 实现了异步非阻塞的并发模型。

4. 🧰事件循环的作用

🔑启用异步编程

事件循环的第一个作用是启用异步编程。

通过事件循环,我们可以把一些需要等待结果的任务注册为异步任务,不会阻塞主线程,非常适合处理诸如网络请求、文件 I/O 等异步操作。

举个例子,在浏览器中发一个 Ajax 请求。我们注册一个回调函数来接收响应,然后该请求被加入事件循环的"任务队列"。我们的主线程可以接着做其他工作,当这个 Ajax 响应返回的时候,对应的回调函数会在事件循环中被调用。这样就实现了异步编程方式。

🔗调度任务执行

事件循环的第二个作用是调度任务执行。

事件循环会按照正确的顺序执行回调函数,比如先点击按钮,后滚动页面,事件循环会保证先调用点击的回调函数,然后再调用滚动的回调。这样就实现了调度作用。

🔧优化性能

事件循环的第三个作用是优化性能。

通过异步非阻塞的方式执行任务,可以大大减少程序中的等待时间,充分利用 CPU,也节约了线程资源,非常高效。

🧯避免长时间阻塞

最后,事件循环避免了长时间运行的任务 blocking 主线程。

因为事件循环会按照队列顺序执行回调函数,所以每个回调函数不能运行太长时间,否则会堵塞后面的任务。通过这种方式,事件循环很好的避免了单个任务的长时间阻塞。

5. 👓常见的异步编程方法

回调函数

第一种是回调函数。一个函数可以接受另一个函数作为参数,这个作为参数传递进去的函数就叫回调函数。回调函数通常在当前任务完成后被执行。

例如,我们可以注册一个回调函数来处理 Ajax 返回的结果:

js 复制代码
function ajaxCallback(result) {
  console.log(result);
}

ajax("http://example.com", ajaxCallback);

Promise

Promise 是一个对象,代表了一个异步任务的最终完成或失败。我们可以在 Promise 对象上注册回调函数。

使用 Promise 的话,代码如下:

js 复制代码
let p = new Promise();

p.then(function (result) {
  console.log(result);
});

p.catch(function (err) {
  console.log(err);
});

async/await

async/await,这是基于 Promise 实现的语法糖。async 函数会返回一个 Promise 对象,await 等待 Promise 结果:

js 复制代码
async function myFunc() {
  let result = await doSomethingAsync();

  console.log(result);
}

事件和监听器

最后是事件和监听器,这是浏览器异步编程的主要方式。例如可以为按钮添加点击事件:

js 复制代码
let btn = document.getElementById("myBtn");

btn.addEventListener("click", function () {
  // 收到点击事件后的处理
});

6. 实现一个简单的事件循环

往往事件循环代表了应用的整个生命周期,事件循环结束意味着应用也就退出了。

我们先来看一个简陋版的事件循环系统。

js 复制代码
class EventSystem {
  constructor() {
    // 任务队列
    this.queue = [];
  }

  // 追加任务
  enQueue(func) {
    this.queue.push(func);
  }

  // 事件循环
  run() {
    while (1) {
      while (this.queue.length) {
        const func = this.queue.shift();
        func();
      }
    }
  }
}
// 新建一个事件循环系统
const eventSystem = new EventSystem();

// 生产任务
eventSystem.enQueue(() => {
  console.log("hi");
});

// 启动事件循环
eventSystem.run();

// 生产任务
eventSystem.enQueue(() => {
  console.log("hi");
});

根据上面我们了解到的知识,我们可以知道这个简陋版的事件循环逻辑如下

  1. 新建一个事件循环系统;
  2. 生产任务;
  3. 启动事件循环系统处理任务。

但是!如果你运行就会发现,当没有任务时,它就会陷入死循环,这不仅浪费了 CPU,新的任务也无法继续添加了。

因此,在一个事件循环系统中,如何处理没有任务时的情况非常关键。这应该怎么解决呢?

js 复制代码
class EventSystem {
  constructor() {
    // 需要处理的任务队列
    this.queue = [];
    // 标记是否需要退出事件循环
    this.stop = 0;
    // 有任务时调用该函数"唤醒" await
    this.wakeup = null;
  }
  // 没有任务时,事件循环的进入"阻塞"状态
  wait() {
    return new Promise((resolve) => {
      // 记录 resolve,可能在睡眠期间有任务到来,则需要提前唤醒
      this.wakeup = () => {
        this.wakeup = null;
        resolve();
      };
    });
  }
  // 停止事件循环,如果正在"阻塞",则"唤醒它"
  setStop() {
    this.stop = 1;
    this.wakeup && this.wakeup();
  }
  // 生产任务
  enQueue(func) {
    this.queue.push(func);
    this.wakeup && this.wakeup();
  }

  // 处理任务队列
  handleTask() {
    if (this.queue.length === 0) {
      return;
    }
    // 本轮事件循环的回调中加入的任务,下一轮事件循环再处理,防止其他任务没有机会处理
    const queue = this.queue;
    this.queue = [];
    while (queue.length) {
      const func = queue.shift();
      func();
    }
  }

  // 事件循环的实现
  async run() {
    // 如果 stop 等于 1 则退出事件循环
    while (this.stop === 0) {
      // 处理任务,可能没有任务需要处理
      this.handleTask();
      // 处理任务过程中如果设置了 stop 标记则退出事件循环
      if (this.stop === 1) {
        break;
      }
      // 没有任务了,进入睡眠
      if (this.queue.length === 0) {
        await this.wait();
      }
    }
    // 退出前可能还有任务没处理,处理完再退出
    this.handleTask();
  }
}

// 新建一个事件循环系统
const eventSystem = new EventSystem();

// 启动前生成的任务
eventSystem.enQueue(() => {
  console.log("1");
});

// 模拟定时生成一个任务
setTimeout(() => {
  eventSystem.enQueue(() => {
    console.log("3");
    eventSystem.setStop();
  });
}, 1000);

// 启动事件循环
eventSystem.run();

// 启动后生成的任务
eventSystem.enQueue(() => {
  console.log("2");
});

现在我们就有了一个简单但是核心功能都有的事件循环系统。

  1. 事件循环的整体架构是一个 while 循环。
  2. 定义任务类型和队列,这里只有一种任务类型和一个队列,比如 Node.js 里有好几种,每种类型的任务有不同的作用。
  3. 没有任务的时候怎么处理:进入阻塞状态,而不是靠忙轮询。

第 3 点是事件循环系统中非常重要的逻辑,任务队列中不可能一直都有任务需要处理,这就意味着生产任务可以是一个异步的过程,所以事件循环系统就需要有一种等待 / 唤醒机制。

7. 总结

🔔异步编程模式的意义

异步编程模式非常重要,它允许我们在等待某个操作时继续执行其他代码,大大提高了效率。

其核心是事件循环机制。它维护一个事件队列,然后循环检查队列并执行回调函数,这样就实现了异步执行流程。

JavaScript 语言利用事件循环实现了异步编程。比如,在浏览器中我们可以注册事件回调函数,Node.js 也使用事件循环处理非阻塞 IO。

💾事件循环的工作机制

  1. 事件循环的整体架构是一个 while 循环。
  2. 定义任务类型和队列,这里只有一种任务类型和一个队列,比如 Node.js 里有好几种,每种类型的任务有不同的作用。
  3. 没有任务的时候怎么处理:进入阻塞状态,而不是靠忙轮询。

💻在 JavaScript 中高效利用事件循环

要在 JavaScript 中高效利用事件循环,我们可以:

  1. 多用异步 API,比如定时器、Promise 等,避免阻塞。
  2. 合理使用异步语法 async/await 来组织代码。
  3. 避免长时间运行的任务阻塞线程。
  4. 使用 Promise 链或 async/await 处理回调地狱。
  5. 合理使用定时器和请求拆分来控制并发。
  6. 使用事件循环可视化工具观察事件循环状态。
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax