专业编程基础技术教程

网站首页 > 基础教程 正文

JavaScript Promise 详解 js中promise的使用与理解

ccvgpt 2024-11-05 09:47:47 基础教程 5 ℃

何为Promise

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

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

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

JavaScript Promise 详解 js中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: 45
2. small number: 27
3. small number: 33
4. small number: 12
5. small number: 27
6. 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 settled
Promise {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);
});

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

Tags:

最近发表
标签列表