深入理解Promise

Promise

背景

与Java和C++这种允许同时执行多段不同代码的多线程语言不同,JS引擎是建立于单线程事件循环这样一个概念之上的,单线程意味着同一时刻只能执行一段代码。代码会被放置在一个任务队列中,每当一段代码准备被执行,它就会被添加到这个任务队列中。当JS引擎结束当前代码的执行后,事件循环就会执行队列中的下一个任务。事件循环(event loop)是JS引擎的一个内部处理线程,能监视代码的执行并管理任务队列。既然是一个队列,任务就会从队列中的第一个开始,依次运行到最后一个。

事件模型

当用户点击一个按钮或按下键盘上的一个键时,一个事件(例如onclick)就被触发了。该事件可能会对此交互进行响应,从而将一个新的任务添加到任务队列的末尾。这就是JS关于异步编程的最基本形式。事件处理代码(回调callback)直到事件发生后才会被执行,此时它会拥有合适的上下文。例如:

--------------------------------------------

let button = document.getElementById("btn");

button.onclick = function(event) {

console.log("点击了...");

};

--------------------------------------------

在此代码中,console.log("点击了...")直到button被点击后才会被执行。当button被点击,赋值给onclick的函数就被添加到任务队列的末尾,并在队列中前面所有任务都结束之后才执行。

为了理解这个时间模型,举一个例子

--------------------------------------------

//在500毫秒之后添加此函数到任务队列

setTimeout(function() {

console.log("Timeout");

}, 500);

console.log("hello world!");

--------------------------------------------

是的,结果会是:

hello world!

Timeout

你也许会认为造成这个顺序的原因是这500毫秒的延迟,但其实不然,我们可以把延时改为

--------------------------------------------

//在毫秒之后添加此函数到任务队列

setTimeout(function() {

console.log("Timeout");

}, 0);

console.log("hello world!");

--------------------------------------------

依然会得到相同的结果:

hello world!

Timeout

原因就在于setTimeout生成的是一个异步的任务,这个任务会被放在队列的末尾,只有主进程的代码跑完了才会轮到这个任务队列里的任务执行。

回调模式

当Node.js被创建时,它通过回调函数编程模式提升了异步编程模型。回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函数(即回调函数)是作为参数传入的,例如:

--------------------------------------------

readFile("omgweb.txt",function(error,content){

if (error) {

throw err;

}

console.log(content);

});

console.log("hello world!");

--------------------------------------------

readFile()函数用于读取磁盘中的文件(由第一个参数指定),并在读取完毕后执行回调函数(即第二个参数)。如果存在错误,回调函数的error参数会是一个错误对象;否则content参数就会以字符串形式包含文件内容。

console.log("hello world!")会在readFile()被调用后立即进行输出,肯定要早于console.log(content)。当readFile()结束操作后,它会将回调函数以及相关参数作为一个新的任务添加到任务队列的末尾。在任务队列中之前的任务全部结束后,该任务才会执行。

再看一个例子:

--------------------------------------------

readFile("omgweb.txt", function(error, content) {

if (error) {

throw err;

}

writeFile("omgweb.txt", function(error) {

if (error) {

throw error;

}

console.log("omgweb.txt was written!");

});

});

--------------------------------------------

在此代码中,对于readFile()的一次成功调用引出了另一个异步调用,即调用writeFile()函数。注意这两个函数都使用了检查error的同一基本模式。当readFile()执行结束后,它添加一个任务到任务队列,从而导致writeFile()在之后被调用(假设没有出现错误)。接下来,writeFile()也会在执行结束后向队列添加一个任务。

这种模式运作得相当好,但你可能会觉得这样很恶心,这种嵌套过多回调函数的形式被恶心它的程序员称为回调地狱(callback hell):

--------------------------------------------

method1(function(err, result) {

if (err) {

throw err;

}

method2(function(err, result) {

if (err) {

throw err;

}

method3(function(err, result) {

if (err) {

throw err;

}

method4(function(err, result) {

if (err) {

throw err;

}

method5(result);

});

});

});

});

--------------------------------------------

像本例一样嵌套多个方法调用会创建错综复杂的代码,会难以理解与调试。Promise能大幅度改善这种情况。

Promise基础

Promise是为异步操作的结果所准备的占位符。函数可以返回一个Promise,而不必订阅一个事件或向函数传递一个回调参数,就像这样:

---例子1------------------------------------

let promise = readFile("omgweb.txt");

--------------------------------------------

Promise的生命周期

每个Promise都会经历一个短暂的生命周期,初始为挂起态(pending state),表示异步操作尚未结束。一旦异步操作结束,Promise就会进入两种可能状态之一:

1.已完成(fulfilled):Promise的异步操作已成功结束;

2.已拒绝(rejected):Promise的异步操作未成功结束,可能是一个错误,或由其他原因导致。

内部的[[PromiseState]]属性会被设置为"pending"、"fulfilled"或"rejected",以反映Promise的状态。该属性并未在Promise对象上被暴露出来,因此你无法以编程方式判断Promise到底处于哪种状态。不过你可以使用then()方法在Promise的状态改变时执行一些特定操作。

then()方法在所有的Promise上都存在,并且接受两个参数。第一个参数是Promise被完成时调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数。第二个参数是Promise被拒绝时调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的任何附加数据。

传递给then()的两个参数都是可选的,因此你可以监听完成与拒绝的任意组合形式。例如:

--------------------------------------------

let promise = readFile("omgweb.txt");

//完成和拒绝都有

promise.then(

function(contents) {

console.log(contents);

},

function(err) {

console.error(err.message);

}

);

//只有完成

promise.then(

function(contents) {

console.log(contents);

}

);

//只有拒绝

promise.then(

null,

function(err) {

console.error(err.message);

}

);

--------------------------------------------

这三个then()调用都操作在同一个Promise上。第一个调用同时监听了完成与失败;第二个调用只监听了完成,错误不会被报告;第三个则只监听了拒绝,并不报告成功信息。

Promis也具有一个catch()方法,其行为等同于只传递拒绝处理函数给then()。例如以下的catch()与then()调用是功能等价的。

--------------------------------------------

promise.catch(function(err) {

console.error(err.message);

});

//等价于

promise.then(null, function(err) {

console.error(err.message);

});

--------------------------------------------

then()与catch()背后的意图是让你组合使用它们来正确处理异步操作的结果。此系统要优于事件与回调函数,因为它让操作是成功还是失败变得完全清晰(事件模式倾向于在出错时不被触发,而在回调函数模式中你必须始终记得检查错误参数)。若未给Promise添加拒绝处理函数,所有的错误就会静默发生。所以建议始终添加一个拒绝处理函数,即使该处理程序只是用于打印错误日志。

即使完成或拒绝处理函数在Promise已经被解决之后才添加到任务队列,它们仍然会被执行。这允许你随时添加新的完成或拒绝处理函数,并保证它们会被调用。例如:

--------------------------------------------

let promise = readFile("omgweb.txt");

promise.then(function(contents) {

console.log(contents);

//添加另一个

promise.then(function(contents) {

console.log(contents);

});

});

--------------------------------------------

在此代码中,完成处理函数又为同一个Promise添加了另一个完成处理函数。这个Promise此刻已经完成了,因此新的处理程序就被添加到任务队列,并在就绪时(前面的任务执行完毕后)被调用。拒绝处理函数使用同样方式工作。

每次调用then()或catch()都会创建一个新的任务,它会在Promise已完成或者已拒绝时被执行。但这些任务最终会进入一个完全为Promise保留的任务队列。这个独立队列的确切细节对于理解如何使用Promise是不重要的,你只需理解任务队列通常来说是如何工作的。

创建未决的Promise(未决的意思是异步任务没有执行结束,还没有到已完成或者已拒绝的时刻,已决是异步任务已经执行结束,到了已完成或者已拒绝的时刻)

一个Promise实例使用Promise这个构造函数来创建。此构造函数接受单个参数:一个被称为执行器(executor)的函数,其中包含了初始化Promise的代码。该执行器会被传递两个名为resolve()与reject()的函数作为参数。resolve()函数在执行器成功结束时被调用,用于示意该Promise已经准备好被决议(resolved),而reject()函数则表明执行器的操作已失败。

例如在Node.js中使用了一个Promise,实现了前面的readFile()函数:

// Node.js范例

let fs = require("fs");

function readFile(filename) {

return new Promise(

function(resolve,reject) {

fs.readFile(

filename,

{ encoding: "utf8" },

function(err, contents) {

if (err) {

reject(err);//将当前promise的状态变为已拒绝态

return;

}

resolve(contents);//将当前promise的状态变为已完成态

let promise = readFile("omgweb.txt");

//同时监听完成与拒绝

promise.then(

function(contents) {

console.log(contents);

},

function(err) {

console.error(err.message);

}

);

Node.js原生的fs.readFile()异步调用被包装在一个Promise中。执行器要么传递错误对象给reject()函数,要么传递文件内容给resolve()函数。要记住执行器会在readFile()被调用时立即运行。

书上讲在resolve和reject在执行的时候就会创建一个异步任务放到异步队列,个人觉得这个说法有问题的,我的理解是这样的:

当前promise有两个属性[[PromiseStatus]][[PromiseValue]]

[[PromiseStatus]]的初始值是"pendding"

[[PromiseValue]]的初始值是undefined

当resolve()在执行器内部被调用时就将state的值变为"resolved"

当reject()在执行器内部被调用时就将state的值变为"rejected"

只有promise.then和promise.catch方法才会触发异步任务,并且将异步任务放置到异步队列中。这样在所有的同步的程序全部跑完之后,事件循环发现主线程中已经没有同步的程序要运行了,就会去检查异步任务队列。当前面的异步任务执行结束轮到promise.then创建的那个任务的时候,这个任务假设是TASK_X的工作应该是先检查promise.state是什么(promise.catche类似)

1、如果是"fulfilled",那么就执行第一个参数,当前异步任务从异步任务队列中销毁//then中的两个参数都是函数

2、如果是"rejected",那么就那么就执行第二个参数,当前异步任务从异步任务队列中销毁

3、如果还是"pendding",当前异步任务从异步任务队列中销毁,再创建一个和TASK_X一样的异步任务到异步任务队列的末尾,如此反复

Promise的执行器会立即执行,早于源代码中在其之后的任何代码。例如:

--------------------------------------------

let promise = new Promise(

function(resolve, reject) {

console.log("Promise");

resolve();

}

);

console.log("hello world!");

--------------------------------------------

输出结果为:

Promise

hello world!

调用resolve()触发了一个异步操作。传递给then()与catch()的函数会异步地被执行,并且它们也被添加到了任务队列(先进队列再执行)。此处有个例子:

--------------------------------------------

let promise = new Promise(

function(resolve, reject) {

console.log("Promise");

resolve();

}

);

promise.then(

function() {

console.log("Resolved");

}

);

console.log("hello world!");

--------------------------------------------

此例的输出结果为:

Promise

hello world!

Resolved

注意:尽管对then()的调用出现在console.log("hello world!")代码行之前,它实际上稍后才会执行(与执行器中那行"Promise"不同)。这是因为完成处理函数与拒绝处理函数总是会在执行器的操作结束后被添加到任务队列的尾部。

创建已决的Promise

基于Promise执行器行为的动态本质,Promise构造函数就是创建未决的Promise的最好方式。但若你想让一个Promise代表一个已知的值,那么安排一个单纯传值给resolve()函数的任务并没有意义。相反,有两种方法可使用指定值来创建已决的Promise。

使用Promise.resolve()

Promise.resolve()方法接受单个参数并会返回一个处于已完成状态的Promise。这意味着没有任何任务调度会发生,并且你需要向Promise添加一个或更多的完成处理函数来提取这个参数值。例如:

--------------------------------------------

let promise = Promise.resolve(42);

promise.then(function(value) {

console.log(value);// 42

});

--------------------------------------------

此代码创建了一个已完成的Promise,因此完成处理函数就接收到42作为value参数。若一个拒绝处理函数被添加到此Promise该拒绝处理函数将永不会被调用,因为此Promise绝不可能再是拒绝态。

使用Promise.reject()

你也可以使用Promise.reject()方法来创建一个已拒绝的Promise。此方法像Promise.resolve()一样工作,区别是被创建的Promise处于已拒绝状态,如下:

--------------------------------------------

let promise = Promise.reject(42);

promise.catch(function(value) {

console.log(value);//42

});

--------------------------------------------

任何附加到这个Promise的拒绝处理函数都将会被调用,而完成处理函数则不会执行。

若你传递一个Promise给Promise.resolve()或Promise.reject()方法,该Promise会不作修改原样返回。在《DEEP IN ES6》这本书上是这样讲的,但是我在chrome上测试得到如下结论:Promise.resolve()调用会将该Promise原样返回。此后,若决议原Promise,在then()中可以接收到原例中的参数42(也就是返回的promise的[[PromiseValue]]为42);而若拒绝原Promise,则在catch()中可以接收到参数为原Promise(也就是返回的promise的[[PromiseValue]]为传进来的Promise)。

thenable

Promise.resolve()与Promise.reject()都能接受非Promise的thenable作为参数。当传入了非Promise的thenable时,这些方法会创建一个新的Promise,此Promise会在then()函数之后被调用。

当一个对象拥有一个能接受resolve与reject参数的then()方法,该对象就会被认为是一个非Promise的thenable,就像这样:

--------------------------------------------

let thenable = {

then: function(resolve, reject) {

resolve(42);

}

};

--------------------------------------------

此例中的thenable对象,除了then()方法之外没有任何与Promise相关的特征。你可以调用Promise.resolve()来将thenable转换为一个已完成的Promise :

--------------------------------------------

let thenable = {

then:function(resolve, reject) {

resolve(42);

}

};

let p1 = Promise.resolve(thenable);

p1.then(function(value) {

console.log(value);// 42

});

--------------------------------------------

//p1的值为:

{

[[PromiseStatus]]:"resolved",

[[PromiseValue]]:42

}

在此例中,Promise.resolve()调用了thenable.then(),确定了这个thenable的Promise状态:由于resolve(42)在thenable.then()方法内部被调用,这个thenable的Promise状态也就被设为已完成。一个名为p1的新Promise被创建为完成态,并从thenable中接收到了值(此处为42 ),于是p1的完成处理函数就接收到一个值为42的参数。

使用Promise.resolve(),同样还能从一个thenable创建一个已拒绝的Promise:

--------------------------------------------

let thenable = {

then: function(resolve, reject) {

reject(42);

}

};

let p1 = Promise.resolve(thenable);

p1.catch(function(value) {

console.log(value);// 42

});

--------------------------------------------

//p1的值为:

{

[[PromiseStatus]]:"rejected",

[[PromiseValue]]:42

}

此例类似于上例,区别是此处的thenable被拒绝了。当thenable.then()执行时,一个处于拒绝态的新Promise被创建,并伴随着一个值( 42 )。这个值此后会被传递给p1的拒绝处理函数。

Promise.resolve()与Promise.reject()用类似方式工作,让你能轻易处理非Promise的thenable。在Promise被引入ES6之前,许多库都使用了thenable,因此将thenable转换为正规Promise的能力就非常重要了,能对之前已存在的库提供向下兼容。当你不能确定一个对象是否是Promise时,将该对象传递给Promise.resolve()或Promise.reject()(取决于你的预期结果)是能找出的最好方式,因为传入真正的Promise只会被直接传递出来,并不会被修改(但请注意前面译注提到的特殊情况)。

执行器错误

--------------------------------------------

let promise = new Promise(function(resolve, reject) {

throw new Error("Explosion!");

});

promise.catch(function(error) {

console.log(error.message);// "Explosion!"

});

--------------------------------------------

在此代码中,执行器故意抛出了一个错误。此处在每个执行器之内并没有显式的try-catch,因此错误就被捕捉并传递给了拒绝处理函数。这个例子等价于:

let promise = new Promise(function(resolve, reject) {

try {

throw new Error("Explosion!");

} catch (ex) {

reject(ex);

}

});

promise.catch(function(error) {

console.log(error.message);

});

// "Explosion!"

--------------------------------------------

执行器处理程序捕捉了抛出的任何错误,以简化这种常见处理。但在执行器内抛出的错误仅当存在拒绝处理函数时才会被报告,否则这个错误就会被隐瞒。这在开发者早期使用Promise的时候是一个问题,但JS环境通过提供钩子( hook )来捕捉被拒绝的Promise,从而解决了此问题。

全局的Promise拒绝处理

Promise最有争议的一个问题就是:当一个Promise被拒绝时若缺少拒绝处理函数,就会静默失败。有人认为这是规范中最大的缺陷,因为这是JS语言所有组成部分中唯一不让错误清晰可见的。

由于Promise的本质,判断一个Promise的拒绝是否已被处理并不直观。例如:

--------------------------------------------

let rejected = Promise.reject(42);

//在此刻rejected不会被处理

//一段时间后......

rejected.catch(

function(value) {

//现在rejected已经被处理了

console.log(value);

}

);

--------------------------------------------

无论Promise是否已被解决,你都可以在任何时候调用then()或catch()并使它们正确工作,这导致很难准确知道一个Promise何时会被处理。此例中的Promise被立刻拒绝,但它后来才被处理。虽然下个版本的ES可能会处理此问题,不过浏览器与Node.js已经实施了变更来解决开发者的这个痛点。这些变更不是ES6规范的一部分,但却是使用Promise时的宝贵工具。

Node.js的拒绝处理

在Node.js中,process对象上存在两个关联到Promise的拒绝处理的事件:

unhandledRejection:当一个Promise被拒绝、并在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发;

rejectionHandled:当一个Promise被拒绝、并在事件循环的一个轮次之后再有拒绝处理函数被调用,该事件就会被触发。

这两个事件旨在共同帮助识别已被拒绝但未曾被处理promise。

unhandledRejection事件处理函数接受的参数是拒绝原因(常常是一个错误对象)以及已被拒绝的Promise。以下代码展示了unhandledRejection的应用:

--------------------------------------------

let rejected;

process.on("unhandledRejection", function(reason, promise) {

console.log(reason.message);

console.log(rejected === promise);

});

rejected = Promise.reject(new Error("Explosion!"));

// "Explosion!"

// true

--------------------------------------------

此例创建了一个带有错误对象的已被拒绝的Promise,并监听了unhandledRejection事件。事件处理函数接收了该错误对象作为第一个参数,原Promise则是第二个参数。

rejectionHandled事件处理函数则只有一个参数,即已被拒绝的Promise。例如:

--------------------------------------------

let rejected;

process.on("rejectionHandled", function(promise) {

console.log(rejected === promise);

});

rejected = Promise.reject(new Error("Explosion!"));

// true

//延迟添加拒绝处理函数

setTimeout(function() {

rejected.catch(function(value) {

console.log(value.message);

}, 1000);

// "Explosion!"

});

--------------------------------------------

此处的rejectionHandled事件在拒绝处理函数最终被调用时触发。若在rejected被创建后直接将拒绝处理函数附加到它上面,那么此事件就不会被触发。因为立即附加的拒绝处理函数在rejected被创建的事件循环的同一个轮次内就会被调用,这样rejectionHandled就不会起作用。

为了正确追踪潜在的未被处理的拒绝,使用rejectionHandled与unhandledRejection事件就能保持包含这些Promise的一个列表,之后等待一段时间再检查此列表。例如:

let possiblyUnhandledRejections = new Map();

//当一个拒绝未被处理,将其添加到map

process.on("unhandledRejection", function(reason, promise) {

possiblyUnhandledRejections.set(promise, reason);

});

process.on("rejectionHandled", function(promise) {

possiblyUnhandledRejections.delete(promise);

});

setInterval(function() {

possiblyUnhandledRejections.forEach(

function(reason, promise) {

console.log(reason.message ? reason.message : reason);

//做点事来处理这些拒绝

handleRejection(promise, reason);

}

);

possiblyUnhandledRejections.clear();

}, 60000);

对于未处理的拒绝,这只是个简单追踪器。它使用了一个Map来储存Promise及其拒绝原因,每个Promise都是键,而它的拒绝原因就是相关的值。每当unhandledRejection被触发,Promise及其拒绝原因就会被添加到此Map中。而每当rejectionHandled被触发,已被处理的Promise就会从这个Map中被移除。这样一来,possiblyUnhandledRejections就会随着事件的调用而扩展或收缩。setInterval()的调用会定期检查这个列表,查看可能未被处理的拒绝,并将其信息输出到控制台(在现实情况下,你可能会想做点别的事情,以便记录或处理该拒绝)。此例使用了一个Map而不是Weak Map,这是因为你需要定期检查此Map来查看哪些Promise存在,而这是使用Weak Map所无法做到的。

尽管此例仅针对Node.js,但浏览器也实现了类似的机制来将未处理的拒绝通知给开发者。浏览器的拒绝处理

浏览器同样能触发两个事件,来帮助识别未处理的拒绝。这两个事件会被window对象触发,并完全等效于Node.js的相关事件:

unhandledrejection :当一个Promise被拒绝、而在事件循环的一个轮次中没有任何拒绝处理函数被调用,该事件就会被触发;

rejectionHandled :若一个Promise被拒绝、并在事件循环的一个轮次之后再有拒绝处理函数被调用,该事件就会被触发。

Node.js的实现会传递分离的参数给事件处理函数,而浏览器事件的处理函数则只会接收到包含下列属性的一个对象:

type :事件的名称( "unhandledrejection"或"rejectionhandled" ); promise :被拒绝的Promise对象;

reason : Promise中的拒绝值(拒绝原因)。

浏览器的实现中存在的另一个差异就是:拒绝值( reason )在两种事件中都可用。例如:

--------------------------------------------

let rejected;

window.onunhandledrejection = function(event) {

console.log(event.type);

console.log(rejected === event.promise);

};

window.onrejectionhandled = function(event) {

console.log(event.type);

console.log(rejected === event.promise);

// "unhandledrejection"

// "Explosion!"

// true

// "rejectionhandled"

// "Explosion!"

// true

};

rejected = Promise.reject(new Error("Explosion!"));

--------------------------------------------

此代码使用了DOM 0级写法的onunhandledrejectiononrejectionhandled,对两个事件处理函数都进行了赋值(若你喜欢,也可以使用addEventListener("unhandledrejection")与addEventListener("rejectionhandled") )。每个事件处理函数都接收一个事件对象,其中包含与被拒绝的Promise有关的信息,type、promise与reason属性都可用。以下代码在浏览器中追踪未被处理的拒绝,与Node.js的代码非常相似:

let possiblyUnhandledRejections = new Map();

//当一个拒绝未被处理,将其添加到map

window.onunhandledrejection = function(event) {

possiblyUnhandledRejections.set(event.promise, event.reason);

};

window.onrejectionhandled = function(event) {

possiblyUnhandledRejections.delete(event.promise);

};

setInterval(function() {

possiblyUnhandledRejections.forEach(

function(reason, promise) {

console.log(reason.message ? reason.message : reason);

//做点事来处理这些拒绝

handleRejection(promise, reason);

}

);

possiblyUnhandledRejections.clear();

}, 60000);

这个实现与Node.js的实现几乎一模一样。使用了相同方法在Map中存储Promise及其拒绝值,并在此后进行检查。唯一真正的区别就是在事件处理函数中信息是从何处被提取出来的。

处理Promise的拒绝可能很麻烦,但你才刚开始见识Promise实际上到底有多强大。现在是时候更进一步了——把几个promises串联在一起使用。

串联Promise

到此为止,Promise貌似不过是个对组合使用回调函数与setTimeout()函数的增量改进,然而Promise的内容远比表面上所看到的更多。更确切地说,存在多种方式来将Promise串联在一起,以完成更复杂的异步行为。

每次对then()或catch()的调用实际上创建并返回了另一个Promise,仅当前一个Promise被完成或拒绝时,后一个Promise才会被决议。比如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

p1.then(function(value) {

console.log(value);

}).then(function() {

console.log("Finished");

})

//42

//Finished

--------------------------------------------

对p1.then()的调用返回了第二个Promise,又在这之上调用了then()。仅当第一个Promise已被决议后,第二个then()的完成处理函数才会被调用。假若你在此例中不使用串联,它看起来就会是这样:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = p1.then(function(value) {

console.log(value);

})

p2.then(function() {

console.log("Finished");

});

--------------------------------------------

在这个无串联版本的代码中,p1.then()的结果被存储在p2中,并且随后p2.then()被调用,以添加最终的完成处理函数。显然,p2.then()的调用也会返回了一个Promise。

你一定以为如果在p5里面讲resolve(42)换成reject(42),在10秒之后p5和p6的状态会同事变成rejected,然而并非如此,比如我们用p7代替前面的p5,p8代替前面的p6,然后发现10秒之后p7的转台变成rejected,p8的状态是resolved,如下图所示

但是如果在p8 = p7.then(null,rejectedCallback)的rejectedCallback中抛出异常,那么p8的状态在10秒后会变成rejected,如下图所示

捕获错误

Promise链允许你捕获前一个Promise的完成或拒绝处理函数中发生的错误。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

p1.then(function(value) {

throw new Error("Boom!");

}).catch(function(error) {

console.log(error.message);

});

// "Boom!"

--------------------------------------------

在此代码中,p1的完成处理函数抛出了一个错误,链式调用指向了第二个Promise上的catch()方法,能通过此拒绝处理函数接收前面的错误。若是一个拒绝处理函数抛出了错误,情况也是一样:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

throw new Error("Explosion!");

});

p1.catch(function(error) {

console.log(error.message);

throw new Error("Boom!");

}).catch(function(error) {

console.log(error.message);

});

// "Explosion!"

// "Boom!"

--------------------------------------------

此处的执行器抛出了一个错误,就触发了p1这个Promise的拒绝处理函数,该处理函数随后抛出了另一个错误,并被第二个Promise的拒绝处理函数所捕获。链式Promise调用能察觉到链中其他Promise中的错误。

为了确保能正确处理任意可能发生的错误,应当始终在Promise链尾部添加拒绝处理函数。

在Promise链中返回值

Promise链的另一重要方面是能从一个Promise传递数据给下一个Promise的能力。传递给执行器中的resolve()处理函数的参数,会被传递给对应Promise的完成处理函数,这点你前面已看到过了。你可以指定完成处理函数的返回值,以便沿着一个链继续传递数据。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

p1.then(function(value) {

console.log(value);// "42"

return value + 1;

}).then(function(value) {

console.log(value);// "43"

});

--------------------------------------------

p1的完成处理函数在被执行时返回了value + 1。由于value的值为42 (来自执行器),此完成处理函数就返回了43。这个值随后被传递给第二个Promise的完成处理函数,并被其输出到控制台。

你能对拒绝处理函数做相同的事。当一个拒绝处理函数被调用时,它也能返回一个值。如果这么做,该值会被用于完成下一个Promise,就像这样:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

reject(42);

});

p1.catch(function(value) {

//第一个完成处理函数

console.log(value);// "42"

return value + 1;

}).then(function(value) {

//第二个完成处理函数

console.log(value);// "43"

});

--------------------------------------------

此处的执行器使用42调用了reject(),该值被传递到这个Promise的拒绝处理函数中,从中又返回了value + 1。尽管后一个返回值是来自拒绝处理函数,它仍然被用于链中下一个Promise的完成处理函数。若有必要,一个Promise的失败可以通过传递返回值来恢复整个Promise链。

在Promise链中返回Promise

从完成或拒绝处理函数中返回一个基本类型值,能够在Promise之间传递数据,但若你返回的是一个对象呢?若该对象是一个Promise,那么需要采取一个额外步骤来决定如何处理。研究以下例子:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

resolve(43);

});

p1.then(function(value) {

//第一个完成处理函数

console.log(value);// 42

return p2;

}).then(function(value) {

//第二个完成处理函数

console.log(value);// 43

});

--------------------------------------------

在此代码中,p1安排了一个决议42的任务,p1的完成处理函数返回了一个已处于决议态的Promise:p2。由于p2已被完成,第二个完成处理函数就被调用了。而若p2被拒绝,会调用拒绝处理函数(如果存在的话),而不调用第二个完成处理函数。

关于此模式需认识的首要重点是第二个完成处理函数并未被添加到p2上,而是被添加到第三个Promise。正因为此,上个例子就等价于:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

resolve(43);

});

let p3 = p1.then(function(value) {

//第一个完成处理函数

console.log(value);// 42

return p2;

});

p3.then(function(value) {

//第二个完成处理函数

console.log(value);// 43

});

--------------------------------------------

此处清楚说明了第二个完成处理函数被附加给p3而不是p2。这是一个细微但重要的区别,因为若p2被拒绝,则第二个完成处理函数就不会被调用。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

reject(43);

});

p1.then(function(value) {

//第一个完成处理函数

console.log(value);// 42

return p2;

}).then(function(value) {

//第二个完成处理函数

console.log(value);//永不被调用

});

--------------------------------------------

在此例中,由于p2被拒绝了,第二个完成处理函数就永不被调用。不过你可以改为对其附加一个拒绝处理函数:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

reject(43);

});

p1.then(function(value) {

//第一个完成处理函数

console.log(value);// 42

return p2;

}).catch(function(value) {

//拒绝处理函数

console.log(value);// 43

});

--------------------------------------------

此处p2被拒绝,导致拒绝处理函数被调用,来自p2的拒绝值43会被传递给拒绝处理函数。

从完成或拒绝处理函数中返回thenable,不会对Promise执行器何时被执行有所改变。第一个被定义的Promise将会首先运行它的执行器,接下来才轮到第二个Promise的执行器执行,以此类推。返回thenable只是让你能在Promise结果之外定义附加响应。你能通过在完成处理函数中创建一个新的Promise,来推迟完成处理函数的执行。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

p1.then(function(value) {

console.log(value);// 42

//创建一个新的promise

let p2 = new Promise(

function(resolve, reject) {

resolve(43);

}

);

return p2

}).then(function(value) {

console.log(value);// 43

});

--------------------------------------------

在此例中,一个新的Promise在p1的完成处理函数中被创建。这意味着直到p2被完成之后,第二个完成处理函数才会执行。若你想等待前面的Promise被解决,之后才去触发另一个Promise,那么这种模式就非常有用。

响应多个Promise

本章至今的每个例子在同一时刻都只响应一个Promise。然而有时你会想监视多个Promise的进程,以便决定下一步行动。ES6提供了能监视多个Promise的两个方法:

Promise.all()与Promise.race()。

Promise.all()

Promise.all()方法接收单个可迭代对象(如数组)作为参数,并返回一个Promise。这个可迭代对象的元素都是Promise,只有在它们都完成后,所返回的Promise才会被完成。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

resolve(43);

});

let p3 = new Promise(function(resolve, reject) {

resolve(44);

});

let p4 = Promise.all([p1, p2, p3]);

p4.then(function(value) {

console.log(Array.isArray(value));// true

console.log(value[0]);//42

console.log(value[1]);//43

console.log(value[2]);//44

});

--------------------------------------------

此处前面的每个Promise都用一个数值进行了决议,对Promise.all()的调用创建了新的Promise p4,在p1、p2与p3都被完成后,p4最终会也被完成。传递给p4的完成处理函数的结果是一个包含每个决议值(42、43与44)的数组,这些值的存储顺序保持了待决议的Promise的顺序(与完成的先后顺序无关),因此你可以将结果匹配到每个Promise。

若传递给Promise.all()的任意Promise被拒绝了,那么方法所返回的Promise就会立刻被拒绝,而不必等待其他的Promise结束:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = new Promise(function(resolve, reject) {

reject(43);

});

let p3 = new Promise(function(resolve, reject) {

resolve(44);

});

let p4 = Promise.all([p1, p2, p3]);

p4.catch(function(value) {

console.log(Array.isArray(value));// false

console.log(value);// 43

});

--------------------------------------------

在此例中,p2被使用数值43进行了拒绝,则p4的拒绝处理函数就立刻被调用,而不会等待p1或p3结束执行(它们仍然会各自结束执行,只是p4不等它们)。

拒绝处理函数总会接收到单个值,而不是一个数组,该值就是被拒绝的Promise所返回的拒绝值。本例中的拒绝处理函数被传入了43,反映了来自p2的拒绝。

Promise.race()

Promise.race()提供了监视多个Promise的一个稍微不同的方法。此方法也接受一个包含需监视的Promise的可迭代对象,并返回一个新的Promise,但一旦来源Promise中有一个被解决,所返回的Promise就会立刻被解决。与等待所有Promise完成的Promise.all()方法不同,在来源Promise中任意一个被完成时,Promise.race()方法所返回的Promise就能作出响应。例如:

--------------------------------------------

let p1 = Promise.resolve(42);

let p2 = new Promise(function(resolve, reject) {

resolve(43);

});

let p3 = new Promise(function(resolve, reject) {

resolve(44);

});

let p4 = Promise.race([p1, p2, p3]);

p4.then(function(value) {

console.log(value);// 42

});

--------------------------------------------

在此代码中,p1被创建为一个已完成的Promise,而其他的Promise则需要调度任务。p4的完成处理函数被使用数值42进行了调用,并忽略了其他的Promise。传递给Promise.race()的Promise确实在进行赛跑,看哪一个首先被解决。若胜出的Promise是被完成,则返回的新Promise也会被完成;而胜出的Promise若是被拒绝,则新Promise也会被拒绝。此处有个使用拒绝的范例:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

setTimeout(function(){

resolve(42);

},1000)

});

let p2 = Promise.reject(43);

let p3 = new Promise(function(resolve, reject) {

setTimeout(function(){

resolve(44);

},1000)

});

let p4 = Promise.race([p1, p2, p3]);

p4.catch(function(value) {

console.log(value);// 43

});

--------------------------------------------

继承Promise

正像其他内置类型,你可将一个Promise用作派生类的基类。这允许你自定义变异的Promise,在内置Promise的基础上扩展功能。例如,假设你想创建一个可以使用success()与failure()方法的Promise,对常规的then()与catch()方法进行扩展,可以像下面这样创建该Promise类型:

--------------------------------------------

//使用默认构造函数

class MyPromise extends Promise {

success(resolve, reject) {

return this.then(resolve, reject);

}

failure(reject) {

return this.catch(reject);

}

}

let promise = new MyPromise(function(resolve, reject) {

resolve(42);

});

promise.success(function(value) {

console.log(value);

}).failure(function(value) {

console.log(value);

});

// 42

--------------------------------------------

在此例中,MyPromise从Promise上派生出来,并拥有两个附加方法。success()方法模拟了resolve(),failure()方法则模拟了reject()。

每个附加方法都使用了this来调用它所模拟的方法。派生的Promise函数与内置的Promise几乎一样,除了可以随你需要调用success()与failure()。

由于静态方法被继承了,MyPromise.resolve()方法、MyPromise.reject()方法、MyPromise.race()方法与MyPromise.all()方法在派生的Promise上都可用。后两个方法的行为等同于内置的方法,但前两个方法则有轻微的不同。

MyPromise.resolve()与MyPromise.reject()都会返回MyPromise的一个实例,无视传递进来的值的类型,这是由于这两个方法使用了Symbol.species属性来决定需要返回的Promise的类型。若传递内置Promise给这两个方法,将会被决议或被拒绝,并且会返回一个新的MyPromise,以便绑定完成或拒绝处理函数。例如:

--------------------------------------------

let p1 = new Promise(function(resolve, reject) {

resolve(42);

});

let p2 = MyPromise.resolve(p1);

p2.success(function(value) {

console.log(value);// 42

});

console.log(p2 instanceof MyPromise);// true

--------------------------------------------

此处的p1是一个内置的Promise,被传递给了MyPromise.resolve()方法。作为结果的p2是MyPromise的一个实例,来自p1的决议值被传递给了p2的完成处理函数。

若MyPromise的一个实例被传递给了MyPromise.resolve()或MyPromise.reject()方法,它会在未被决议的情况下就被直接返回。在其他情况下,这两个方法的行为都会等同于Promise.resolve()与Promise.reject()。

await语法(已被纳入ES8)

在我写这本书的时候,针对JS中的异步任务运行,为之引入简单语法的一项工作正在进行。此工作开展在await语法上,极度借鉴了上述以Promise为基础的例子。其基本理念是使用一个被async标记的函数(而非生成器),并在调用另一个函数时使用await而非yield,就像这样:

--------------------------------------------

async function() {

let contents = await readFile("config.json");

doSomethingWith(contents);

console.log("Done");

};

--------------------------------------------

在function之前的async关键字标明了此函数使用异步方式运行。await关键字则表示对于readFile("config.json")的函数调用应返回一个Promise,若返回类型不对,则会将其包装为Promise。await会在Promise被拒绝的情况下抛出错误,否则它将返回该Promise被决议的值。最终结果是你可以将异步代码当作同步代码来书写,而无须为管理基于迭代器的状态机而付出额外开销。

总结

1、Promise被设计用于改善JS中的异步编程,与事件及回调函数对比,在异步操作方面为你提供了更多的控制权与组合性。Promise调度被添加到JS引擎任务队列,以便稍后执行。不过此处有另一个任务队列追踪着Promise的完成与拒绝处理函数,以确保适当的执行。

2、Promise具有三种状态:挂起、已完成、已拒绝。一个Promise起始于挂起态,并在成功时转为完成态,或在失败时转为拒绝态。在这两种情况下,处理函数都能被添加以表明Promise何时被解决。then()方法允许你绑定完成处理函数与拒绝处理函数,而catch()方法则只允许你绑定拒绝处理函数。

3、你能用多种方式将多个Promise串联在一起,并在它们之间传递信息。每个对then()的调用都创建并返回了一个新的Promise在前一个Promise被决议时,新Promise也会被决议。Promise链可被用于触发对一系列异步事件的响应。你还能使用Promise.race()与Promise.all()来监视多个Promise的进程,并进行相应的响应。

4、组合使用生成器与Promise会让异步任务运行得更容易,这是由于Promise提供了异步操作可返回的一个通用接口。这样你就能使用生成器与yield运算符来等待异步响应,并作出适当的应答。

多数新的web API都基于Promise创建,并且你可以期待未来会有更多的效仿之作。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180319G1T51E00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

同媒体快讯

扫码关注云+社区

领取腾讯云代金券