Promise对象

ECMAScript 6 中增加了 Promise 对象,它一经推出就大受欢迎,逐渐成为当前主流的异步编程模式。Promise 对象就如它的中文含义 “期约” 一样,用来保证在未来返回某种结果。

声明并使用Promise对象

Promise 对象有以下 3 种状态。

  • pending:初始状态,异步任务执行中。

  • resolved:结果一,异步任务执行成功。

  • rejected:结果二,异步任务执行失败。

Promise 对象可用于封装异步操作,其实例化语法如下。

let 对象名称 = new Promise(封装异步操作的自定义函数);

封装异步操作的自定义函数必须为以下类型。

(
    resolve: (value: unknown) => void,
    reject: (reason?: any) => void
) => void;

注意,resolve() 函数和 reject() 函数并不是自定义函数,不需要单独定义,它们是在 Promise 对象执行期间调用封装异步操作的自定义函数时传入的内置函数。resolve() 函数与 reject() 函数分别为自定义函数内的异步任务执行成功和失败时需要调用的函数。

封装异步操作的自定义函数的语法通常如以下代码所示。根据异步处理的结果,调用 resolve() 函数或 reject() 函数。

function(resolve, reject) {
    // 异步处理
    // 处理成功后调用"resolve(成功后的数据)",Promise对象将由pending状态变为resolved状态
    // 处理失败后调用"reject(原因)",Promise对象将由Pending状态变为rejected状态
}

下面用 Promise 对象封装通过 Request 框架发送的 HTTP 请求,封装前的代码如下。

import * as request from 'request'

request('http://www.baidu.com', function (error, response, body) {
    if (!error && response.statusCode == 200) {
        console.log(body) //显示百度首页的HTML源码
    }
});

使用 Promise 对象封装后的部分代码如下。

import * as request from 'request'

function GetHttpResponse(url) {
    return new Promise((resolve, reject) => {
        request(url, function (error, response, body) {
            if (!error) {
                if (response.statusCode == 200) {
                    resolve(body);
                }
                else {
                    reject(`http status code is ${response.statusCode}`)
                }
            }
            else {
                reject(error);
            }
        });
    });
};

以上代码先声明了一个函数 GetHttpResponse(),用于创建并返回新的 Promise 对象。在实例化 Promise 对象时,传入的封装异步操作的自定义函数使用 request() 函数发送网络请求并获得响应。request() 函数的用法和前面并没有太大差异,都需要传入请求的 URL 以及回调函数。在回调函数中,如果成功获得响应(error 对象为空且 response.status 等于 200),则调用 resolve() 内置函数,并传入 body 作为参数;如果出现异常(当 error 对象有值时),则调用 reject() 函数,并传入 error 对象作为参数;如果相应状态码不正常(response.status 不是 200),则调用 reject() 函数,并传入一串自定义说明作为参数。

TypeScript 的默认编译版本为 ECMAScript 5,以上代码需要指定编译成 ECMAScript 6 及以上的版本,而示例代码中以非相对模块形式导入了 Request 框架,代码须按 CommonJS 规范编译成 JavaScript 代码才能执行。

基于以上原因,在编译时需要执行以下命令,同时指定 target 和 module。

$ tsc a.ts --target esnext --module commonjs

之后就可以使用 Promis 对象来执行异步操作,Promise 对象实例化后封装异步操作的自定义函数将立即执行,可以通过 Promise 对象的 then() 方法接收上一个 Promise 对象在自定义函数中传给内置函数 resolve() 的参数并进行处理。

例如,在请求的响应返回后,将结果输出到控制台的代码如下。

GetHttpResponse("http://www.baidu.com").then(body => {
    console.log(body);
});

截至目前,这种方式似乎并没有体现出 Promise 对象的优势,看似还更复杂。然而,真的是这样吗?

还记得前面的回调灾难场景吗?某个业务需要连续调用多个 API,每个 API 都要从上一个 API 中获取数据,例如,需要先后获取产品主数据、产品评论、产品促销和产品推荐的信息。如果使用 Promise 对象来处理,代码如下。

let product: any = {};
GetHttpResponse("http://xxx.api/product").then(body => {
    product.mainInfo = JSON.parse(body as string);
    return GetHttpResponse("http://xxx.api/product/reviews");
}).then(body => {
    product.reviews = JSON.parse(body as string);
    return GetHttpResponse("http://xxx.api/product/promotinon");
}).then(body => {
    product.promotinon = JSON.parse(body as string);
    return GetHttpResponse("http://xxx.api/recommendation");
}).then(body => {
    product.recommendation = JSON.parse(body as string);
    //其他操作,例如将Product渲染到UI...
});

then() 方法的返回值是一个新的 Promise 对象,因此可以接着调用这个新 Promise 对象的 then() 方法,以流式编程的风格,形成自上而下的调用链。

可以看到,Promise 对象消除了回调灾难,改善了书写形式,代码再也没有横向增长,无论有多少互相依赖的业务,代码都只向下增长。代码将按照一定的顺序,通过 then() 方法一步步地向下执行,层次结构更加清晰。

当 then() 方法内的函数没有明确返回新的 Promise 对象时,then() 方法将返回一个默认的空 Promise 对象,确保后续能够持续调用 Promise 对象的方法,示例代码如下。

GetHttpResponse("http://www.baidu.com")
    .then(p => console.log("first then"))
    .then(p => console.log("second then"))
    .then(p => console.log("thrid then"));

错误处理

Promise 对象还有一个 catch() 方法,它只会在自定义函数调用 reject() 方法或 then() 方法并且内部产生错误时才执行。当在自定义函数中调用 reject() 函数时,catch() 方法将接收到在自定义函数中传给内置函数 reject() 的参数;当 then() 方法出错时,catch() 方法将接收到在 then() 方法中产生的 error 对象。

catch() 方法返回的是也一个新的 Promise 对象。当 catch() 方法内的函数没有明确返回新的 Promise 对象时,catch() 方法将返回一个空的 Promise 对象,确保后续能够持续调用 Promise 对象的方法。

例如,为了访问一个不存在的网址,由于无法正常获取响应,因此自定义函数将调用内置的 reject() 函数,这个错误可以用 Promise 对象的 catch() 方法来处理。

GetHttpResponse("http://xxxxxxx").then(body => {
    //throw new Error("something error!")
    console.log(body);
}).catch(error => {
    console.log("there is something wrong!");
    console.log(error)
});

输出结果如下。

> there is something wrong!
> Error: getaddrinfo ENOTFOUND xxxxxxx
      at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:67:26) {
    errno: -3008,
    code: 'ENOTFOUND',
    syscall: 'getaddrinfo',
    hostname: 'xxxxxxx'
}

使用 catch() 方法不仅可以处理主动调用 reject() 内置函数的情况,还可以处理 then() 方法中产生的错误。例如,以下代码中的 URL 虽然可以访问,但是在 then() 方法中抛出了一个错误,这个错误将会使代码跳转到最近的一个 catch() 方法中,并将 error 对象传递给 catch() 方法中的函数。

GetHttpResponse("http://www.baidu.com").then(body => {
    throw new Error("something error!");
    console.log(body);
}).catch(error => {
    console.log("there is something wrong!");
    console.log(error)
});

输出结果如下。

> there is something wrong!
> Error: something error!

使用 catch() 方法可以获取在它前面的任何 then() 方法中产生的错误。例如,以下代码连续调用了 3 个 then() 方法,末尾处调用了 1 个 catch() 方法。在第 2 个 then() 方法中抛出了错误,catch() 方法也捕获到了这个错误。

GetHttpResponse("http://www.baidu.com").then(body => {
    console.log("first then");
}).then(p => {
    console.log("second then");
    throw new Error("something error!");
    console.log("thrid then");
}).then(p => {
    console.log("forth then");
}).catch(error => {
    console.log("there is something wrong!");
    console.log(error);
});

输出结果如下。

> first then
> second then
> there is something wrong!
> Error: something error!

由于 catch() 方法之后也会返回 Promise 对象,因此可以再次执行 then() 方法,示例代码如下。

GetHttpResponse("http://www.baidu.com").then(body => {
    throw new Error("something error!");
}).catch(error => {
    console.log("there is something wrong!");
    console.log(error);
    return GetHttpResponse("http://www.baidu.com");
}).then(body => {
    console.log(body as string);
});

最终必须被执行的代码

在异步操作中,可能会有一些最终必须执行的代码,例如,释放资源或清理操作的代码等。由于使用 Promise 对象时可能同时调用 then() 方法和 catch() 方法,因此最终必须执行的代码需要在这两个方法中各写一份,这会造成冗余。例如,以下代码最终都会执行 console.log("call api done!")。

GetHttpResponse("http://www.baidu.com").then(body => {
    console.log(body);
    console.log("call api done!")
}).catch(error => {
    console.log("there is something wrong!");
    console.log(error)
    console.log("call api done!")
});

此时可以使用 finally() 方法,将必须执行的代码放到 finally() 方法的函数中,这样当 finally 前面的 then() 和 catch() 方法执行完毕后,无论是否产生错误,最终 finally() 方法都会执行。示例代码如下。

GetHttpResponse("http://www.baidu.com")
    .then(p => { console.log("first then"); })
    .catch(p => { console.log("first catch"); })
    .finally(() => { console.log("first finally") })
    .then(p => { console.log("second then"); })
    .catch(p => { console.log("second catch"); })
    .finally(() => { console.log("second finally") })

组合Promise对象

你可以将多个 Promise 对象组合到一起,形成一个新的 Promise 对象。通过 Promise.all() 静态方法能够完成此操作,向 all() 方法传入的参数为 Promise 对象数组,返回值为新生成的一个 Promise 对象,只有当数组中的所有 Promise 对象执行成功并变成 resolved 状态时,这个新的 Promise 对象才能执行 then() 方法中的函数。

例如,以下代码定义了 3 个 Promise 对象,它们分别访问不同的网址。使用 Promise.all() 方法将它们组合到一起,然后调用这个组合后的 Promise 对象的 then() 方法。函数中的结果参数 values 也是一个数组,其各个元素分别对应各个子 Promise 对象的执行结果。

let promise1 = GetHttpResponse("http://www.baidu.com");
let promise2 = GetHttpResponse("http://www.bing.com");
let promise3 = GetHttpResponse("http://www.sogou.com");

Promise.all([promise1, promise2, promise3]).then(values => {
    console.log(values[0]); //输出baidu的HTML源码
    console.log(values[1]); //输出bing的HTML源码
    console.log(values[2]); //输出sogou的HTML源码
});

对于前面回调灾难的问题,还可以通过组合 Promise 对象进一步简化代码,示例代码如下。

let product: any = {};
Promise.all([
    GetHttpResponse("http://xxx.api/product"),
    GetHttpResponse("http://xxx.api/product/reviews"),
    GetHttpResponse("http://xxx.api/product/promotinon"),
    GetHttpResponse("http://xxx.api/recommendation")
]).then(values => {
    product.mainInfo = JSON.parse(values[0] as string);
    product.reviews = JSON.parse(values[1] as string);
    product.promotinon = JSON.parse(values[2] as string);
    product.recommendation = JSON.parse(values[3] as string);
    //其他操作,例如将Product渲染到UI...
});

注意,如果任何一个子 Promise 对象执行失败,变成 rejected 状态,那么组合后的 Promise 对象也将变成 rejected 状态,此时需要用 catch() 方法进行处理。

创建resolved或rejected状态的Promise对象

通常情况下,一个 Promise 对象创建后将处于 pending 状态,直到运行才产生结果。但也可以直接创建 resolved 或 rejected 状态的 Promise 对象,这需要使用 Promise 对象的 resolve() 或 reject() 静态函数。具体语法如下。

Promise.resolve(成功后的数据);  //创建resolved状态的Promise对象
Promise.reject(原因);          //创建rejected状态的Promise对象

当 resolved 或 rejected 状态的 Promise 对象创建后,就可以直接使用它的 then() 或 catch() 方法了。示例代码如下。

let promise1 = Promise.resolve("hello");
promise1.then(p => console.log(p));  //输出hello

let promise2 = Promise.reject("something wrong");
promise2.catch(p => console.log(p)); //输出something wrong