Day 17:Promise 基础

学习目标:理解异步编程的必要性,掌握 Promise 的基本用法,能够用 Promise 替代回调地狱,理解单线程异步与 C++ 多线程的根本区别。


第一部分:为什么需要异步编程

1.1 同步执行的问题 —— 耗时操作阻塞后续代码

在 C/C++ 中,你一定遇到过这种情况:

// C 语言示例:同步读取文件
FILE *fp = fopen("data.txt", "r");
char buffer[1024];
// 阻塞!直到文件读取完成
fread(buffer, 1, 1024, fp);
// 这行代码必须等上面读完才能执行
printf("读取完成\n");

问题:在 ArkTS 中,如果像上面这样执行耗时操作,会发生什么?

// 假设这是同步的网络请求(伪代码)
let data: string = syncHttpRequest("https://api.example.com/data"); // 阻塞3秒
console.log("请求完成,数据:" + data);
console.log("这行等了3秒才执行");

答案:整个程序会卡住,用户界面无响应。这在 ArkTS 中是致命的,因为 ArkTS 是单线程的。

1.2 C++ 的解决方案 —— 多线程

在 C++ 中,面对耗时操作,标准做法是开新线程:

// C++ 多线程方案
#include <thread>
#include <future>

void fetchData() {
    // 在后台线程执行耗时操作
    std::string data = httpRequest("https://api.example.com/data");
    return data;
}

int main() {
    // 启动异步任务
    std::future<std::string> future = std::async(std::launch::async, fetchData);
    
    // 主线程继续执行,不被阻塞
    std::cout << "请求已发送,继续执行其他代码" << std::endl;
    
    // 需要结果时再获取(可能阻塞,也可能已准备好)
    std::string result = future.get();
    std::cout << "结果:" << result << std::endl;
    
    return 0;
}

C++ 的核心思路:用多线程实现并发,耗时操作丢给子线程,主线程继续跑。

1.3 ArkTS 的解决方案 —— 单线程 + 异步非阻塞

关键认知转变:ArkTS(以及 JavaScript/TypeScript)没有多线程(Worker 是特殊情况,先不考虑)。

那怎么解决阻塞问题?答案是:异步非阻塞 + 事件循环

// ArkTS 异步方案
function fetchData(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        // 模拟异步操作(如网络请求)
        setTimeout((): void => {
            resolve("数据获取成功");
        }, 3000);
    });
}

// 调用异步函数,不会阻塞
fetchData().then((data: string): void => {
    console.log("收到数据:" + data);
});

// 这行代码**立即执行**,不会等上面的3秒
console.log("请求已发送,我继续执行");

输出顺序

请求已发送,我继续执行
(3秒后)
收到数据:数据获取成功

核心区别

特性 C++ 多线程 ArkTS 单线程异步
执行实体 多个 OS 线程 单个事件循环
并发方式 真正并行(多核) 协作式调度(任务队列)
状态共享 需要锁/原子操作 无竞态条件(单线程)
复杂度 线程同步复杂 回调/Promise 链
适用场景 CPU 密集型计算 I/O 密集型操作

1.4 生活类比 —— 餐厅点菜

同步阻塞模式

  • 你走进餐厅,点了一份牛排
  • 你站在柜台前,盯着厨师做,什么都不干
  • 30分钟后牛排好了,你拿着吃
  • 问题:这30分钟你浪费了

异步非阻塞模式

  • 你走进餐厅,点了一份牛排,服务员给你一个"叫号器"
  • 你去找座位、刷手机、聊天(做其他事)
  • 牛排好了,叫号器震动,你去取餐
  • 优势:等待期间你能做其他事

Promise 就是那个"叫号器" —— 它代表一个"未来会完成的操作",完成后通知你。


第二部分:回调的局限(铺垫)

2.1 回调函数回顾

在 Day 07 函数高级中,我们学过回调函数。它是最原始的异步处理方式:

// 模拟异步读取文件(用 setTimeout 模拟延迟)
function readFileAsync(
    path: string,
    callback: (error: Error | null, data: string | null) => void
): void {
    setTimeout(() => {
        if (path === "") {
            callback(new Error("路径不能为空"), null);
        } else {
            callback(null, "文件内容:Hello ArkTS");
        }
    }, 1000);
}

// 使用回调
readFileAsync("test.txt", (err: Error | null, data: string | null) => {
    if (err !== null) {
        console.log("错误:" + err.message);
    } else {
        console.log("成功:" + data);
    }
});

2.2 回调地狱

当多个异步操作需要顺序执行时,代码会变成这样:

// 回调地狱示例:先读配置,再连数据库,再查数据
readConfig("config.json", (err: Error | null, config: string | null) => {
    if (err !== null) {
        console.log("读配置失败");
        return;
    }
    connectDatabase(config, (err: Error | null, db: string | null) => {
        if (err !== null) {
            console.log("连数据库失败");
            return;
        }
        queryData(db, "SELECT * FROM users", (err: Error | null, result: string | null) => {
            if (err !== null) {
                console.log("查询失败");
                return;
            }
            processResult(result, (err: Error | null, output: string | null) => {
                if (err !== null) {
                    console.log("处理失败");
                    return;
                }
                console.log("最终结果:" + output);
            });
        });
    });
});

问题

  1. 代码横向发展,形成"金字塔"结构
  2. 每一层都要处理错误,重复代码多
  3. 逻辑嵌套深,可读性差
  4. 错误处理分散,容易遗漏

2.3 错误处理困难

在回调模式中,每个回调都要单独处理错误:

function doStep1(callback: (err: Error | null, result: string | null) => void): void {
    setTimeout(() => {
        // 50% 概率失败
        if (Math.random() > 0.5) {
            callback(new Error("步骤1失败"), null);
        } else {
            callback(null, "步骤1结果");
        }
    }, 100);
}

// 每一步都要写 if (err) 处理错误
doStep1((err: Error | null, result: string | null) => {
    if (err !== null) {
        // 错误处理 1
        console.log("错误:" + err.message);
        return;
    }
    doStep2(result, (err: Error | null, result2: string | null) => {
        if (err !== null) {
            // 错误处理 2
            console.log("错误:" + err.message);
            return;
        }
        // ... 更多嵌套
    });
});

引出:我们需要一种更好的异步模式,能够:

  1. 扁平化代码结构(不要嵌套)
  2. 集中处理错误(不要每个回调都写错误判断)
  3. 更优雅地组合多个异步操作

这就是 Promise 要解决的问题。


第三部分:Promise 基础

3.1 Promise 是什么

Promise 是对"未来某个时刻会得到的值"的承诺

对比 C++ 的 std::future

// C++ std::future
std::promise<std::string> promise;
std::future<std::string> future = promise.get_future();

// 在另一个线程设置值
promise.set_value("数据准备好了");

// 在主线程获取值
std::string data = future.get(); // 阻塞等待
// ArkTS Promise
let promise: Promise<string> = new Promise<string>((resolve: (value: string) => void): void => {
    setTimeout((): void => {
        resolve("数据准备好了"); // 类似 promise.set_value()
    }, 1000);
});

// 通过 then() 获取结果(非阻塞)
promise.then((data: string): void => {
    console.log("收到:" + data);
});

核心概念

  • Promise 是一个对象,代表一个尚未完成但预期将来会完成的操作
  • 它有三个状态:Pending(进行中)、Fulfilled(成功)、Rejected(失败)
  • 状态一旦改变(Pending → Fulfilled 或 Pending → Rejected),就不可再次改变

3.2 三态模型

┌─────────┐    resolve()    ┌───────────┐
│ Pending │ ──────────────→ │ Fulfilled │
│  (等待) │                 │  (成功)   │
└─────────┘                 └───────────┘
     │
     │ reject()
     ↓
┌───────────┐
│ Rejected  │
│  (失败)   │
└───────────┘

状态规则

  1. Pending:初始状态,操作正在进行中
  2. Fulfilled:操作成功完成,有结果值
  3. Rejected:操作失败,有错误原因
  4. 不可变性:状态一旦改变,不能再变(Fulfilled 不能变 Rejected,反之亦然)
// 状态变化示例
let promise: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
        // 当前状态:Pending

        setTimeout((): void => {
            if (Math.random() > 0.5) {
                resolve("成功"); // 状态变为 Fulfilled
            } else {
                reject(new Error("失败")); // 状态变为 Rejected
            }
        }, 1000);
    }
);

3.3 创建 Promise

语法

new Promise<T>(
    (resolve: (value: T) => void, reject: (reason: Error) => void): void => {
        // 异步操作代码
    }
);

参数说明

  • T:泛型参数,指定 Promise 成功时返回的数据类型(必须显式声明
  • 构造函数接收一个执行器函数,该函数有两个参数:
    • resolve:成功时调用,将 Promise 状态变为 Fulfilled
    • reject:失败时调用,将 Promise 状态变为 Rejected
// 示例:创建一个返回数字的 Promise
let numberPromise: Promise<number> = new Promise<number>(
    (resolve: (value: number) => void, reject: (reason: Error) => void): void => {
        setTimeout((): void => {
            resolve(42);
        }, 1000);
    }
);

// 示例:创建一个返回字符串数组的 Promise
let arrayPromise: Promise<string[]> = new Promise<string[]>(
    (resolve: (value: string[]) => void, reject: (reason: Error) => void): void => {
        setTimeout((): void => {
            resolve(["a", "b", "c"]);
        }, 500);
    }
);

// 示例:创建一个可能失败的 Promise
let riskyPromise: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
        setTimeout((): void => {
            if (Math.random() > 0.5) {
                resolve("运气不错");
            } else {
                reject(new Error("运气不好"));
            }
        }, 100);
    }
);

3.4 使用 resolve 和 reject

resolve 用法

function fetchUserData(userId: number): Promise<string> {
    return new Promise<string>(
        (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
            setTimeout((): void => {
                if (userId <= 0) {
                    reject(new Error("无效的用户ID"));
                } else {
                    // 成功时调用 resolve,传递结果值
                    resolve("用户" + userId.toString() + "的数据");
                }
            }, 1000);
        }
    );
}

// 调用
fetchUserData(100).then((data: string): void => {
    console.log("获取成功:" + data);
});

reject 用法

function divide(a: number, b: number): Promise<number> {
    return new Promise<number>(
        (resolve: (value: number) => void, reject: (reason: Error) => void): void => {
            if (b === 0) {
                // 失败时调用 reject,传递 Error 对象
                reject(new Error("除数不能为0"));
            } else {
                resolve(a / b);
            }
        }
    );
}

// 调用
divide(10, 0).then(
    (result: number): void => {
        console.log("结果:" + result.toString());
    },
    (error: Error): void => {
        console.log("错误:" + error.message);
    }
);

重要规则

  1. resolve 和 reject 只能调用一次,多次调用无效
  2. reject 通常传递 Error 对象(便于错误追踪)
  3. 执行器函数中的同步错误会自动转为 reject
// 同步错误自动转为 reject
let errorPromise: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
        // 这行会抛出异常
        throw new Error("出错了");
        // 等价于:reject(new Error("出错了"));
    }
);

// 可以用 catch 捕获
errorPromise.catch((error: Error): void => {
    console.log("捕获错误:" + error.message);
});

第四部分:Promise 链式调用

4.1 then() 处理成功

基本用法

let promise: Promise<string> = new Promise<string>((resolve: (value: string) => void): void => {
    setTimeout((): void => {
        resolve("Hello");
    }, 1000);
});

// then() 接收一个回调,在 Promise 成功时执行
promise.then((value: string): void => {
    console.log("收到值:" + value);
});

then() 的返回值

then() 方法返回一个新的 Promise,这使得链式调用成为可能:

let promise: Promise<number> = new Promise<number>((resolve: (value: number) => void): void => {
    resolve(10);
});

// then() 返回新的 Promise
let newPromise: Promise<string> = promise.then((value: number): string => {
    return "数字是:" + value.toString();
});

// 可以继续链式调用
newPromise.then((str: string): void => {
    console.log(str); // 输出:数字是:10
});

4.2 catch() 处理失败

基本用法

let promise: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
        reject(new Error("操作失败"));
    }
);

// catch() 捕获 Promise 链中的任何错误
promise.catch((error: Error): void => {
    console.log("捕获到错误:" + error.message);
});

对比 C++ 异常处理

// C++ try/catch
try {
    throw std::runtime_error("出错了");
} catch (const std::exception& e) {
    std::cout << "捕获:" << e.what() << std::endl;
}
// ArkTS Promise catch
new Promise<string>(
    (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
        reject(new Error("出错了"));
    }
).catch((error: Error): void => {
    console.log("捕获:" + error.message);
});

4.3 finally() 最终处理

作用:无论 Promise 成功还是失败,finally 都会执行。类似 C++ 的 try/catch/finally

function doSomething(): Promise<string> {
    return new Promise<string>(
        (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
            setTimeout((): void => {
                if (Math.random() > 0.5) {
                    resolve("成功");
                } else {
                    reject(new Error("失败"));
                }
            }, 1000);
        }
    );
}

doSomething()
    .then((result: string): void => {
        console.log("结果:" + result);
    })
    .catch((error: Error): void => {
        console.log("错误:" + error.message);
    })
    .finally((): void => {
        // 无论成功失败,这里都会执行
        console.log("操作完成(清理资源等)");
    });

适用场景

  • 关闭文件句柄
  • 释放资源
  • 隐藏加载动画
  • 重置状态

4.4 链式调用解决回调地狱

对比:回调地狱 vs Promise 链

// ========== 回调地狱写法 ==========
readConfig("config.json", (err: Error | null, config: string | null) => {
    if (err !== null) {
        console.log("错误:" + err.message);
        return;
    }
    connectDB(config, (err: Error | null, db: string | null) => {
        if (err !== null) {
            console.log("错误:" + err.message);
            return;
        }
        queryData(db, (err: Error | null, data: string | null) => {
            if (err !== null) {
                console.log("错误:" + err.message);
                return;
            }
            console.log("数据:" + data);
        });
    });
});

// ========== Promise 链式写法 ==========
readConfigPromise("config.json")
    .then((config: string) => {
        return connectDBPromise(config);
    })
    .then((db: string) => {
        return queryDataPromise(db);
    })
    .then((data: string) => {
        console.log("数据:" + data);
    })
    .catch((error: Error) => {
        // 统一处理所有错误!
        console.log("错误:" + error.message);
    });

链式调用的优势

  1. 代码扁平化,不再是金字塔
  2. 错误处理集中,一个 catch 捕获所有错误
  3. 每一步的逻辑更清晰

链式调用简写

// 当箭头函数直接返回 Promise 时,可以简写
readConfigPromise("config.json")
    .then((config: string) => connectDBPromise(config))  // 省略 return 和 {}
    .then((db: string) => queryDataPromise(db))
    .then((data: string) => {
        console.log("数据:" + data);
        return processData(data);
    })
    .catch((error: Error) => {
        console.log("错误:" + error.message);
    });

重要:单参数箭头函数必须保留括号,这是 ArkTS 语法要求。


第五部分:Promise 组合方法

5.1 Promise.all —— 全部成功才成功

作用:等待多个 Promise 全部完成,返回一个包含所有结果的数组。

对标 C++:类似等待所有线程完成(std::thread::join 所有线程)。

// 模拟三个独立的异步操作
function task1(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务1结果"), 1000);
    });
}

function task2(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务2结果"), 2000);
    });
}

function task3(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务3结果"), 1500);
    });
}

// 等待所有任务完成
Promise.all([task1(), task2(), task3()])
    .then((results: string[]): void => {
        console.log("所有任务完成");
        console.log("结果1:" + results[0]);
        console.log("结果2:" + results[1]);
        console.log("结果3:" + results[2]);
    })
    .catch((error: Error): void => {
        // 任意一个任务失败,就会进入这里
        console.log("有任务失败:" + error.message);
    });

行为规则

  1. 所有 Promise 都成功 → then() 接收结果数组
  2. 任意一个 Promise 失败 → catch() 捕获第一个错误
  3. 总耗时 = 最慢的那个 Promise 的耗时(并行执行)
// 失败示例
let p1: Promise<string> = Promise.resolve<string>("成功1");
let p2: Promise<string> = Promise.reject<string>(new Error("失败"));
let p3: Promise<string> = Promise.resolve<string>("成功2");

Promise.all([p1, p2, p3])
    .then((results: string[]): void => {
        console.log("不会执行到这里");
    })
    .catch((error: Error): void => {
        console.log("捕获:" + error.message); // 输出:捕获:失败
    });

5.2 Promise.race —— 取最先完成的结果

作用:多个 Promise 竞争,返回最先完成的那个(无论成功或失败)。

对标 C++:类似竞争条件,谁先完成用谁的结果。

// 模拟网络请求超时控制
function fetchWithTimeout(url: string, timeout: number): Promise<string> {
    let fetchPromise: Promise<string> = new Promise<string>(
        (resolve: (value: string) => void): void => {
            setTimeout((): void => {
                resolve("从" + url + "获取的数据");
            }, 2000); // 模拟2秒的网络请求
        }
    );

    let timeoutPromise: Promise<never> = new Promise<never>(
        (resolve: (value: never) => void, reject: (reason: Error) => void): void => {
            setTimeout((): void => {
                reject(new Error("请求超时"));
            }, timeout);
        }
    );

    // 谁先完成用谁的结果
    return Promise.race<string>([fetchPromise, timeoutPromise]);
}

// 设置1秒超时,但请求需要2秒
fetchWithTimeout("https://api.example.com", 1000)
    .then((data: string): void => {
        console.log("获取成功:" + data);
    })
    .catch((error: Error): void => {
        console.log("失败:" + error.message); // 输出:失败:请求超时
    });

重要副作用说明

Promise.race 返回最快完成的 Promise 结果,但其他 Promise 不会自动终止

let fast: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("快"), 100);
    }
);
let slow: Promise<string> = new Promise<string>(
    (resolve: (value: string) => void): void => {
        setTimeout((): void => {
            console.log("slow 执行了");  // 这行仍然会打印!
            resolve("慢");
        }, 1000);
    }
);

Promise.race([fast, slow]).then((result: string): void => {
    console.log("race 结果:" + result);  // "快"
});
// 输出顺序:
// race 结果:快
// slow 执行了   ← slow 的 setTimeout 仍会执行!

原因:Promise 一旦创建就会执行到底,没有内置的取消机制。race 只取结果,不终止执行。

适用场景

  • 请求超时控制
  • 多个数据源竞争(用最快的)
  • 缓存优先 vs 网络优先

5.3 Promise.allSettled —— 等全部完成,不论成败

作用:等待所有 Promise 完成(无论成功或失败),返回每个 Promise 的状态和结果。

对比 Promise.all

方法 成功条件 失败处理
Promise.all 全部成功 一个失败就失败
Promise.allSettled 全部完成(无论成败) 不会失败,返回所有状态

标准 TypeScript 中的用法

在标准 TypeScript 中,Promise.allSettled 返回一个数组,每个元素包含 status("fulfilled" 或 "rejected")和对应的值或错误原因:

// 标准 TypeScript(非 ArkTS)示例
let promises = [
    Promise.resolve("成功1"),
    Promise.reject(new Error("失败1")),
    Promise.resolve("成功2")
];

Promise.allSettled(promises)
    .then((results) => {
        results.forEach((result) => {
            if (result.status === "fulfilled") {
                console.log("成功:" + result.value);
            } else {
                console.log("失败:" + result.reason.message);
            }
        });
    });

ArkTS 中的限制

重要Promise.allSettled 在 ArkTS 中使用受限

原因如下:

  1. 返回联合类型allSettled 返回 PromiseSettledResult<T>[],其中 PromiseSettledResult 是联合类型(fulfilled | rejected)
  2. ArkTS 不支持解构赋值:无法方便地提取 statusvalue
  3. ArkTS 不支持索引访问:无法使用 result["status"] 访问属性

因此,在 ArkTS 中如果需要等待多个 Promise 且处理部分失败,建议:

  • 使用 Promise.all + 统一错误处理
  • 或者逐个处理每个 Promise 的结果

适用场景(概念层面):

  • 批量操作,需要知道每个操作的结果
  • 容错场景,部分失败不影响整体
  • 日志记录、批量上报

第六部分:小结与练习

知识点对比总结表

概念 ArkTS Promise C++ 类比
异步表示 Promise<T> std::future<T>
设置结果 resolve(value) promise.set_value(value)
获取结果 then(callback) future.get()(但非阻塞)
错误处理 reject(error) + catch() 异常机制
等待多个 Promise.all() 多线程 join
竞争 Promise.race() 竞争条件
执行模型 单线程 + 事件循环 多线程并发

单线程异步 vs 多线程并发的根本区别

特性 ArkTS 单线程异步 C++ 多线程并发
并行性 无真正并行(I/O 交给 OS) 真正并行(多核)
竞态条件 不存在(单线程) 需要锁保护
死锁 不可能 可能
适用场景 I/O 密集型 CPU 密集型
编程复杂度 回调/Promise 链 线程同步

练习题

练习 1:基础 Promise 创建

创建一个 delay 函数,接收毫秒数,返回一个 Promise,在指定时间后 resolve。

function delay(ms: number): Promise<void> {
    // 你的代码
}

// 使用
delay(1000).then(() => {
    console.log("1秒后执行");
});
点击查看答案
function delay(ms: number): Promise<void> {
    return new Promise<void>((resolve: () => void): void => {
        setTimeout(resolve, ms);
    });
}

// 使用
delay(1000).then((): void => {
    console.log("1秒后执行");
});

要点

  • new Promise<void> 显式声明泛型为 void
  • setTimeout 可以直接传 resolve,不需要包装

练习 2:Promise 链式调用

实现一个计算流程:先获取数字 5,然后乘以 2,再加 10,最后打印结果。要求使用 Promise 链。

点击查看答案
function getNumber(): Promise<number> {
    return Promise.resolve<number>(5);
}

getNumber()
    .then((value: number): number => {
        return value * 2;
    })
    .then((value: number): number => {
        return value + 10;
    })
    .then((value: number): void => {
        console.log("结果:" + value.toString());  // 20
    });

要点

  • 每个 then 接收前一个 then 的返回值
  • 最后一个 then 返回 void,用于副作用(打印)

练习 3:错误处理

编写一个 safeDivide 函数,接收两个数字,如果除数为 0 则 reject,否则 resolve 商。使用 catch 处理错误。

点击查看答案
function safeDivide(a: number, b: number): Promise<number> {
    return new Promise<number>(
        (resolve: (value: number) => void, reject: (reason: Error) => void): void => {
            if (b === 0) {
                reject(new Error("除数不能为0"));
            } else {
                resolve(a / b);
            }
        }
    );
}

// 使用
safeDivide(10, 0)
    .then((result: number): void => {
        console.log("结果:" + result.toString());
    })
    .catch((error: Error): void => {
        console.log("错误:" + error.message);
    });

要点

  • 除数为 0 时调用 reject,传递 Error 对象
  • 使用 catch 统一捕获错误,比在每个 then 中处理更简洁

练习 4:Promise.all 实践

模拟同时加载三张图片(用 setTimeout 模拟),每张加载时间不同。使用 Promise.all 等待全部加载完成,然后打印"所有图片加载完成"。

点击查看答案
function loadImage(url: string, delay: number): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => {
            console.log("加载完成:" + url);
            resolve(url);
        }, delay);
    });
}

Promise.all<string>([
    loadImage("图片1", 1000),
    loadImage("图片2", 2000),
    loadImage("图片3", 1500)
])
    .then((results: string[]): void => {
        console.log("所有图片加载完成");
        console.log("结果:" + results.join(", "));
    });

要点

  • Promise.all<string> 显式声明泛型
  • 总耗时 = 最慢的那个(2秒)
  • 结果数组顺序与传入顺序一致

练习 5:Promise.race 实践

实现一个函数 fetchWithRetry,尝试从两个不同的 URL 获取数据,使用 Promise.race 取最快返回的结果。

点击查看答案
function fetchFrom(url: string, delay: number): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => {
            resolve("来自 " + url + " 的数据");
        }, delay);
    });
}

function fetchWithRetry(url1: string, url2: string): Promise<string> {
    return Promise.race<string>([
        fetchFrom(url1, 1000),
        fetchFrom(url2, 500)
    ]);
}

// 使用
fetchWithRetry("https://api1.com", "https://api2.com")
    .then((data: string): void => {
        console.log("最快返回:" + data);
    })
    .catch((error: Error): void => {
        console.log("请求失败:" + error.message);
    });

要点

  • Promise.race 返回最先完成的 Promise 结果
  • 较慢的请求仍会执行,只是结果被忽略

练习 6:重构回调地狱

将下面的回调地狱代码重构为 Promise 链:

step1((err: Error | null, result1: string | null) => {
    if (err !== null) {
        console.log("步骤1失败");
        return;
    }
    step2(result1, (err: Error | null, result2: string | null) => {
        if (err !== null) {
            console.log("步骤2失败");
            return;
        }
        step3(result2, (err: Error | null, result3: string | null) => {
            if (err !== null) {
                console.log("步骤3失败");
                return;
            }
            console.log("最终结果:" + result3);
        });
    });
});
点击查看答案
// 假设 step1/step2/step3 已有 Promise 版本
step1Promise()
    .then((result1: string): Promise<string> => {
        return step2Promise(result1);
    })
    .then((result2: string): Promise<string> => {
        return step3Promise(result2);
    })
    .then((result3: string): void => {
        console.log("最终结果:" + result3);
    })
    .catch((error: Error): void => {
        console.log("错误:" + error.message);
    });

要点

  • 每个 then 返回下一个 Promise,实现链式调用
  • 用一个 catch 捕获所有步骤的错误
  • 代码从"金字塔"变成"链式",可读性大幅提升

练习 7:综合应用

实现一个数据加载函数:

  1. 先加载配置(1秒)
  2. 根据配置加载用户数据(1.5秒)
  3. 同时加载订单数据(2秒)
  4. 等用户数据和订单数据都准备好后,合并输出
点击查看答案
function loadConfig(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("配置数据"), 1000);
    });
}

function loadUsers(config: string): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("用户数据"), 1500);
    });
}

function loadOrders(): Promise<string> {
    return new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("订单数据"), 2000);
    });
}

// 执行流程
loadConfig()
    .then((config: string): Promise<string[]> => {
        console.log("配置加载完成:" + config);
        // 同时加载用户数据和订单数据
        return Promise.all<string>([
            loadUsers(config),
            loadOrders()
        ]);
    })
    .then((results: string[]): void => {
        console.log("用户数据:" + results[0]);
        console.log("订单数据:" + results[1]);
        console.log("合并输出完成");
    })
    .catch((error: Error): void => {
        console.log("加载失败:" + error.message);
    });

要点

  • 步骤 1 → 步骤 2 是顺序依赖(需要配置才能加载用户),用 then
  • 步骤 2 和步骤 3 是并行关系,用 Promise.all
  • 总耗时 ≈ 1秒(配置)+ max(1.5秒, 2秒) = 3秒

练习 8:思考讨论

  1. 为什么 ArkTS 选择单线程异步而不是多线程?
  2. Promise 相比回调函数的优势是什么?
  3. 什么情况下应该使用 Promise.all 而不是 Promise.race?
点击查看答案

问题 1:为什么 ArkTS 选择单线程异步而不是多线程?

  • 简化编程模型:单线程避免了锁、竞态条件、死锁等并发问题
  • I/O 密集型场景:UI 应用主要是网络、文件等 I/O 操作,单线程异步足够高效
  • 事件循环机制:通过事件循环调度异步任务,I/O 等待期间 CPU 可以处理其他任务
  • 与浏览器/JavaScript 生态一致:ArkTS 继承自 JavaScript,保持单线程模型

问题 2:Promise 相比回调函数的优势是什么?

  • 代码扁平化:从嵌套金字塔变成链式结构,可读性更好
  • 统一错误处理:一个 catch 捕获链中所有错误,不用每个回调都写错误判断
  • 可组合性Promise.allPromise.race 等方法方便组合多个异步操作
  • 状态可追踪:Promise 有明确的三态(Pending/Fulfilled/Rejected),便于调试

问题 3:什么情况下应该使用 Promise.all 而不是 Promise.race?

  • Promise.all:需要所有任务的结果,例如:

    • 加载多个资源(图片、数据)后才能渲染页面
    • 并行执行多个独立计算,汇总结果
    • 批量操作,需要全部成功才算成功
  • Promise.race:只需要最快完成的任务结果,例如:

    • 请求超时控制
    • 多个数据源竞争,用最快的
    • 缓存优先 vs 网络优先

附录:完整代码示例

示例 1:Promise 基础用法

// 创建 Promise
function fetchData(): Promise<string> {
    return new Promise<string>(
        (resolve: (value: string) => void, reject: (reason: Error) => void): void => {
            console.log("开始获取数据...");
            setTimeout((): void => {
                if (Math.random() > 0.3) {
                    resolve("数据获取成功");
                } else {
                    reject(new Error("网络错误"));
                }
            }, 1000);
        }
    );
}

// 使用 Promise
fetchData()
    .then((data: string): string => {
        console.log("成功:" + data);
        return "处理后的:" + data;
    })
    .then((processed: string): void => {
        console.log(processed);
    })
    .catch((error: Error): void => {
        console.log("失败:" + error.message);
    })
    .finally((): void => {
        console.log("操作结束");
    });

示例 2:Promise 组合使用

// 并行执行多个任务
function parallelTasks(): void {
    let task1: Promise<string> = new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务1"), 1000);
    });

    let task2: Promise<string> = new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务2"), 2000);
    });

    let task3: Promise<string> = new Promise<string>((resolve: (value: string) => void): void => {
        setTimeout((): void => resolve("任务3"), 1500);
    });

    // 等待全部完成
    Promise.all<string>([task1, task2, task3])
        .then((results: string[]): void => {
            console.log("全部完成:" + results.join(", "));
        });

    // 取最快完成的
    Promise.race<string>([task1, task2, task3])
        .then((winner: string): void => {
            console.log("最快完成:" + winner);
        });
}

parallelTasks();
posted @ 2026-04-21 14:21  thammer  阅读(4)  评论(0)    收藏  举报