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只关心两种情况:成功或失败。
如果成功,则返回一个回调函数,并将成功加工的数据作为参数向该回调函数传入。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);} );
将使得程序正常运行。
注意,Promise的catch也是一个回调方法,而不是一个语句。
如果我们不需要抛出异常,则如下所示,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是有状态的,开始时其状态为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的内部细节,以加深对其了解。
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方法则当一人成功则触发成功,或当一人出局,就会触发出局。
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所获得的数据。
在上面,我们使用这样的代码来创建一个Promise对象:
let a = new Promise((resolve) => { resolve(5);});
这样的代码还不够简练。我们可以编写等效的代码如下:
let a = Promise.resolve(5);console.log(a); // Promise {<fullfilled>: 1}
Promise的静态方法resolve直接创建并返回一个Promise对象,并且直接为then回调函数传入参数值5。因此,我们可以直接调用a的then方法:
a.then(value => { console.log(value);});
用这种方法创建并返回一个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);});
虽然涉及到的对象较多,但思路清晰后,应该不难理解。