在前端开发中,Promise 是处理异步操作的重要工具。它通过将异步操作封装在 Promise 实例中,解决了传统回调地狱的问题,提高了代码的可读性和可维护性。虽然 Promise 的概念源自前端,但在 C++11 标准中也引入了 std::promise,用于实现类似的功能。
本文将通过一个基于 C++11 的手写 Promise 实现(命名为 CPromise),深入分析其工作原理,并与 std::promise 进行对比,探讨两者的异同点以及适用场景。
前端 Promise 的应用与优势
常见应用场景
定时器 Promise 可以很好地处理定时器,使代码更加直观:
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
timeout(1000).then(() => {
console.log('1 秒后执行');
});
网络请求 使用 Promise 处理 AJAX 请求,能简化异步数据获取的逻辑:
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log('获取到数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
并发请求
利用 Promise.all 可以同时处理多个异步请求:
const promise1 = fetch('https://api.example.com/data1');
const promise2 = fetch('https://api.example.com/data2');
Promise.all([promise1, promise2])
.then(responses => {
const [data1, data2] = responses.map(response => response.json());
return Promise.all([data1, data2]);
})
.then(([data1, data2]) => {
console.log('两个数据都获取成功:', data1, data2);
})
.catch(error => {
console.error('至少一个请求失败:', error);
});
Promise 解决的问题
- 回调地狱:通过链式调用,解决了传统回调嵌套导致的代码难以阅读和维护的问题。
- 错误处理:提供了统一的错误处理机制,通过
catch方法可以集中处理所有异步操作中的错误。 - 代码可读性:使得异步代码的逻辑更加清晰,符合同步代码的书写习惯。
- 并发控制:通过
Promise.all和Promise.race,可以方便地控制多个异步操作的执行顺序和结果。
手写 C++ Promise 实现
类结构与成员变量
核心在于状态管理和回调队列。我们定义了一个模板类 CPromise:
template<typename Element>
class CPromise {
private:
using Resolve = std::function<void(Element)>;
using Reject = std::function<void(const std::string&)>;
private:
Element m_element; // 异步操作的结果
std::string m_reason; // 拒绝的原因
CPromiseState m_state; // 当前状态
std::list<Resolve> m_resolves; // 成功回调函数列表
std::list<Reject> m_rejects; // 失败回调函数列表
public:
CPromise();
void reject(const std::string& reason);
void resolve(Element element);
void onCatch(const Reject& rej);
CPromise* then(const Resolve& res);
};
这里定义了 Resolve 和 Reject 作为回调类型,分别存储成功和失败的处理逻辑。m_state 表示 Promise 的当前状态,初始为 PENDING。m_resolves 和 m_rejects 则用于暂存注册时的回调函数。
构造函数
初始化时,状态设为 PENDING:
CPromise::CPromise() : m_state(CPromiseState::PENDING) {}
resolve 方法
当异步操作成功时,调用 resolve。如果当前处于 PENDING 状态,则更新状态并立即执行所有已注册的回调:
void CPromise::resolve(Element element) {
m_element = element;
if (m_state == CPromiseState::PENDING) {
m_state = CPromiseState::FULFILLED;
for (Resolve res : m_resolves) {
res(element);
}
}
}
注意这里只允许一次状态变更,一旦完成就不再响应新的 resolve 调用。
reject 方法
类似地,处理失败情况:
void CPromise::reject(const std::string& reason) {
m_reason = reason;
if (m_state == CPromiseState::PENDING) {
m_state = CPromiseState::REJECTED;
for (Reject rej : m_rejects) {
rej(reason);
}
}
}
then 方法
这是实现链式调用的关键。如果 Promise 已完成,立即执行回调;否则将回调加入队列:
CPromise* CPromise::then(const Resolve& res) {
if (m_state == CPromiseState::FULFILLED) {
res(m_element);
} else if (m_state == CPromiseState::PENDING) {
m_resolves.push_back(res);
}
return this;
}
返回 this 是为了支持链式调用,让代码更流畅。
onCatch 方法
用于注册错误处理回调:
void CPromise::onCatch(const Reject& rej) {
if (m_state == CPromiseState::REJECTED) {
rej(m_reason);
} else if (m_state == CPromiseState::PENDING) {
m_rejects.push_back(rej);
}
}
链式调用与使用示例
结合上述方法,我们可以写出类似前端的链式调用:
CPromise<int>* promise = new CPromise<int>();
promise->then([](int ele) -> void {
std::cout << ele << std::endl;
})->onCatch([](const std::string& reason) -> void {
std::cout << reason << std::endl;
});
promise->reject("网络异常!!!");
这种写法逻辑清晰,避免了深层嵌套。
std::promise 与 CPromise 对比
基础功能对比
| 功能 | CPromise 实现 | std::promise |
|---|---|---|
| 状态管理 | 手动实现 | 标准库实现 |
| 回调注册与执行 | 手动实现 | 标准库实现 |
| 异步支持 | 需结合线程 | 内置支持 |
| 链式调用 | 支持 | 不支持 |
实现细节差异
状态管理:CPromise 依赖自定义枚举 CPromiseState,而 std::promise 由标准库内部处理,用户无需关心底层细节。
回调注册:CPromise 维护回调队列,通过 then 和 onCatch 灵活注册;std::promise 通常配合 std::future,通过 get 或 then 触发回调。
异步支持:CPromise 本身不处理线程,需结合 std::thread 或其他框架;std::promise 内置异步语义,常与 std::async 或 std::thread 配合。
链式调用:CPromise 支持链式调用,体验接近前端 Promise;std::promise 原生不支持链式注册回调。
代码示例对比
CPromise 示例:
CPromise<int>* promise = new CPromise<int>();
promise->then([](int ele) -> void {
std::cout << "结果:" << ele << std::endl;
})->onCatch([](const std::string& reason) -> void {
std::cout << "错误:" << reason << std::endl;
});
promise->resolve(42);
std::promise 示例:
#include <future>
#include <thread>
#include <iostream>
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
// 异步操作
std::thread([&prom]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
prom.set_value(42);
}).detach();
// 注册回调
fut.then([](std::future<int> fut) {
try {
int result = fut.get();
std::cout << "结果:" << result << std::endl;
} catch (const std::exception& e) {
std::cout << "错误:" << e.what() << std::endl;
}
});
// 主线程阻塞等待
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
优缺点分析
CPromise
- 优点:代码简洁,易于理解;支持链式调用,使用方式类似前端 Promise;适合学习 Promise 原理。
- 缺点:不支持内置异步操作,需自行结合线程;功能较为基础,缺乏高级特性。
std::promise
- 优点:内置异步支持,与
std::future配合功能强大;标准库实现,性能优化和稳定性有保障;兼容性好。 - 缺点:使用方式相对复杂,缺乏链式调用支持;回调机制灵活性不如手写实现。
总结
通过手写 CPromise,我们可以深入理解 Promise 的核心机制,包括状态流转、回调队列管理等。在实际工程中,std::promise 依然是更稳健的选择,因为它提供了更强的功能和更好的性能保障。
对于开发者而言,理解手写实现的原理有助于更好地把握 std::promise 的工作方式,从而根据具体场景选择合适的工具来处理异步操作。


