简介

JavaScript 中的异步编程是前端开发中不可或缺的一部分。异步编程的主要目标是避免阻塞主线程,保证应用的流畅性和响应性。JavaScript 采用了几种不同的方式来实现异步编程,其中最常见的包括 Promise、async/await 和事件循环机制

JavaScript 是单线程语言,这意味着它一次只能处理一个任务。为了避免长时间的阻塞,JavaScript 引入了异步编程,允许程序在执行耗时操作(如网络请求、文件操作等)时,不会阻塞主线程,保持 UI 响应性


事件循环

事件循环(Event Loop)是一种用于处理和调度事件的机制,广泛应用于异步编程模型中。它通常在单线程环境中运行,通过不断地检查和处理事件队列来实现并发任务的顺序执行

事件循环的核心思想是将并发任务转化为顺序任务,通过轮询和非阻塞方式来解决并发问题。这种方式可以提高程序的响应性和吞吐量,并减少系统资源的浪费。事件循环主要由两个组件构成:事件队列和事件处理器。事件队列是一个先进先出(FIFO)的数据结构,用于存储待处理的事件;而事件处理器则负责从队列中取出事件并执行相应的处理逻辑

在JavaScript中,事件循环是其运行时环境处理异步操作的核心机制。它允许JavaScript在不阻塞单线程执行的情况下,响应用户交互、处理网络请求、定时器回调等异步事件。事件循环的工作流程包括以下几个步骤:

  • 执行同步代码:JavaScript引擎首先执行同步代码,按照顺序执行函数调用和表达式求值,直到遇到异步操作或事件
  • 检查异步任务:当遇到异步操作或事件时,它们将被放置在相应的任务队列中,而不会立即执行
  • 处理宏任务和微任务:事件循环从宏任务队列中取出一个任务执行,然后检查微任务队列,执行所有微任务。这个过程会一直重复下去,直到所有队列都为空

在每个宏任务执行完毕后,事件循环会再次检查微任务队列,并执行其中的所有任务。这个过程确保了高效的异步处理。理解事件循环的工作原理有助于编写高性能和响应迅速的JavaScript应用程序

总之,事件循环是处理持续运行程序中事件的关键机制,其设计和实现对于优化程序性能和适应不同应用场景至关重要

宏任务

宏任务(MacroTask)是指那些独立、离散的工作单元,它们通常包括以下几种类型:

  • script标签中的脚本执行
  • setTimeout、setInterval
  • 网络请求(如Ajax)
  • 文件操作
  • UI渲染等

微任务

微任务(MicroTask)是指那些更小的任务,它们通常包括以下几种类型:

  • Promise.then 、.catch、.finally
  • process.nextTick (Node.js 环境)
  • MutationObserver等

微任务总是先于宏任务执行

事件循环的工作流程可以总结为以下步骤:

  1. 执行栈选择最先进入队列的宏任务(通常是整体代码),如果有则执行
  2. 检查是否存在微任务,如果存在则不停地执行,直至清空微任务队列
  3. 更新渲染(每一次事件循环,浏览器都可能会去更新渲染)
  4. 重复以上步骤,直到所有宏任务和微任务都已执行完毕

Promise

PromiseJavaScript 中用于处理异步操作的一种机制,它代表一个可能还未完成的操作,并且提供了 then 和 catch 方法来处理成功和失败的回调,摆脱了传统异步编程的回调地狱

Promise三种状态:

  • Pending(进行中):初始状态,操作未完成
  • Fulfilled(已成功):操作成功完成
  • Rejected(已失败):操作失败

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变

promise是同步还是异步?

Promise 是用来管理异步编程的,它本身不是异步的 ,promise是同步代码,示例:

let promise = new Promise((resolve, reject) => {
    console.log(1);
})
console.log(2);
console.log(promise);
// 1
// 2
// Promise {<pending>}

promise的回调then,catch是异步,示例:

let promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve()
})
 
promise.then(function() {
  console.log('resolved.');
});
 
console.log(2);
 
// 1  2  resolved

Promise新建后立即执行,所以首先输出的是1,其次2。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出

用法

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例

// 基本语法
const promise = new Promise((resolve, reject) => {
    // 异步操作
    if (/*成功*/) {
        resolve(value);
    } else {
        reject(error);
    }
});

// 链式调用
promise
    .then(value => {
        // 处理成功
    })
    .catch(error => {
        // 处理错误
    })
    .finally(() => {
        // 总是执行
    });

// Promise方法
Promise.all([p1, p2, p3]) // 所有Promise都完成
Promise.race([p1, p2, p3]) // 第一个完成的Promise
Promise.allSettled([p1, p2, p3]) // 所有Promise都结束
Promise.any([p1, p2, p3]) // 第一个成功的Promise

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署

  1. resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  2. reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
  3. Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
  4. Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例,率先改变的 Promise 实例的返回值则传递给回调函数

async/await

async用于申明一个function是异步的;而await则可以认为是 async await的简写形式,是等待一个异步方法执行完成的。
它是消灭异步回调的终极武器;async/await 是基于 Promise 的语法糖,它让异步代码看起来像同步代码,也就是用同步的写法写异步的代码,使得代码更加简洁易读

相较于Promise,async/await有何优势?

  1. 同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的)
  2. 和同步代码更一致的错误处理方式( async/await 可以⽤成熟的 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
  3. 同步代码和异步代码可以一起编写,调试时的阅读性, 也相对更友好
  4. Promise并不排斥,两者相辅相成,await 相当于 Promise 的 then ,then指的是成功,不指失败

使用规则

  • async 表示这是一个async函数, await只能用在async函数里面,不能单独使用
  • async 返回的是一个Promise对象,await就是等待这个promise的返回结果后,再继续执行
  • await 等待的是一个Promise对象,后面必须跟一个Promise对象,但是不必写then(),直接就可以得到返回值
  • 异步async 调用和普通函数的使用方式一样
// 基本语法
async function getData() {
    try {
        const result = await fetchData();
        return result;
    } catch (error) {
        console.error(error);
    }
}

// 并行执行
async function parallel() {
    const [result1, result2] = await Promise.all([
        fetch('url1'),
        fetch('url2')
    ]);
}

// 循环中使用
async function processArray(array) {
    // 串行执行
    for (const item of array) {
        await processItem(item);
    }
    
    // 并行执行
    const promises = array.map(item => processItem(item));
    await Promise.all(promises);
}

示例解析

示例1

// 事件循环示例
console.log('1'); // 同步

setTimeout(() => {
    console.log('2'); // 宏任务1
    Promise.resolve().then(() => {
        console.log('3'); // 宏任务1中的微任务
    });
}, 0);

Promise.resolve().then(() => {
    console.log('4'); // 微任务1
    setTimeout(() => {
        console.log('5'); // 微任务1中的宏任务
    }, 0);
});

console.log('6'); // 同步

// 输出:1, 6, 4, 2, 3, 5

示例2

function timeout(){
  return new Promise(resolve=>{
    setTimeout(()=>{
      console.log(1)
      resolve()//成功态
    })
  })
}

//情况一
async function fn(){
  timeout()
  console.log(2)
}
fn() //打印出 2 一秒后再打印 1

//情况2
async function fn(){
  await timeout()
  console.log(2)
}
fn() //1秒后打印出 1 2

示例3

function timeout(){
  return new Promise(resolve=>{
    setTimeout(()=>{
      resolve(1)//成功态,传递参数1
    })
  })
}
async function fn(){
  const res = await timeout() //需要一个变量来接收await return的参数
  console.log(res)
  console.log(2)
}
fn() //1秒后打印出 1 2

Last modification:January 8, 2025
如果觉得我的文章对你有用,您可以给博主买一杯果汁,谢谢!