JavaScript 异步编程深度解析:从事件循环到 Promise 实战

0 阅读5分钟

同步与异步:代码的执行顺序

在 JavaScript 中,代码的编写顺序并不总是等于执行顺序。让我们从一个基础示例开始:

console.log(1);

// setTimeout 是异步操作,不会阻塞后续代码
// 3000毫秒后,回调函数会被放入事件队列等待执行
setTimeout(function(){
    console.log(2);
}, 3000);

console.log(3);

// 执行结果:1 → 3 → 2
// 解释:同步代码立即执行,异步代码等待同步代码执行完毕后再执行

为什么 JavaScript 需要异步?

单线程的挑战

JavaScript 是单线程的脚本语言,只能同时执行一个任务。这意味着如果所有代码都同步执行,遇到耗时操作时整个页面就会卡住。

事件循环(Event Loop)机制

当遇到异步代码时(如 setTimeout),JavaScript 不会等待,而是:

  1. 将异步任务放入事件循环中
  2. 继续执行后续同步代码
  3. 等同步代码执行完毕后,从事件循环中取出异步任务执行
console.log("开始执行");

setTimeout(() => {
    console.log("3秒后执行的异步任务");
}, 3000);

console.log("同步任务继续执行");

// 输出顺序:
// 开始执行
// 同步任务继续执行  
// 3秒后执行的异步任务

Promise:异步任务的同步化管理

回调地狱问题

在 Promise 出现之前,多层嵌套的回调函数会导致"回调地狱",代码难以阅读和维护。

Promise 的基本概念

Promise 是一个表示异步操作的对象,它有三种状态:

  • pending:初始状态
  • fulfilled:操作成功完成
  • rejected:操作失败
console.log(1);

// 创建 Promise 实例,执行器函数会立即执行
const p = new Promise((resolve) => {
    console.log("Promise 执行器开始"); // 同步执行
    
    setTimeout(function () {
        console.log(2);
        resolve("异步操作完成"); // 调用 resolve 并传递结果
        // resolve 的参数会传递给 then 方法的回调函数
    }, 3000);
});

// then 方法注册成功回调
p.then((result) => {
    console.log(3);
    console.log("接收到resolve传递的数据:", result); // 输出:异步操作完成
});

console.log(4);

// 执行结果:
// 1
// Promise 执行器开始
// 4
// 2
// 3
// 接收到resolve传递的数据: 异步操作完成

Promise 实战:文件读取

让我们看一个实际的 Promise 应用场景——文件读取:

import fs from 'fs';

console.log(1);

const p = new Promise((resolve, reject) => {
    console.log(3); // 同步任务,立即执行
    
    // 异步文件读取
    fs.readFile('./b.txt', function (err, data) {
        if (err) {
            // 文件读取失败,调用 reject 并传递错误信息
            // reject 的参数会传递给 catch 方法的回调函数
            reject({ 
                message: '文件读取失败', 
                error: err 
            });
            return;
        }
        // 文件读取成功,调用 resolve 并传递文件内容
        // resolve 的参数会传递给 then 方法的回调函数
        resolve({
            message: '文件读取成功',
            content: data.toString()
        });
    });
});

p.then((successResult) => {
    console.log('成功回调:', successResult.message);
    console.log('文件内容:', successResult.content);
}).catch((errorResult) => {
    console.log('失败回调:', errorResult.message);
    console.log('错误详情:', errorResult.error);
});

console.log(2);

// 执行顺序:1 → 3 → 2 → 成功回调/失败回调

关键点解析:

  1. new Promise 中的同步代码会立即执行(输出 3)
  2. 异步操作(文件读取)被放入事件循环
  3. 主线程继续执行同步代码(输出 2)
  4. 文件读取完成后,根据结果调用 resolvereject
  5. resolve/reject 的参数会自动传递给对应的 then/catch 回调

网络请求中的 Promise

在实际开发中,Promise 最常见的应用场景就是网络请求:

<ul id="members">
    </ul>


    <script>
        //异步变同步,让 then 里的回调函数 等 fetch 拿到数据后再执行
        fetch('https://api.github.com/orgs/lemoncode/members')
        .then(data => data.json())
        .then(res => {
            console.log(res);
            document.getElementById("members").innerHTML = 
            res.map(item => `<li>${item.login}</li>`).join('');
        })

        console.log(fetch('https://api.github.com/orgs/lemoncode/members')); // Promise对象
    </script>

理解:为什么 fetch() 打印出的是 Promise 对象?

console.log(fetch('https://api.github.com/orgs/lemoncode/members')); 
// 立即输出:Promise {<pending>}

简单解释

fetch() 被调用时,会立即返回一个 Promise 对象,而不是等待网络请求完成。

工作原理

  • fetch() 内部创建并返回一个 new Promise
  • 实际的网络请求在后台异步执行
  • 当网络请求完成时,Promise 会自动调用 resolve(响应数据)reject(错误信息)
  • 我们通过 .then() 来接收 resolve 传递的响应数据

类比理解

// fetch 的内部原理类似这样:
function fetch(url) {
    return new Promise((resolve, reject) => {
        // 在后台发起网络请求
        // 请求完成后调用:
        // resolve(响应数据) 或 reject(错误信息)
    });
}

所以 console.log(fetch(...)) 打印的是立即返回的 Promise 对象,而不是网络请求的结果。

Promise 执行机制

new Promise 接受一个执行函数,该函数接收两个参数:resolvereject

resolve 机制

const promise = new Promise((resolve, reject) => {
    // 执行异步任务
    setTimeout(() => {
        resolve('任务完成'); // 调用 resolve
    }, 1000);
});

promise.then((result) => {
    console.log(result); // 输出:'任务完成'
});
  1. resolve(value)
    • 将 Promise 状态改为 fulfilled
    • value 参数会传递给第一个 then 的成功回调
    • 可以传递任何数据类型(对象、数组、字符串等)

reject 机制

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('任务失败'); // 调用 reject
    }, 1000);
});

promise.catch((error) => {
    console.log(error); // 输出:'任务失败'
});
  1. reject(reason)
    • 将 Promise 状态改为 rejected
    • reason 参数会传递给 catchthen 的失败回调
    • 通常传递错误对象或错误信息

关键点

  • 必须调用 resolve 或 rejectthencatch 才会执行
  • 如果既不 resolve 也不 reject,Promise 会一直处于 pending 状态,then/catch 永远不会执行
  • resolve 和 reject 只能调用一次,多次调用无效
// 错误示例:没有调用 resolve/reject
const stuckPromise = new Promise((resolve, reject) => {
    // 这里没有调用 resolve 或 reject
    console.log('执行了,但没有结果');
});

stuckPromise.then(() => {
    console.log('这行永远不会执行'); // 不会输出
});

总结

JavaScript 的异步编程核心在于理解事件循环机制和 Promise 工作流程。通过事件循环,单线程的 JavaScript 能够高效处理异步任务而不阻塞主线程;而 Promise 则通过 resolve/reject 状态转换和参数传递机制,为异步操作提供了清晰的执行路径和错误处理方案。掌握这些基础概念,是编写现代 JavaScript 异步代码、避免回调地狱和构建响应式应用的关键所在。