首页 TypeScript TypeScript 中的 Promise 和 Async/Await

TypeScript 中的 Promise 和 Async/Await

0 1.9K

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 链

thencatch以及finally函数会隐式返回一个Promise对象。由这些回调函数返回的任何值都会被包装成一个Promise对象,然后返回,甚至包括undefined。这种隐式的 promise 默认为接受状态,除非你自己新建一个Promise对象然后将其设置为拒绝状态。

因此,你可以在thencatch或者finally函数后面追加另外的thencatchfinally函数。如果隐式的 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 对象p1p2这些全部进入接受状态之后,进入接受状态。这个 promise 会返回一个包含所有的 promise 对象p1p2这些的接受值的数组,元素顺序就是p1p2这些出现的顺序。

// 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具有快速拒绝的机制,这意味着,只要输入值中p1p2任一 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确定时,其返回值是PromiseFulfilledResultPromiseRejectedResult类型的数组。

PromiseFulfilledResultPromiseFulfilledResultPromiseRejectedResult类型的联合,每个元素里面都有一个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.allPromise.allSettled不同,Promise.race只返回包含第一个设置状态的 promise 的值。因此,上面例子中,返回的 promise 类型就是Promise<number>。由于第一个 promise 在所有输入的 promise 中第一个被设置状态,所以fastestPromisethen回调函数在 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常量的类型就是numberisEven函数返回一个接受值类型为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关键字手动抛出一个错误。

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2