name: rn-async-patterns description: Async/await 正确性在 React Native 中使用 Zustand。当调试竞态条件、缺失 awaits、悬浮承诺或在 Expo/React Native 应用中的异步定时问题时使用。
React Native 异步模式
问题陈述
React Native 中的异步错误是隐蔽的,因为它们通常在开发中可以工作,但在负载下或在较慢的设备上失败。最常见的问题:缺失对异步函数的 await、状态更新之间的竞态条件,以及假设操作按顺序完成。
模式:悬浮承诺检测
问题: 调用异步函数而不使用 await 会导致它在后台运行。如果后续代码依赖于其完成,您将获得竞态条件。
示例(来自重拍错误):
// 之前(有缺陷)- enableSkillAreaRetake 是异步的但没有被等待
enableSkillAreaRetake(skillArea); // 先开火后忘记 ❌
await clearSkillAreaAnswers(skillArea); // 在 enable 完成前运行
// 之后(已修复)
await enableSkillAreaRetake(skillArea); // 等待状态更新 ✅
await clearSkillAreaAnswers(skillArea); // 现在按正确的顺序运行
为什么它是微妙的: 两个函数可能在其签名中都有 async,但只有一个被等待了。代码乍一看“看起来是对的”。
检测:
# 查找潜在的悬浮承诺 - 没有 await 的异步调用
grep -rn "^\s*[a-zA-Z]*\s*(" --include="*.ts" --include="*.tsx" | \
grep -v "await\|return\|const\|let\|if\|else\|=>"
预防:
- ESLint 规则
@typescript-eslint/no-floating-promises- 在 lint 时间捕获这个问题 - 代码审查触发器:任何调用可能异步的函数的行,没有
await、return或赋值
模式:后置条件验证
问题: 假设异步调用成功而没有验证。调用可能提前返回,默默抛出,或未能更新状态。
示例(来自重拍错误):
// 之前(有缺陷)- 假设加载工作了
await loadCompletedAssessmentAnswers(id);
// 盲目继续重拍流程...
// 之后(防御性)
await loadCompletedAssessmentAnswers(id);
const loaded = useAssessmentStore.getState().completedAssessmentAnswers;
if (Object.keys(loaded).length === 0) {
throw new Error(
`Failed to load answers for assessment ${id} - cannot proceed with retake`
);
}
原则: 将每个异步调用视为可能失败,直到证明否则。
何时验证:
- 加载数据后,后续操作依赖于这些数据
- 状态更新后,必须在继续之前完成
- 在不可逆操作之前(提交、删除)
模式模板:
await someAsyncOperation();
const result = getRelevantState();
if (!isValid(result)) {
throw new Error(`[${functionName}] Post-condition failed: ${diagnosticContext}`);
}
模式:异步函数识别
问题: 不是所有异步函数看起来都是异步的。Zustand 动作、回调和返回承诺的函数可能没有明显的 async 关键字。
隐藏的异步模式:
// 明显的异步
async function fetchData() { ... }
// 较不明显 - 返回 Promise
function fetchData(): Promise<Data> { ... }
// 隐藏 - 实际上异步的 Zustand 动作
const useStore = create((set, get) => ({
// 这看起来是同步的,但内部调用了异步
enableRetake: (area: string) => {
someAsyncSetup().then(() => { // ← 隐藏的异步!
set({ retakeAreas: [...get().retakeAreas, area] });
});
},
}));
// 正确的异步 Zustand 动作
const useStore = create((set, get) => ({
enableRetake: async (area: string) => {
await someAsyncSetup();
set({ retakeAreas: [...get().retakeAreas, area] });
},
}));
检测: 检查函数签名和实现:
# 查找返回 Promise 的函数
grep -rn "): Promise<" --include="*.ts" --include="*.tsx"
# 查找可能需要 await 的 .then() 链
grep -rn "\.then(" --include="*.ts" --include="*.tsx"
模式:顺序与并行异步
问题: 当它们可以并行时,顺序运行异步操作(慢),或者当它们必须是顺序时并行(竞态条件)。
// 顺序 - 当顺序重要时正确
await enableSkillAreaRetake(skillArea);
await clearSkillAreaAnswers(skillArea);
await loadRetakeQuestions(skillArea);
// 并行 - 当操作独立时正确
const [user, settings, history] = await Promise.all([
fetchUser(id),
fetchSettings(id),
fetchHistory(id),
]);
// 错误 - 当顺序重要时并行
await Promise.all([
enableSkillAreaRetake(skillArea), // 这些有依赖关系!
clearSkillAreaAnswers(skillArea),
]);
决策框架:
| 操作共享状态? | 必须按顺序运行? | 模式 |
|---|---|---|
| 否 | 否 | Promise.all() |
| 是 | 是 | 顺序 await |
| 是 | 否 | 通常顺序以安全为上 |
模式:useEffect 中的异步
问题: useEffect 回调不能直接是异步的。常见的清理和竞态条件错误。
// 错误 - useEffect 不能是异步的
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
// 正确 - 内部异步函数
useEffect(() => {
async function load() {
const data = await fetchData();
setData(data);
}
load();
}, []);
// 更好 - 用于竞态条件的清理
useEffect(() => {
let cancelled = false;
async function load() {
const data = await fetchData();
if (!cancelled) {
setData(data);
}
}
load();
return () => {
cancelled = true;
};
}, [dependency]);
ESLint 配置
应用 configs/eslint-async.json 中的配置,在 lint 时间捕获这些问题:
{
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/no-misused-promises": "error"
}
}
所需: @typescript-eslint/eslint-plugin 和适当的 TypeScript 配置。
代码审查清单
审查异步代码时,请检查:
- [ ] 每个异步函数调用都被
await、return或明确注释为火并忘记 - [ ] 相互依赖的操作使用
await顺序 - [ ] 在关键异步操作后验证后置条件
- [ ] 带有异步的
useEffect使用内部函数模式 - [ ] 在组件可能在异步期间卸载时考虑竞态条件
- [ ] 异步失败时存在错误处理
快速调试
当出现异步定时问题时:
// 添加时间戳以追踪执行顺序
console.log(`[${Date.now()}] Starting enableRetake`);
await enableSkillAreaRetake(skillArea);
console.log(`[${Date.now()}] Finished enableRetake`);
console.log(`[${Date.now()}] Starting clearAnswers`);
await clearSkillAreaAnswers(skillArea);
console.log(`[${Date.now()}] Finished clearAnswers`);
查找:
- 操作完成顺序不符合预期
- 操作在前一个操作完成前开始
- 异常快速的“完成”(可能没有等待)