深入解析JavaScript Promise

发表时间: 2023-06-17 16:16

何为Promise

Promise是一个用于异步执行的对象。它可以在当前的工作成功后,使用then方法来执行成功后的下一步工作。它可以用来取代原来的回调函数。下面是传统的异步工作机制:

function loadFile(successfulFunc, failedFunc) {    // load data    let data = ...;        if (successful) {        successfulFunc(data);    } else {        failedFunc(errMsg);    }}

两个回调函数作为参数传入。这样可能会导致嵌套的代码过长。而Promise可以编写下面的代码:

let a = new Promise(...);a.then((value) => {    trim(value);}).then((newValue) => {    print(newValue);}).catch((errMsg) => {    console.error(errMsg);});

then方法是一个回调函数,由JavaScript将上一步成功处理后的数据作为参数自动传入。在第二步中,如果trim函数也返回一个Promise对象,则可以继续串联下去。看起来简洁而有序。

而如果在串联过程中出现任何错误,则可使用catch语句来捕获它。


Promise如何向下传递上一步的数据

从上面可以看出,Promise只关心两种情况:成功或失败。

如果成功,则返回一个回调函数,并将成功加工的数据作为参数向该回调函数传入。then方法,其参数就是这个回调函数。由于该函数已经注入了成功加工的数据,因此我们可以直接使用该数据。

如果失败,则抛出一个错误,并将失败原因为参数向该回调函数传入。用户可以使用catch方法进行捕获。

如何算成功,如何算失败,当然是业务逻辑的问题,应由用户自主定义。Promise只关心:

1、如果成功,数据是什么?

2、如果失败,出错信息是什么?

因此,Promise构造函数只有一个参数,该参数是一个函数。这个函数,又带有两个回调函数作为其参数,分别负责接收成功的数据与失败的错误信息。

let paramFunc = function(resolveFunc, rejectFunc) {    ...};let aPromise = new Promise(paramFunc);

现在,假设我们要随机生成一个[1, 100]范围内的随机数,如果在[51, 100]的范围内,则视为成功;否则,视为失败。

function getRandomIntInclusive(min, max) {    min = Math.ceil(min);    max = Math.floor(max);    return Math.floor(Math.random() * (max - min + 1) + min);}let paramFunc = function(resolveFunc, rejectFunc) {    let randNum = getRandomIntInclusive(1, 100);        if (randNum >= 51 && randNum <=100) {        resolveFunc(randNum);    } else {        rejectFunc('Number is too small');    }};let aPromise = new Promise(paramFunc);

生成一个随机数,如果在[51, 100]范围内,则视为成功,则以该数作为参数调用resolveFunc方法。resolveFunc方法将作为参数传递给then方法。

如果随机数落在[1, 50]范围内,则视为失败,则以字符串Number is too small作为参数调用rejectFunc方法。rejectFunc方法将作为参数传递给catch方法。

此时运行程序,将出现错误:

Uncaught (in promise) Number is too small

这属于“未捕获异常”的错误。

因为我们调用了rejectFunc回调函数,且一旦随机数小于等于50时,将触发此函数。此函数实际上是一个抛出异常的函数,该函数以我们所提供的错误信息作为参数构造一个Error对象并抛出此异常。故需要用户予以捕获。综上,下面的代码:

aPromise    .then(        (value) => {console.log(value);}    )    .catch(        (errMsg) => {console.error(errMsg);}    );

将使得程序正常运行。

注意,Promisecatch也是一个回调方法,而不是一个语句。


只有resolveFunc的Promise

如果我们不需要抛出异常,则如下所示,Promise的构造方法的参数paramFunc的参数可以只带有一个resolveFunc函数。

function getRandomIntInclusive(min, max) {    min = Math.ceil(min);    max = Math.floor(max);    return Math.floor(Math.random() * (max - min + 1) + min);}let paramFunc = function(resolveFunc) {    let randNum = getRandomIntInclusive(1, 100);    let count = 1;    while(randNum <= 50) {        console.log(`${count++}. small number: ${randNum}`);        randNum = getRandomIntInclusive(1, 100);    }    resolveFunc({count: count, number: randNum});};let aPromise = new Promise(paramFunc);aPromise    .then((value) => {        console.log(`${value.count}. big number: ${value.number}`);    });

在上面的代码中,如果生成了一个小数,与其抛出异常,不如直接重新生成一个随机数,直至该随机数为大数为止。然后,我们以一个对象{count: count, number: randNum}的形式作为参数传给resolveFunc。而在then方法中,对其进行解包即可。

多次运行这个例子,有时可能一次就能得到大数;而有时可能有好几次都是小数,最后一次才是大数。

1. small number: 452. small number: 273. small number: 334. small number: 125. small number: 276. big number  : 59

这个例子也演示了Promise的真实本质:它能够以异步的方式,不知疲倦地干着许多见不得人的脏活、累活,当这些工作都完成后,它才通过then方法向我们一次性地递交最终的成果。


查看Promise的状态

Promise是有状态的,开始时其状态为pending;如果成功,其状态为resolved;如果失败,其状态为rejected。但,我们不能直接使用下面的代码来查看其状态:

console.log(aPromise.status);  // undefined

只能查看aPromise的全部内部状态:

console.log(aPromise);  // all inner states

下面的代码可查看其全部内部状态的细节:

let aPromise = new Promise((resolve) => {    setTimeout(() => {        resolve();    }, 5 * 1000);});let isJobDone = false;function showStatus(aPromise) {    console.log(aPromise);    if (isJobDone) {        clearInterval(tickId);    }}let tickId = setInterval(() => showStatus(aPromise), 1 * 1000);aPromise.then(() => {            console.log('Time up, Promise settled');            isJobDone = true;        });

首先,在创建aPromise对象时,令其5秒后完成工作。

其次,每隔1秒,查看其状态。

第三,在工作完成后,最后一次查看其状态,并取消定时器。

运行代码,显示:

Promise {status: "pending"}Promise {status: "pending"}Promise {status: "pending"}Promise {status: "pending"}Time up, Promise settledPromise {status: "resolved", result: "well done!"}

需注意的是,在Promise内部,使用属性名称result来存储resolve的参数值。

虽然只能看而不能调用,但至少可以帮助我们了解Promise的内部细节,以加深对其了解。


多个Promises的并发控制

Promise有几个静态方法,可以有效地管理多个Promises如何协同工作。

先看all方法。此方法在所有promises对象都成功,或只要有一个失败时,就会触发。

let promise1 = new Promise((resolve) => {    setTimeout(() => {        resolve(`First job done`);    }, 1 * 1000);});let promise2 = new Promise((resolve) => {    setTimeout(() => {        resolve(`Second job done`);    }, 2 * 1000);});let promise3 = new Promise((resolve) => {    setTimeout(() => {        resolve(`Third job done`);    }, 3 * 1000);});Promise.all([promise1, promise2, promise3])    .then((values) => {        console.log(values);  // ["First job done", "Second job done", "Third job done"]    });

共有3个promises对象,分别在1秒、2秒、3秒后完成各自的工作。上面的代码将在3秒后,将所有工作成果都打包进一个数组中并打印出来。类似于接力赛跑,3个选手都跑到终点线后,方可打印成绩。而如果期间有任何一个选手出局,也会立即打印该队的成绩。

allSettled方法则不管成败,必须等待他们每个人的最后结果。

any方法则不管成败,只要有一个完成了工作,或所有人都出局,就会触发。

race方法则当一人成功则触发成功,或当一人出局,就会触发出局。


将Ajax转换为Promise

import {ajax} from '/js/esm/ajax.js';function ajaxLoadTextAsPromise() {    return new Promise((resolve) => {        ajax.loadText('/test.txt', (data) => {            resolve(data);        });    });}ajaxLoadTextAsPromise().then((data) => {    console.log(data);});

要旨:

1、最外层返回一个Promise对象

2、Promise对象内部,调用resolve方法传递AJAX所获得的数据。


神奇的resolve静态方法

在上面,我们使用这样的代码来创建一个Promise对象:

let a = new Promise((resolve) => {    resolve(5);});

这样的代码还不够简练。我们可以编写等效的代码如下:

let a = Promise.resolve(5);console.log(a);  // Promise {<fullfilled>: 1}

Promise的静态方法resolve直接创建并返回一个Promise对象,并且直接为then回调函数传入参数值5。因此,我们可以直接调用athen方法:

a.then(value => {    console.log(value);});

用这种方法创建并返回一个Promise对象,非常方便。


串联两个以上的Promise

let a = Promise.resolve(1);let b = a.then(value => {    value += 1;    return Promise.resolve(value);});console.log(a);  // Promise {<fullfilled>: 1}console.log(b);  // Promise {<pending>}

变量a在调用then方法时,在其内部,对数值加1,再返回一个新的Promise对象。

此时查看两个对象的状态,变量b由于未调用then方法,因此还属于pending状态。

b.then(value => {    console.log(value);  // 2});console.log(b);  // Promise {<pending>}

上面的代码引入了另一个新的变量用以存储所返回的Promise对象。但更直接的,我们可以不引入新对象,直接串联:

let a = Promise.resolve(1);a.then(value => {    value += 1;    return Promise.resolve(value);}).then(value => {    console.log(value);  // 2});console.log(a);  // Promise {<fullfilled>: 1}

因此,如果需要串联,只需在then方法中返还一个新的Promise对象即可。


一个稍微复杂的例子

结合上面几节的内容,我们现在可以编写一个实用的用例代码。

static load六十四卦Text(卦名) {    return ajax.loadTextAsPromise('/data/txt/六十四卦.txt')        .then(text => {            let regexp = new RegExp(`(${卦名}(卦.{1,3})\n(.+\n){16}(用.+\n.+\n)?)`, 'g');            let resultArr;            let 六十四卦Text;            while((resultArr = regexp.exec(text)) !== null) {                六十四卦Text = resultArr[0];            }            return Promise.resolve(六十四卦Text);        });}

此例中,loadTextAsPromise返回一个Promise对象。虽是文本文件,但所有六十四卦的内容都在里面,故需要根据特定的卦名取出相应的卦即可。上面代码使用正则表达式来快速取出符合条件的卦。由于需要对数据进行进一步加工,故在then方法中又再次返回一个Promise对象。

这样,在客户端就可以使用这样的代码:

MhysTextLoader.load六十四卦Text('大有').then(data => {    console.log(data);});

虽然涉及到的对象较多,但思路清晰后,应该不难理解。