MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript Promise详解与使用场景

2024-05-056.2k 阅读

JavaScript Promise 详解与使用场景

Promise 基础概念

在 JavaScript 异步编程的世界里,Promise 是一个至关重要的概念。简单来说,Promise 是一个表示异步操作最终完成(或失败)及其结果值的对象。它处于三种状态之一:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

  • pending 状态:这是 Promise 对象创建时的初始状态。此时,异步操作正在进行中,尚未有结果。
  • fulfilled 状态:当异步操作成功完成时,Promise 对象会从 pending 状态转变为 fulfilled 状态,并携带一个成功的值(resolved value)。
  • rejected 状态:如果异步操作失败,Promise 对象会进入 rejected 状态,并带有一个表示失败原因的拒因(reason)。

一旦 Promise 对象进入 fulfilled 或 rejected 状态,它就被称为已敲定(settled),状态将不再改变。

创建 Promise 对象

在 JavaScript 中,可以通过 new Promise() 构造函数来创建一个 Promise 对象。Promise 构造函数接受一个执行器函数(executor)作为参数,这个执行器函数会立即执行。执行器函数接受两个参数:resolvereject

const myPromise = new Promise((resolve, reject) => {
    // 模拟异步操作,比如网络请求或定时器
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('操作成功');
        } else {
            reject('操作失败');
        }
    }, 1000);
});

在上述代码中,通过 setTimeout 模拟了一个异步操作,1 秒后根据 success 的值来决定是调用 resolve 还是 reject

Promise 的链式调用

Promise 的强大之处在于它支持链式调用,通过 then() 方法可以为 Promise 注册成功和失败的回调函数。then() 方法返回一个新的 Promise,这使得我们可以将多个 then() 方法链式调用在一起。

myPromise
   .then((result) => {
        console.log(result); // 输出:操作成功
        return '新的返回值';
    })
   .then((newResult) => {
        console.log(newResult); // 输出:新的返回值
    })
   .catch((error) => {
        console.error(error);
    });

在第一个 then() 方法中,我们处理了 Promise 成功的结果,并返回了一个新的值。这个新的值会作为下一个 then() 方法的参数。如果在任何一个 then() 方法中抛出错误,或者返回一个被拒绝的 Promise,那么后续的 catch() 方法将会捕获到这个错误。

Promise 的错误处理

在 Promise 链式调用中,错误处理是非常重要的。可以使用 catch() 方法来捕获 Promise 链中任何一个环节抛出的错误。

const errorPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('这是一个错误');
    }, 1000);
});

errorPromise
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error(error); // 输出:这是一个错误
    });

此外,then() 方法也可以接受第二个参数来处理错误,但使用 catch() 方法更加清晰和统一,尤其是在链式调用中。

Promise 静态方法

Promise.all()

Promise.all() 方法接受一个 Promise 对象的数组作为参数,并返回一个新的 Promise。只有当所有传入的 Promise 都成功时,这个新的 Promise 才会成功,并且它的成功值是一个包含所有传入 Promise 成功值的数组。如果有任何一个传入的 Promise 失败,那么新的 Promise 就会立即失败,其拒因就是第一个失败的 Promise 的拒因。

const promise1 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise 2 成功');
    }, 2000);
});

const promise3 = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise 3 成功');
    }, 3000);
});

Promise.all([promise1, promise2, promise3])
   .then((results) => {
        console.log(results); // 输出:['Promise 1 成功', 'Promise 2 成功', 'Promise 3 成功']
    })
   .catch((error) => {
        console.error(error);
    });

Promise.race()

Promise.race() 方法同样接受一个 Promise 对象的数组作为参数,并返回一个新的 Promise。只要数组中的任何一个 Promise 率先敲定(无论是成功还是失败),这个新的 Promise 就会跟着敲定,其结果(成功值或拒因)就是第一个敲定的 Promise 的结果。

const fastPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve('快速的 Promise 成功');
    }, 1000);
});

const slowPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve('缓慢的 Promise 成功');
    }, 3000);
});

Promise.race([fastPromise, slowPromise])
   .then((result) => {
        console.log(result); // 输出:快速的 Promise 成功
    })
   .catch((error) => {
        console.error(error);
    });

Promise.resolve()

Promise.resolve() 方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 Promise,那么将返回这个 Promise;如果这个值是 thenable(即具有 then() 方法的对象),返回的 Promise 会“跟随”这个 thenable 的状态;否则返回的 Promise 将以此值完成。

const value = '直接值';
const resolvedPromise = Promise.resolve(value);

resolvedPromise.then((result) => {
    console.log(result); // 输出:直接值
});

Promise.reject()

Promise.reject() 方法返回一个被拒绝的 Promise 对象,其拒因就是传入的参数。

const errorPromise = Promise.reject('这是一个拒绝的 Promise');

errorPromise.catch((error) => {
    console.error(error); // 输出:这是一个拒绝的 Promise
});

Promise 使用场景

处理异步操作顺序执行

在实际开发中,经常会遇到需要按顺序执行多个异步操作的情况。例如,先从服务器获取用户信息,然后根据用户信息获取用户的订单列表。

function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ name: '张三', id: 1 });
        }, 1000);
    });
}

function getOrderList(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId) {
                resolve([{ orderId: 101, product: '商品 A' }]);
            } else {
                reject('用户 ID 无效');
            }
        }, 1000);
    });
}

getUserInfo()
   .then((user) => {
        return getOrderList(user.id);
    })
   .then((orders) => {
        console.log(orders); // 输出:[{ orderId: 101, product: '商品 A' }]
    })
   .catch((error) => {
        console.error(error);
    });

并行执行多个异步操作

当有多个异步操作之间没有依赖关系,可以并行执行以提高效率。例如,同时从多个 API 获取不同的数据。

function fetchData1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('数据 1');
        }, 2000);
    });
}

function fetchData2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('数据 2');
        }, 3000);
    });
}

Promise.all([fetchData1(), fetchData2()])
   .then((results) => {
        console.log(results); // 输出:['数据 1', '数据 2']
    })
   .catch((error) => {
        console.error(error);
    });

竞争多个异步操作

有时候需要在多个异步操作中取最先完成的结果。比如,同时向多个服务器发送请求,只要有一个服务器响应就停止其他请求。

function requestServer1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('服务器 1 响应');
        }, 3000);
    });
}

function requestServer2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('服务器 2 响应');
        }, 1000);
    });
}

Promise.race([requestServer1(), requestServer2()])
   .then((result) => {
        console.log(result); // 输出:服务器 2 响应
    })
   .catch((error) => {
        console.error(error);
    });

Promise 与回调地狱的对比

在 Promise 出现之前,JavaScript 处理异步操作主要依赖回调函数。但当异步操作嵌套过多时,就会出现回调地狱(Callback Hell),代码变得难以阅读和维护。

回调地狱示例

getData((data1) => {
    processData1(data1, (result1) => {
        getMoreData(result1, (data2) => {
            processData2(data2, (result2) => {
                // 更多嵌套...
            });
        });
    });
});

使用 Promise 解决回调地狱

getData()
   .then(processData1)
   .then(getMoreData)
   .then(processData2)
   .catch((error) => {
        console.error(error);
    });

通过 Promise 的链式调用,代码变得更加线性,易于理解和维护。

深入理解 Promise 的内部机制

微任务队列

Promise 的异步行为与 JavaScript 的事件循环机制密切相关。当一个 Promise 从 pending 状态转变为 fulfilled 或 rejected 状态时,它会将相关的回调函数放入微任务队列(microtask queue)。微任务队列会在当前调用栈清空后,下一次事件循环开始前被执行。

console.log('开始');

Promise.resolve()
   .then(() => {
        console.log('Promise 回调');
    });

console.log('结束');
// 输出顺序:开始,结束,Promise 回调

在上述代码中,Promise.resolve() 的回调函数被放入微任务队列,在当前调用栈(console.log('开始');console.log('结束'); 执行完毕)清空后才会执行。

Promise 状态转换

Promise 的状态转换是不可逆的。一旦进入 fulfilled 或 rejected 状态,就无法再改变。这种特性保证了 Promise 的行为一致性和可预测性。

const promise = new Promise((resolve, reject) => {
    resolve('已成功');
    reject('这不会生效'); // 此操作无效,因为状态已变为 fulfilled
});

promise.then((result) => {
    console.log(result); // 输出:已成功
}).catch((error) => {
    console.error(error);
});

实际项目中 Promise 的优化与注意事项

错误处理的一致性

在大型项目中,确保 Promise 链中的错误处理一致非常重要。建议在每个 Promise 链的末尾添加 catch() 方法,以捕获可能出现的错误,避免错误被忽略导致难以调试。

function asyncOperation1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('操作 1 失败');
        }, 1000);
    });
}

function asyncOperation2() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('操作 2 成功');
        }, 2000);
    });
}

asyncOperation1()
   .then(() => asyncOperation2())
   .catch((error) => {
        console.error('统一错误处理:', error);
    });

避免内存泄漏

在 Promise 链式调用中,如果不小心在回调函数中创建了循环引用或者没有释放资源,可能会导致内存泄漏。例如,在 Promise 回调中创建 DOM 元素但没有在适当的时候移除。

// 错误示例,可能导致内存泄漏
let element;
const createElementPromise = new Promise((resolve) => {
    setTimeout(() => {
        element = document.createElement('div');
        document.body.appendChild(element);
        resolve();
    }, 1000);
});

createElementPromise
   .then(() => {
        // 这里没有移除 element,可能导致内存泄漏
    });

// 正确示例,及时释放资源
const createAndRemoveElementPromise = new Promise((resolve) => {
    setTimeout(() => {
        const newElement = document.createElement('div');
        document.body.appendChild(newElement);
        setTimeout(() => {
            document.body.removeChild(newElement);
            resolve();
        }, 2000);
    }, 1000);
});

createAndRemoveElementPromise.then(() => {
    // 资源已正确释放
});

性能优化

在使用 Promise.all() 处理大量 Promise 时,可能会对性能产生影响。如果这些 Promise 中有一些是不必要的,可以考虑优化逻辑,减少并行执行的 Promise 数量。

// 假设我们有一个包含大量 Promise 的数组
const largePromiseArray = Array.from({ length: 1000 }, (_, i) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(i);
        }, 100 * i);
    });
});

// 优化前,并行执行所有 Promise
Promise.all(largePromiseArray)
   .then((results) => {
        console.log(results);
    })
   .catch((error) => {
        console.error(error);
    });

// 优化后,只执行必要的 Promise
const filteredPromiseArray = largePromiseArray.filter((_, index) => index < 100);
Promise.all(filteredPromiseArray)
   .then((results) => {
        console.log(results);
    })
   .catch((error) => {
        console.error(error);
    });

通过以上对 JavaScript Promise 的详细讲解和使用场景分析,希望能帮助开发者更深入地理解和运用 Promise,写出更高效、可读的异步代码。在实际项目中,合理利用 Promise 的特性,可以大大提升代码的质量和可维护性。同时,注意错误处理、内存管理和性能优化等方面,以确保项目的稳定性和高效性。无论是处理简单的异步操作还是复杂的并发任务,Promise 都为我们提供了强大而灵活的解决方案。