Promise 是 JavaScript 语言引入的新特性之一。前面的一篇文章我们已经介绍过 JavaScript 中的 promise 和 async/await 的语法。这篇文章将重点聚焦于 TypeScript 语言中的 promise 和 async/await。
promise 是Promise
类的实例。创建一个 promise 对象,我们需要使用new Promise(executor)
语法,其中,将执行器函数作为构造函数的参数。这个执行器函数定义了 promise 接受或者拒绝时所应执行的行为。
在 TypeScript 中,我们可以给出 promise 接受时的返回值数据类型。由于 promise 拒绝时可以接受任意类型,所以,TypeScript 把 promise 拒绝时的返回值类型定义为any
。
为了定义 promise 的返回值类型,我们使用了泛型声明。一般而言,你可以使用new Promise<Type>()
形式的Promise
构造函数;这种形式的构造函数指定了 promise 接受状态的值的类型。同时,你也可以使用const p: Promise<Type> = new Promise()
这样的语法,以达到相同的效果。
"use strict"; // returns a random integer between 0 and 9 const getRandomInt = () => { return (Math.random() * 10).toFixed(0); }; // resolve with an `even` integer const findEven = new Promise((resolve, reject) => { setTimeout(function () { // convert `string` to `number` const value = parseInt(getRandomInt()); if (value % 2 === 0) { resolve(value); } else { reject('Odd number found!'); } }, 1000); }); // listen to promise resolution findEven.then((value) => { // (parameter) value: number console.log('Resolved:', value + 1); }).catch((error) => { // (parameter) error: any console.log('Rejected:', error); }).finally(() => { console.log('Completed!'); });
在上面的例子中,findEven
是一个使用Promise
构造函数创建的 promise 对象。在 1 秒钟之后,promise 进入接受状态,其数据类型是number
,因此,TypeScript 编译器不会允许你在使用resolve
函数时,传入非number
类型的参数。
promise 拒绝值的默认类型是any
,因此,用任意值作为参数调用reject
函数都是合法的。这是 TypeScript 的默认行为,你可以在这里进一步讨论。
由于我们指定number
类型作为 promise 接受状态的数据类型,TypeScript 编译器会直接将number
类型作为then
回调函数的value
参数类型。
then
函数的回调函数在 promise 接受时被执行,catch
函数的回调会在 promise 拒绝或者在执行过程中出现错误时执行,finally
函数的回调函数则在不论 promise 是接受或拒绝时都会执行。
如果 TypeScript 编译器对finally
函数报错,这意味着你的 TypeScript 编译器没有引入finally
函数的类型定义。这个函数在 ES2016 被引入,因此还算是比较新的特性。本文中 Promise API 的其它特性可能更新,因此你需要确保你的 tsconfig.json 文件能够使用这些新的特性。
在这个 tsconfig.json 中,我们加载了 ES2020 标准库。这意味着我们可以使用 ES2020 之前的所有 JavaScript 特性。
promise 链
then
、catch
以及finally
函数会隐式返回一个Promise
对象。由这些回调函数返回的任何值都会被包装成一个Promise
对象,然后返回,甚至包括undefined
。这种隐式的 promise 默认为接受状态,除非你自己新建一个Promise
对象然后将其设置为拒绝状态。
因此,你可以在then
、catch
或者finally
函数后面追加另外的then
、catch
或finally
函数。如果隐式的 promise 由这些函数之一返回,那么,这个隐式 promise 的接受值类型应该是返回值的类型。下面看一个简单的例子。
// returns a random integer between 0 and 9 const getRandomInt = (): string => { return ( Math.random() * 10 ).toFixed( 0 ); }; // resolve with an `even` integer const findEven = new Promise<number>( ( resolve, reject ) => { setTimeout( function(): void { // convert `string` to `number` const value = parseInt( getRandomInt() ); if( value % 2 === 0 ) { resolve( value ); } else { reject( 'Odd number found!' ); } }, 1000 ); } ); // listen to promise resolution findEven.then( ( value ) => { // (parameter) value: number console.log( 'Resolved-1:', value + 1 ); return `${ value + 1 }`; } ).then( ( value ) => { // (parameter) value: string console.log( 'Resolved-2:', value + 1 ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.log( 'Completed!' ); } );
我们修改了前面的示例,在第一个then
函数之后增加了另外的then
函数。由于第一个then
函数返回string
类型的值,隐式的 promise 则是string
类型的接受值类型。因此,第二个then
函数的接收到的参数类型是string
。
Promise.resolve
Promise
类的static
函数resolve
返回一个接受状态的 promise,其接受值就是Promise.resolve(value)
传入的值。这比创建新的Promise
对象,然后直接接受这样的逻辑要简单一些。
// returns a random integer between 0 and 9 const getRandomInt = (): string => { return ( Math.random() * 10 ).toFixed( 0 ); }; // get random integer of type `number` const value: number = parseInt( getRandomInt() ); // const numPromise: Promise<number> const numPromise = Promise.resolve( value ); // listen to promise resolution numPromise.then( ( value ) => { // (parameter) value: number console.log( 'Resolved:', value + 1 ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } );
上面的例子,Promise.resolve(value)
语句返回的 promise 始终是接受状态,其值的类型就是value
的实际类型number
。
Promise.reject
与Promise.resolve
类似,Promise.reject(error)
返回一个拒绝状态的 promise,拒绝值来自error
,类型是any
。
// create a sample error message const message = 'Oops! Something went wrong.'; // const oopsPromise: Promise<never> const oopsPromise = Promise.reject( message ); // listen to promise resolution oopsPromise.then( ( value ) => { // (parameter) value: never console.log( 'Resolved:', value ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } );
由Promise.reject
返回的 promise 类型是Promise<never>
,因为这个 promise 不会进入接受状态,所以不会有任何接受状态的值。因此,这个 promise 接受值的类型是never
,而never
类型意味着这个值永远不会出现。
Promise.all
在某些场景中,可能需要处理多个 promise。如果你希望在所有 promise 都进入接受状态之后执行一个回调函数,可以使用Promise.all
函数。
var pAll = Promise.all([ p1, p2, ... ]) pAll.then( ( [ r1, r2, ... ] ) => {___});
Promise.all
函数参数是 promise 数组(确切的说是可遍历的),返回一个新的 promise 对象。返回的 promise pAll
会在所有 promise 对象p1
、p2
这些全部进入接受状态之后,进入接受状态。这个 promise 会返回一个包含所有的 promise 对象p1
、p2
这些的接受值的数组,元素顺序就是p1
、p2
这些出现的顺序。
// a function that return a delayed promise const getPromise = ( value: number, delay: number, fail: boolean ): Promise<number> => { return new Promise<number>( ( resolve, reject ) => { setTimeout( () => fail ? reject( value ) : resolve( value ), delay ); } ); }; // const allPromise: Promise<number[]> const allPromise = Promise.all<number>( [ getPromise( 0, 0, false ), // 0s getPromise( 1, 2000, false ), // 2s getPromise( 2, 1000, false ), // 1s ] ); // listen to `allPromise` resolution console.time( 'settled-in' ); allPromise.then( ( value ) => { // (parameter) value: number[] console.log( 'Resolved:', value ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.timeEnd( 'settled-in' ); } );
上面的例子中,Promise.all
是泛型的,其类型是每一个 promise 的值类型。泛型类型在使用接受值时非常有用,正如上面的例子显示的那样。
需要注意的是,Promise.all
具有快速拒绝的机制,这意味着,只要输入值中p1
、p2
任一 promise 拒绝,pAll
就会拒绝。它不会等待其它的 promise 结束。
// a function that return a delayed promise const getPromise = ( value: number, delay: number, fail: boolean ): Promise<number> => { return new Promise<number>( ( resolve, reject ) => { setTimeout( () => fail ? reject( value ) : resolve( value ), delay ); } ); }; // const allPromise: Promise<number[]> const allPromise = Promise.all<number>( [ getPromise( 0, 0, false ), // 0s getPromise( 1, 2000, false ), // 2s getPromise( 2, 1000, true ), // 1s (rejects) ] ); // listen to `allPromise` resolution console.time( 'settled-in' ); allPromise.then( ( value ) => { // (parameter) value: number[] console.log( 'Resolved:', value ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.timeEnd( 'settled-in' ); } );
在上面的例子中,第三个 promise 在 1 秒后拒绝,整个allPromise
立即拒绝了。
Promise.allSettled
Promise.allSettled
函数类似于Promise.all
,与后者不同的是,Promise.allSettled
会等待所有 promise 对象设置状态,即要么进入接受状态,要么进入拒绝状态。因此,Promise.allSettled
返回的 promise 永远不会拒绝(但在下面的代码中,我们还是加入了catch
回调函数)。
// a function that return a delayed promise const getPromise = ( value: number, delay: number, fail: boolean ): Promise<number> => { return new Promise<number>( ( resolve, reject ) => { setTimeout( () => fail ? reject( value ) : resolve( value ), delay ); } ); }; /* const allPromise: Promise[ PromiseSettledResult<number>, PromiseSettledResult<number>, PromiseSettledResult<number> ]> */ const allPromise = Promise.allSettled( [ getPromise( 0, 0, false ), // 0s getPromise( 1, 2000, false ), // 2s getPromise( 2, 1000, true ), // 1s (rejects) ] ); // listen to `allPromise` resolution console.time( 'settled-in' ); allPromise.then( ( value ) => { console.log( 'Resolved:', value ); value.forEach( result => { switch( result.status ) { case 'fulfilled': { // result: PromiseFulfilledResult<number> console.log( 'success =>', result.value ); break; } case 'rejected': { // result: PromiseRejectedResult console.log( 'error =>', result.reason ); break; } } } ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.timeEnd( 'settled-in' ); } );
Promise.allSettled
在使用起来有一些困难之处,正如上面的示例代码显示的那样。注意,Promise.allSettled
函数返回的 promise 类型是PromiseSettledResult<type>
类型的数组,其中的type
由输入的 promise 类型推断而来。PromiseSettledResult
定义如下:
interface PromiseFulfilledResult<T> { status: "fulfilled"; value: T; // promise resolved value } interface PromiseRejectedResult { status: "rejected"; reason: any; // promise rejected value } type PromiseSettledResult<T> = PromiseFulfilledResult<T> | PromiseRejectedResult;
这些类型是 TypeScript 的标准库。因此,当 promise 进入接受状态时,allSettled
函数会将其值转换为PromiseFulfilledResult
类型;拒绝时则转换为PromiseRejectedResult
类型。这就是为什么当allSettled
确定时,其返回值是PromiseFulfilledResult
或PromiseRejectedResult
类型的数组。
PromiseFulfilledResult
是PromiseFulfilledResult
和PromiseRejectedResult
类型的联合,每个元素里面都有一个status
字面量常量,因此,我们可以根据这个字面量作为类型判别式。
注意上面的代码示例。在allPromise.then
回调中的switch
语句,利用status
判断是哪种类型。
Promise.race
Promise.race
接受一个 promise 数组(确切的说是可遍历的),当其中一个输入的 promise 接受或者拒绝,就会返回一个新的与输入的那个 promise 状态一致的 promise 对象。换句话说,当输入的 promise 对象中的任何一个被设置状态的时候,Promise.race
返回的那个 promise 就会被立即设置。
// a function that return a delayed promise const getPromise = ( value: number, delay: number, fail: boolean ): Promise<number> => { return new Promise<number>( ( resolve, reject ) => { setTimeout( () => fail ? reject( value ) : resolve( value ), delay ); } ); }; // const fastestPromise: Promise<number> const fastestPromise = Promise.race<number>( [ getPromise( 0, 500, false ), // 0.5s getPromise( 1, 2000, false ), // 2s getPromise( 2, 1000, true ), // 1s (rejects) ] ); // listen to `allPromise` resolution console.time( 'settled-in' ); fastestPromise.then( ( value ) => { // (parameter) value: number console.log( 'Resolved:', value ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.timeEnd( 'settled-in' ); } );
与Promise.all
或Promise.allSettled
不同,Promise.race
只返回包含第一个设置状态的 promise 的值。因此,上面例子中,返回的 promise 类型就是Promise<number>
。由于第一个 promise 在所有输入的 promise 中第一个被设置状态,所以fastestPromise
的then
回调函数在 500ms 之后被调用,其参数就是被设置的值。
ES2021 新引入了Promise.any
,非常类似于Promise.race
,区别在于,Promise.any
会在任意一个 promise 达到成功状态时才会返回。如果所有的 promise 都失败,则会抛出AggregateError
错误。
Async/Await
用正常的方式编写 promise 代码,有时候会难以管理,经常会出现一些不必要的冗余代码。通常,你所需要的就是一个函数以及一些参数,用于处理 promise 接受或者拒绝的数据。有时候,使用Promise
构造函数创建一个 promise 也会让人感到厌烦。
JavaScript 提供了async
关键字,可以将一个普通的函数隐式转换为返回 promise 的函数。由这个async
函数返回的任意值都会被转换为一个隐式的 promise,返回值作为该 promise 的接受值。如果这个函数抛出异常,则返回的 promise 将其作为错误信息并拒绝。
await
关键字只能在async
函数中使用,用于等待 promise 返回。如果 promise 前面有await
关键字,那么,函数后面的代码不会被执行,直到这个 promise 返回。
// resolve with a random integer between 0 and 9 const getRandomInt: () => Promise<number> = async () => { return parseInt( (Math.random() * 10).toFixed( 0 ) ); }; // returns a promise of type `Promise<boolean>` // const isEven: (answer: boolean) => Promise<boolean> const isEven = async ( answer: boolean ) => { // const value: number const value = await getRandomInt(); // const isEven: boolean const isEven = value % 2 === 0; // return value is `boolean` return isEven === answer; }; // listen to promise resolution isEven( true ).then( ( value ) => { // (parameter) value: boolean console.log( value === true ? 'lucky :)' : 'unlucky :(' ); } ).catch( ( error ) => { // (parameter) error: any console.log( 'Rejected:', error ); } ).finally( () => { console.log( 'Completed!' ); } );
在上面的例子中,getRandomInt
是一个async
函数,因为其声明有async
关键字。由于这个函数返回number
类型的值,因此我们可以给getRandomInt
添加正确的类型信息:
const getRandomInt: () => Promise<number>
但是,这并不是必须的。TypeScript 理解async
关键字,它会根据函数的返回值,提供这个函数的隐式返回类型。因此,isEven
常量具有如下类型:
const isEven: (answer: boolean) => Promise<boolean>
我们在isEven
函数中使用await
关键字,用于等待getRandomInt
函数的返回值。这意味着,isEven
函数执行会被挂起,直到 promise 返回,同时会将value
赋值。
既然 TypeScript 知道getRandomInt
函数的返回值类型,value
常量的类型就是number
。isEven
函数返回一个接受值类型为boolean
的 promise。
你可能想知道,由async
函数返回的 promise 如何进入拒绝状态?在另外一篇文章中,我们详细介绍了相关内容。简单来说,如果async
函数中抛出异常,则async
函数返回的 promise 自动进入拒绝状态。
// resolve with a random integer between 0 and 9 const getRandomInt: () => Promise<number> = async () => { return parseInt( (Math.random() * 10).toFixed( 0 ) ); }; // resolve with a boolean value const isEven = async ( answer: boolean ) => { // const value: number const value = await getRandomInt(); // reject if the `value` is `0` if( value === 0 ) { throw new Error( 'Can\'t work with 0 :/' ); } // returned value is `boolean` const isEven = value % 2 === 0; return isEven === answer; }; // listen to promise resolution isEven( true ).then( ( value ) => { // (parameter) value: boolean console.log( value === true ? 'lucky :)' : 'unlucky :(' ); } ).catch( ( error: Error ) => { console.log( 'Rejected:', error.message ); } ).finally( () => { console.log( 'Completed!' ); } );
在这个例子中,我们对前面的例子做了些许修改。我们在isEven
函数中增加了一个判断,如果value
值为 0,则返回一个 JavaScript 错误。
现在,由isEven
函数返回的 promise 有可能被拒绝。这个可以被catch
函数捕获,拒绝值会包含async
函数抛出的错误信息。
我们把catch
回调函数的参数error
的类型设置为Error
,而不是默认的any
类型,因为我们知道拒绝值的实际类型。
由于async
函数使用throw
关键字抛出错误,从而拒绝了这个 promise,这会导致另外一个问题。如果async
函数在等待这个 promise,而这个 promise 却被拒绝了呢?
简单的答案是,错误会在async
函数中冒泡。这意味着,如果我们使用await
关键字正在等待的那个 promise 被拒绝了,JavaScript 会在当前async
函数抛出UC哦呜,await
关键字后面的任何代码都不会被执行。如果当前的async
函数还在另外的async
函数中,这种机制会一直进行下去。
使用try
/catch
可以检测等待的 promise 是否被拒绝。如果我们在try
块中等待 promise,那么,我们就可以在catch
块中知道这个 promise 是不是被拒绝了。catch
的参数就是 promise 的拒绝值。
值得注意的是,我们可以直接等待由Promise
构造函数创建的 promise 对象。这并不会有任何区别。如果这个 promise 被拒绝,同样会抛出错误。
// resolve with a random integer between 0 and 9 // const getRandomInt: () => Promise<number> const getRandomInt = async () => { const value = parseInt((Math.random() * 10).toFixed(0)); // reject if the `value` is `0` if( value === 0 ) { throw new Error( 'Can\'t work with 0 :/' ); } return value; }; // resolve with a boolean value // const isEven: (answer: boolean) => Promise<boolean> const isEven = async ( answer: boolean ) => { try { const value = await getRandomInt(); const isEven = value % 2 === 0; return isEven === answer; } catch( e ) { console.log( 'getRandomInt rejection:', (e as any).message ); return false; // return `false` deliberately } }; // listen to promise resolution isEven( true ).then( ( value ) => { // (parameter) value: boolean console.log( value === true ? 'lucky :)' : 'unlucky :(' ); } ).catch( ( error: Error ) => { console.log( 'Rejected:', error.message ); } ).finally( () => { console.log( 'Completed!' ); } );
上面的例子中,由getRandomInt
返回的 promise 可能被拒绝。因此,在isEven
函数中,我们将等待 promise 的代码放在try
块中。如果try
块中的代码抛出错误,catch
块就会以该错误为参数被执行。
在这个例子中,isEven
函数会返回一个永远不会被拒绝的 promise。如果你想拒绝这个 promise,你需要在catch
块中使用throw
关键字手动抛出一个错误。