Promise 是 JavaScript 中解决异步编程问题的一种重要工具 …… 哪怕你不知道 Promise 里面发生了什么,照抄 LLM 生成的代码也没错,但我想最好还是稍微有点探索精神,了解一下 Promise 的概念、用法及常见陷阱。
基础下文使用 TypeScript 语法。
Promise 是一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。
在 TS 中你可以明确声明 Promise 的类型:Promise<T>,其中 T 是 Promise 成功时返回的值的类型(失败时返回的值的类型是 any)。
也可以自动推导类型。 Promise<void> 表示没有返回值的 Promise。1 2 3 4 5 const myPromise : Promise <string > = new Promise ((resolve, reject ) => { setTimeout (() => { resolve ("Success" ); }, 1000 ); });
它有三种状态:
pending:初始状态。fulfilled:操作成功。rejected:操作失败。只允许两种状态转换:
pending → fulfilledpending → rejected且一旦状态改变,就不能再改变了。
这个状态有什么意义呢?fulfilled 和 rejected 状态下,Promise 对象视为已经完成,并立即调用相应的回调函数。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const trialPromise : Promise <number > = new Promise ((resolve, reject ) => { if (Math .random () > 0.5 ) { resolve (42 ); } else { reject (new Error ("Rejected" )); } }).then ( (result ) => { console .log ("Success" , result); }, (error ) => { console .error ("Failed" , error); } );
其中,then(onFulfilled, onRejected) 方法接受两个参数,分别相当于 resolve 和 reject 的回调函数。
不过一般不会见到 then 的第二个参数,因为 Promise 失败时会抛出异常,通常会使用 catch 方法来处理错误。
1 2 3 4 5 6 7 8 const trialPromise : Promise <number > = new Promise ((resolve, reject ) => { if (Math .random () > 0.5 ) { resolve (42 ); } else { reject (new Error ("Rejected" )); } }).then ((result ) => console .log ("Success" , result)) .catch ((error ) => console .error ("Failed" , error));
注意在 Promise 完成后,这个 Promise 变量可以作为一个值使用。假设实验成功,trialPromise 变量的值是 42;否则,trialPromise 变量的值是 Error 对象。
链式调用由于 Promise 的 then 方法返回一个新的 Promise 对象,因此可以进行链式调用。
这有什么意义呢?
假设我们使用传统异步,将 PDF 文件转化为 base64 字符串然后进行 OCR 识别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const pdfToImages = async (pdfUrl : string , maxPages : number = 1 ) => { const pdfjs = require ('pdfjs-dist/webpack' ); const pdfDoc = await pdfjs.getDocument (pdfUrl).promise ; const images = []; const canvas = document .createElement ('canvas' ); const numPages = Math .min (maxPages, pdfDoc.numPages ); if (!numPages) { throw new Error ('Invalid PDF file' ); } for (let i = 1 ; i <= numPages; i++) { const page = await pdfDoc.getPage (i); const viewport = page.getViewport ({ scale : 3 }); const context = canvas.getContext ('2d' ); canvas.height = viewport.height ; canvas.width = viewport.width ; await page.render ({ canvasContext : context, viewport }).promise ; images.push (canvas.toDataURL ('image/png' )); } canvas.remove (); return images; }; const OCR = async (image : string ) => { const result = await Tesseract .recognize (image, 'eng' ); return result.data .text ; }; try { const images = await pdfToImages ('example.pdf' ); const ocrResults = []; for (const image of images) { try { const result = await OCR (image); ocrResults.push (result); } catch (error) { console .error ('OCR Error:' , error); } } } catch (error) { console .error ('Error:' , error); }
这样就有一个问题是:要处理的错误有很多种,可能是 PDF 文件本身的问题,也可能是 OCR 识别的问题。我们需要在每个 try...catch 块中处理错误。
如果我们使用 Promise,就可以将错误处理放在链的末尾:
1 2 3 4 5 const ocrResults : Promise <string []> = pdfToImages ('example.pdf' ) .then ((images ) => { return Promise .all (images.map ((image ) => OCR (image))); }) .catch ((error ) => console .error ('Error:' , error));
这里有两级 Promise:
pdfToImages 返回一个 Promise,表示 PDF 转换为图片的结果。Promise.all 返回一个 Promise,收集所有图片的 OCR 结果。但只需要一个 catch 块来处理所有错误。
可能有人疑惑开头没有用到 new Promise((resolve, reject) => {}),因为异步函数本身就返回一个 Promise 对象。前面的例子只是没有明确写出来而已,实际上都是和 pdfToImages 相似的异步函数。
四种 Promise 方法 Promise.all上一节最后一个例子中使用了 Promise.all,它接受一个 Promise 数组(每一页都是一个 Promise),组合成一个新的 Promise 对象(数组)。
具体来说,在所有 Promise 都成功时,返回一个新的 Promise 对象,值是一个数组,包含所有 Promise 的结果;如果有一个 Promise 失败,则返回一个失败的 Promise 对象,值是第一个失败的 Promise 的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const timeoutPromise = (ms : number ) => { if (ms < 0 ) { return Promise .reject (new Error (`Timeout cannot be negative: ${ms} ` )); } return new Promise ((resolve ) => { setTimeout (() => { console .log (`Resolved after ${ms} ms` ); resolve (ms); }, ms); }); }; const promises = [ timeoutPromise (100 ), timeoutPromise (10 ), timeoutPromise (1 ), ]; const start = Date .now ();Promise .all (promises) .then ((values ) => { console .log (`All promises resolved in ${Date .now() - start} ms` ); console .log ("Values:" , values); }) .catch ((error ) => { console .error ("Error:" , error); });
输出结果:
1 2 3 4 5 Resolved after 1 ms Resolved after 10 ms Resolved after 100 ms All promises resolved in 103 ms Values: [ 1, 10, 100 ]
所以 Promise.all 干了两件事:
(理论上)并行执行所有 Promise。 等待所有 Promise 完成,收集其返回值,将这个数组作为一个 Promise 返回。(数组的顺序和传入的 Promise 顺序一致) 再试一下:
1 2 3 4 5 6 const promises = [ timeoutPromise (10 ), timeoutPromise (-1000 ), timeoutPromise (-100 ), timeoutPromise (1 ) ];
输出结果:
1 2 3 Error: Timeout cannot be negative: -1000 Resolved after 1 ms Resolved after 10 ms
所以要注意:
如果任何有一个 Promise 失败,Promise.all 会立即返回一个失败的 Promise。此时,其他 Promise 仍然会继续执行,但由于 Promise.all 的状态已经转为 rejected:
成功的 Promise 的结果不会被收集,then 方法不会被调用。 仅第一个失败的 Promise 的结果会被传递给 catch 方法。 单个 Promise 的失败仍然不会影响其他 Promise 的执行。 Promise.allSettled如果你希望所有 Promise 都执行完毕后再处理结果,可以使用 Promise.allSettled 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const promises = [ timeoutPromise (10 ), timeoutPromise (-1000 ), timeoutPromise (-100 ), timeoutPromise (1 ) ]; const start = Date .now ();Promise .allSettled (promises) .then ((results ) => { console .log (`All promises settled in ${Date .now() - start} ms` ); results.forEach ((result, index ) => { if (result.status === 'fulfilled' ) { console .log (`Promise ${index} resolved with value: ${result.value} ` ); } else { console .error (`Promise ${index} rejected with reason: ${result.reason} ` ); } }); });
输出结果:
1 2 3 4 5 6 7 Resolved after 1 ms Resolved after 10 ms All promises settled in 13 ms Promise 0 resolved with value: 10 Promise 1 rejected with reason: Error: Timeout cannot be negative: -1000 Promise 2 rejected with reason: Error: Timeout cannot be negative: -100 Promise 3 resolved with value: 1
要注意,allSettled 方法返回的 Promise 对象的值是一个数组,包含每个 Promise 的结果对象。
每个结果对象都有两个属性:
status:表示 Promise 的状态,可以是 fulfilled 或 rejected。value:如果 Promise 成功,包含成功的值;否则为 undefined。reason:如果 Promise 失败,包含失败的原因;否则为 undefined。allSettled 本身只有一种结果:fulfilled,即所有 Promise 都执行完毕,所以只需要一个 then 方法来处理结果。
Promise.racePromise.race 和 Promise.all 类似,但它只返回第一个完成的 Promise 的结果,无论是成功还是失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const promises = [ timeoutPromise (100 ), timeoutPromise (10 ), timeoutPromise (1 ), ]; const start = Date .now ();Promise .race (promises) .then ((value ) => { console .log (`First promise resolved in ${Date .now() - start} ms` ); console .log ("Value:" , value); }) .catch ((error ) => { console .error ("Error:" , error); });
输出结果:
1 2 3 4 5 Resolved after 1 ms First promise resolved in 4 ms Value: 1 Resolved after 10 ms Resolved after 100 ms
又或者
1 2 3 4 5 const promises = [ timeoutPromise (100 ), timeoutPromise (-10 ), timeoutPromise (-1 ), ];
输出结果:
1 2 Error: Timeout cannot be negative: -10 Resolved after 100 ms
因为 resolve 有 100ms 延迟,reject 立即返回,这里肯定是 timeoutPromise(-10) 先完成,所以 Promise.race 返回的 Promise 是失败的。
Promise.race 被 rejected 后的行为与 Promise.all 一样,其他 Promise 仍然会继续执行,但 then 方法不会被调用,只有第一个失败的 Promise 的结果会被传递给 catch 方法。
可以再给失败加个延时:
1 2 3 4 5 6 const delay = (ms : number ) => new Promise ((resolve ) => setTimeout (resolve, ms));const promises = [ timeoutPromise (100 ), delay (10 ).then (() => timeoutPromise (-10 )), timeoutPromise (1 ), ];
输出结果:
1 2 3 4 Resolved after 1 ms First promise resolved in 4 ms Value: 1 Resolved after 100 ms
我们可以用同样的道理分析:在 timeoutPromise(1) resolved 之后,整个 Promise 的状态就变成了 fulfilled,所以:
成功的 Promise 的结果不会被收集,只有第一个成功的 Promise 的结果会被传递给 then 方法。 失败的 Promise 的结果被完全忽略。 Promise.race 有一个典型的应用场景,就是实现超时控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const timeout = (ms : number ) => new Promise ((_, reject ) => { setTimeout (() => { reject (new Error (`Timeout after ${ms} ms` )); }, ms); }); const OCRWithTimeout = (image : string , timeoutMs : number ) => { return Promise .race ([ OCR (image), timeout (timeoutMs) ]).then ((result ) => result.data .text ) .catch ((error ) => { console .error ('OCR Error:' , error); throw error; }); }; const ocrResults : Promise <string []> = pdfToImages ('example.pdf' ) .then ((images ) => { return Promise .all (images.map ((image ) => OCRWithTimeout (image, 5000 ))); }) .catch ((error ) => console .error ('Error:' , error));
这样我们用一个 catch 同时处理了 OCR 失败和超时的错误。
Promise.any在往下看之前,可以先根据 any 的意思,推测一下它的行为。
可以思考 any 在其他语言中是如何作为关键字使用的:
1 2 3 4 5 6 if all ([x > 0 for x in arr]): return max (arr) elif any ([x < 0 for x in arr]): return min (arr) else : return 0
由此可见,Promise.any 和 Promise.all 的行为正好相反。all 需要全部成功才返回成功,any 则是全部失败才返回失败。
具体来说:
只要有一个成功的 Promise,立即 resolve,返回这个 Promise 的结果。 如果所有 Promise 都失败,返回一个失败的 Promise,值是一个 AggregateError 对象,包含所有失败的 Promise 的错误信息。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const promises = [ timeoutPromise (100 ), timeoutPromise (-10 ), timeoutPromise (-1 ), ]; const start = Date .now ();Promise .any (promises) .then ((value ) => { console .log (`First promise resolved in ${Date .now() - start} ms` ); console .log ("Value:" , value); }) .catch ((error ) => { console .error ("Error:" , error); for (const err of error.errors ) { console .error ("Error:" , err); } });
输出结果:
1 2 3 Resolved after 100 ms First promise resolved in 103 ms Value: 100
再试一下:
1 2 3 4 5 const promises = [ timeoutPromise (-100 ), timeoutPromise (-10 ), timeoutPromise (-1 ), ];
输出结果:
1 2 3 4 Error: All promises were rejected Error: Timeout cannot be negative: -100 Error: Timeout cannot be negative: -10 Error: Timeout cannot be negative: -1
总结四种 Promise 方法的区别整理如下:
方法 成功条件 成功返回值 失败条件 失败返回值 .all全部成功 [结果数组]任意失败 最先失败的值 .allSettled全部完成 [结果/错误数组]- - .race任意成功 最先成功的值 任意失败 最先失败的值 .any任意成功 最先成功的值 全部失败 AggregateError 对象
额外注意:
在 .all 和 .allSettled 中,结果数组是按照传入的 Promise 顺序排列的。 在 .all(提前失败)、.race(提前成功或失败)和 .any(提前成功)后,未完成的 Promise 仍然会继续执行,但不会影响结果。 Promise 数组中每个 Promise 都不会影响其他 Promise 的执行,状态独立。 React 中的 Promise 常见陷阱