5月27日 23:31

Promise 的常见陷阱和最佳实践有哪些?

常见陷阱

忘记返回 Promise

这是 Promise 链中最容易犯的错误。then 回调中的返回值会作为下一个 then 的输入,忘记 return 会导致链断裂:

javascript
// 错误:then 中忘记 return function fetchUser() { getUser().then(user => { return getPosts(user.id); // 没有 return,外层拿不到结果 }); } fetchUser().then(posts => console.log(posts)); // undefined // 正确:return 让 Promise 链延续 function fetchUser() { return getUser().then(user => { return getPosts(user.id); }); } fetchUser().then(posts => console.log(posts)); // posts 数据

在 then 中嵌套 Promise

嵌套写法失去了 Promise 链的核心优势——扁平化异步流程:

javascript
// 错误:回调地狱的 Promise 版 getUser().then(user => { getPosts(user.id).then(posts => { // 嵌套了 getComments(posts[0].id).then(comments => { // 越嵌越深 }); }); }); // 正确:链式扁平调用 getUser() .then(user => getPosts(user.id)) .then(posts => getComments(posts[0].id)) .then(comments => console.log(comments));

忘记 catch

未捕获的 Promise rejection 在 Node.js 中会导致进程退出(unhandledRejection),在浏览器中则静默失败,排查困难:

javascript
// 错误:请求失败无任何提示 fetch('/api/data').then(r => r.json()).then(data => render(data)); // 正确:至少捕获错误 fetch('/api/data') .then(r => r.json()) .then(data => render(data)) .catch(err => console.error('请求失败:', err));

追问: .then(func).catch(handler).then(func, handler) 有什么区别?

catch 能捕获 func 内部的异常,而 .then(null, handler) 的第二个参数只处理上一个 Promise 的 rejection,捕获不到 func 抛出的错误。

循环中顺序 await

当多个异步操作互不依赖时,逐个 await 会白白浪费时间:

javascript
// 错误:串行等待,3 个请求耗时 = 3 × 单次耗时 async function loadDashboard() { const user = await fetchUser(); // 等 1s const posts = await fetchPosts(); // 再等 1s const stats = await fetchStats(); // 再等 1s return { user, posts, stats }; } // 正确:并行发起,3 个请求耗时 ≈ 单次耗时 async function loadDashboard() { const [user, posts, stats] = await Promise.all([ fetchUser(), fetchPosts(), fetchStats() ]); return { user, posts, stats }; }

混用 async/await 和 .then()

两种风格混用会让代码风格不一致,增加阅读负担:

javascript
// 错误:同一个函数里混用两种写法 async function getData() { const res = await fetch('/api'); return res.json().then(data => { // 突然切到 .then return transform(data); }); } // 正确:统一用 async/await async function getData() { const res = await fetch('/api'); const data = await res.json(); return transform(data); }

不必要的 Promise 包装

已经返回 Promise 的函数不需要再用 new Promise 包一层:

javascript
// 错误:反模式 —— Promise 构造函数包装 function getData() { return new Promise((resolve, reject) => { fetch('/api') // fetch 本身就返回 Promise .then(r => r.json()) .then(resolve) .catch(reject); }); } // 正确:直接返回 function getData() { return fetch('/api').then(r => r.json()); }

这种写法被称为 deferred anti-pattern,不仅多余,还会吞掉 resolve/reject 回调中的异常。

构造函数中执行异步操作

构造函数必须同步返回实例,无法 await,导致实例属性可能处于未就绪状态:

javascript
// 错误:data 可能为 null class UserService { constructor(id) { this.data = null; fetch(`/api/users/${id}`) .then(r => r.json()) .then(data => { this.data = data; }); } } const svc = new UserService(1); console.log(svc.data); // null —— 请求还没完成 // 正确:静态工厂方法 class UserService { constructor(data) { this.data = data; } static async create(id) { const data = await fetch(`/api/users/${id}`).then(r => r.json()); return new UserService(data); } } const svc = await UserService.create(1); console.log(svc.data); // 有数据

误用 Promise.all 替代条件请求

Promise.all 会等所有请求完成,如果部分请求的结果并不需要,就是浪费:

javascript
// 错误:无条件并发所有请求 async function getPage(cond) { const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]); return cond ? a : b; // c 永远用不到,但请求已经发了 } // 正确:按需请求 async function getPage(cond) { if (cond) return { a: await fetchA() }; return { b: await fetchB() }; }

最佳实践

用 Promise.allSettled 处理部分失败

Promise.all 只要有一个失败就整体失败,而 allSettled 会等全部完成,适合"能拿多少拿多少"的场景:

javascript
async function fetchAll(urls) { const results = await Promise.allSettled(urls.map(u => fetch(u).then(r => r.json()))); const ok = results.filter(r => r.status === 'fulfilled').map(r => r.value); const failed = results.filter(r => r.status === 'rejected').map(r => r.reason); return { ok, failed }; }

用 Promise.race 实现超时控制

javascript
function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`超时 ${ms}ms`)), ms) ); return Promise.race([promise, timeout]); } // 用法 const data = await withTimeout(fetch('/api'), 5000);

用 Promise.any 获取最快成功结果

多个数据源竞争时,Promise.any 返回第一个 fulfilled 的结果,只有全部失败才抛 AggregateError

javascript
async function getFastest(urls) { try { const res = await Promise.any(urls.map(u => fetch(u))); return await res.json(); } catch (e) { // e 是 AggregateError,包含所有失败原因 throw new Error('所有数据源均不可用'); } }

用 AbortController 取消请求

javascript
const controller = new AbortController(); async function search(query) { const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal }); return res.json(); } // 用户输入新关键词时取消上一次请求 input.addEventListener('input', () => { controller.abort(); search(input.value); });

实现带退避的重试机制

javascript
async function retry(fn, max = 3) { for (let i = 0; i < max; i++) { try { return await fn(); } catch (err) { if (i === max - 1) throw err; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); // 指数退避 } } }

用 finally 做资源清理

finally 无论成功失败都会执行,适合关闭连接、隐藏 loading 等场景:

javascript
async function query() { let conn; try { conn = await pool.getConnection(); return await conn.query('SELECT * FROM users'); } finally { conn?.release(); // 无论是否抛异常都会释放连接 } }

请求去重

同一时刻对同一资源发起多次请求是浪费,可以用 Map 缓存正在进行的 Promise:

javascript
const pending = new Map(); function fetchOnce(url) { if (pending.has(url)) return pending.get(url); const p = fetch(url) .then(r => r.json()) .finally(() => pending.delete(url)); pending.set(url, p); return p; }

并发控制

Promise.all 一次全部发出,当数量大时可能打爆服务端。用一个简单的并发池控制:

javascript
async function concurrent(tasks, limit) { const results = []; const executing = new Set(); for (const task of tasks) { const p = task().then(r => { executing.delete(p); return r; }); executing.add(p); results.push(p); if (executing.size >= limit) { await Promise.race(executing); } } return Promise.all(results); } // 最多同时 3 个请求 await concurrent(urls.map(u => () => fetch(u).then(r => r.json())), 3);

易错辨析

Promise.then() 返回的是同一个 Promise 吗?

不是。每次 .then() 都会返回一个新的 Promise,这也是链式调用的基础:

javascript
const p1 = fetch('/'); const p2 = p1.then(r => r.json()); const p3 = p2.then(data => data.id); console.log(p1 === p2); // false console.log(p2 === p3); // false

await 一个非 Promise 值会怎样?

会立即 resolve。await 42 等价于 await Promise.resolve(42),不会阻塞后续微任务。但在 for...of 中加 await 会拖慢循环,即使值不是 Promise。

为什么 catch 之后还能继续 then?

catch 返回的也是新 Promise,且状态为 fulfilled(除非 catch 回调内又抛异常),所以后面可以继续 .then()

javascript
Promise.reject('err') .catch(e => 'recovered') // 返回 fulfilled('recovered') .then(val => console.log(val)); // 'recovered'
标签:Promise