【译】《Understanding ECMAScript6》- 第七章-Promise

时间:2022-04-25
本文章向大家介绍【译】《Understanding ECMAScript6》- 第七章-Promise,主要内容包括目录、异步编程、回调函数、Promise基础、不稳定Promise、稳定Promise、executor错误捕捉、链式Promise、promise链的返回值、promise链的返回promise、多重Promise响应、Promise.race()、异步任务调度、Promise继承、总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

目录

异步操作是JavaScript最强大的功能之一。JavaScript的设计初衷是作为一种面向web的语言,因此具备响应用户行为(比如鼠标和键盘事件)的功能。Node.js使用回调函数代替事件驱动,进一步强化了JavaScript语言的异步编程能力。但是,随着异步编程被广泛使用,开发者们发现这两种异步模式(事件驱动和回调函数)并不能满足所有的产品需求。在这样的背景下,Promise应运而生。

Promise是实现异步编程的一种方式,其他编程语言中与Promise类似的功能称为future或defered。与事件驱动和回调函数类似,Promise的基本思想是实现延迟执行,并且对执行成功或失败有明确的标识,根据标识可以进行链式Promise操作。

在正式介绍Promise之前,我们首先讲解一些与之相关的基础知识。

异步编程

JavaScript引擎的执行序列是单线程的,也就是说同一时间只能执行一段代码。这一点与Java或C++等支持多线程的语言不同。多线程允许多段代码同时执行,这意味着某一状态标识可能被多段代码同时修改,加大了维护难度,并且可能引起安全性问题。

由于JavaScript引擎只能同时执行一段代码,所以必须要对待执行的代码进行跟踪。我们成待执行的代码处于执行序列中。当一段代码准备执行时,就会被加入执行序列。待当前代码执行完毕后,事件循环(event loop)取出队列中的下一段代码并执行。事件循环是JavaScript引擎用于监控代码执行和管理执行序列的进程。执行序列是按照从头到尾的顺序依次执行的。

事件驱动

用户的鼠标点击和键盘输入会触发对应的事件(比如onclick)。随后,事件对应的响应函数被加入执行序列的末尾。这是JavaScript语言实现异步编程最基本的方式:事件响应函数只会在对应的事件触发后执行,并且响应函数运行在适当的执行上下文内。如下:

let button = document.getElementById("my-btn");
button.onclick = function(event) {
    console.log("Clicked");
};

上述代码中的console.log("Clicked")在button被点击之前是不会被执行的。button被点击后,赋值给onclick的匿名函数被加入执行序列的末尾,等待它前面的代码执行完毕后,响应函数被执行。

事件驱动足够解决类似本例的简单需求,但是在处理连续的独立异步回调时显得捉襟见肘,因为你需要非常小心的跟踪事件的target(例如本例中的button)。另外,你必须确保在事件触发之前已经定义了对应的响应函数。比如本例中如果onclick被定义之前点击button,不会产生任何响应。

虽然事件驱动能够实现用户交互响应,但是在应对复杂需求时并不十分灵活。

回调函数

Node.js使用回调函数实现异步编程。回调函数模式与事件驱动相同的地方是,指定代码在对应的条件触发后才会被执行。不同的是,延迟执行的回调函数作为一个参数被传入指定的函数。如下:

readFile("example.txt", function(err, contents) {
    if (err) {
        throw err;
    }

    console.log(contents);
});
console.log("Hi!");

上述代码使用Node.js经典的error-first回调函数。readFile()函数从硬盘中检索第一个参数指定的文件,检索完毕后执行第三个参数指定的回调函数。如果检索失败,回调函数中的err参数是一个包含失败信息的对象;如果检索成功,contents参数是一个包含文件信息的字符串。

本例中回调函数的工作流程如下:运行readFile()函数后,readFile()在从硬盘中检索文件时暂停占用JavaScript线程;随后console.log("Hi!")立即执行;当readFile()检索完毕之后,将第二个参数指定的回调函数加入JavaScript执行序列的末尾等待执行。

回调函数模式优于事件驱动的一点是,回调函数模式可以链式操作。如下:

readFile("example.txt", function(err, contents) {
    if (err) {
        throw err;
    }

    writeFile("example.txt", function(err) {
        if (err) {
            throw err;
        }

        console.log("File was written!");
    });
});

上述代码中,readFile()检索成功后会执行另一个异步回调函数writeFile()。请注意每个回调函数都是error-first风格。readFile()检索完成后,如果没有发生错误,它的回调函数中又调用了writeFile()。随后,writeFile()执行完毕后在JavaScript执行序列末尾新增的回调函数。

尽管回调函数模式可以很好地满足一些应用场景,但是在复杂得场景下,回调函数的多重嵌套使用很容易引起所谓的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机制下的异步函数返回一个Promise,如下:

// readFile promises to complete at some point in the future
let promise = readFile("example.txt");

上述代码中,readFile()函数并非立即开始检索工作,而是延后执行。它返回一个代表异步操作的Promise对象,此后的逻辑可以在Promise对象上进行操作。

生命周期

每个Promise的生命周期都很短。Promise的起始状态为pending状态,代表异步操作未完成。上例中提到的Promise在声明的时候(也就是readFile()返回Promise的时候)便立即进入pending状态。一旦异步操作执行完毕,便认为Promise已经稳定(settled)了,进入以下两种状态之一:

  1. Fulfilled状态-代表Promise的异步操作成功执行完毕;
  2. Rejected状态-代表Promise的异步操作执行失败(原因不唯一)。

并不能预测Promise将进入哪一个状态,但是可以使用then()为每种状态添加对应的响应函数。

then()方法接收两个参数(任何实现then()方法的对象被称为thenable),第一个参数是Promise进入fullfilled状态的响应函数,其参数是异步操作执行成功后的数据信息;第二个参数是Promise进入rejection状态的响应函数,其参数是异步操作执行失败后的错误信息。

两个参数都是可选的,可以根据具体需求使用对应的组合模式,如下:

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

// 监听两种状态fulfilled和rejection
promise.then(function(contents) {
    // fulfilled
    console.log(contents);
}, function(err) {
    // rejection
    console.error(err.message);
});

// 只监听fullfilled状态,不监听rejection
promise.then(function(contents) {
    // fullfilled
    console.log(contents);
});

// 只监听rejection,不监听fullfilled
promise.then(null, function(err) {
    // rejection
    console.error(err.message);
});

如果只监听rejection状态,还可以使用catch()方法。如下:

promise.catch(function(err) {
    // rejection
    console.error(err.message);
});

//功能等价于:
promise.then(null, function(err) {
    // rejection
    console.error(err.message);
});

使用then()和catch()可以对Promise的不同状态做出合理的响应。Promise相对于事件驱动和回调函数的优势在于,不论异步操作执行成功或者失败,Promise都可以进行清晰合理的响应,而事件驱动模式下,异步操作失败将不会触发响应函数;回调函数模式下,必须时刻谨记error-first原则。

如果Promise没有rejection监听,那么所有的失败信息会被静默处理。所以,rejection监听是非常必要的,即便只是输入失败信息,以便调试。

Promise还有另外一种特性,即Promise稳定之后,仍然可以添加fullfilled和rejection的响应函数并且被执行。也就是说,可以在任何时刻添加响应监听,并且会被执行。如下:

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

// 原始监听响应
promise.then(function(contents) {
    console.log(contents);
    // 新增监听响应
    promise.then(function(contents) {
        console.log(contents);
    });
});

上述代码中,在Promise的fulfilled响应函数内部给同一Promise又新增了fulfilled响应。此时Promise已经是fulfilled状态,所以新增的fulfilled响应函数被立即加入执行序列。rejection响应同理。

then()和catch()方法被调用时会生成一段待执行的逻辑。Promise响应的执行序列是独立于JavaScript主序列的。对于普通开发者而言,并不需要对Promise独立执行序列有深入了解,只要熟知JavaScript执行序列的工作原理即可。

不稳定Promise

通过Promise构造函数创建的Promise成为不稳定Promise。Promise构造函数接受一个参数,此参数是一个函数对象(也叫做executor),内部是Promise待执行的代码。executor接收两个函数对象作为参数:resolve()和reject()。resolve()函数在executor执行成功后被调用,reject()函数在executor执行失败后被调用。如下:

// Node.js example

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);
                return;
            }

            // 检索成功后的回调
            resolve(contents);

        });
    });
}

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

// listen for both fulfillment and rejection
promise.then(function(contents) {
    // fulfillment
    console.log(contents);
}, function(err) {
    // rejection
    console.error(err.message);
});

上述代码是Node.js使用Promise实现前文提到的readFile()函数。fs.readFile()被封装在一个Promise内。executor内部将错误信息作为参数传递给reject(),将成功检索的文件信息传递给resolve()。

需要注意的是,当readFile()被调用时,executor并非立即执行,而是被加入执行序列中延后执行。这跟setTimeout()或者setInterval()的机制类似,加入执行队列的逻辑延迟执行。但是使用setTimeout()和setInterval()时必须指定延迟执行的时间:

// add this function to the job queue after 500ms have passed
setTimeout(function() {
    console.log("Timeout");
}, 500)

console.log("Hi!");

上述代码使用setTimeout延迟500毫秒执行内部代码。最终输出如下:

Hi!
Timeout

输出结果表明setTImeout()内部的代码在console.log("Hi!")后执行。Promise的工作模式与之类似。

Promise的exectuor将内部逻辑代码立即加入执行序列,等待它之前的逻辑执行完毕后执行。如下:

let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

console.log("Hi!");

输出结果如下:

Hi!
Promise

executor总是等待执行序列内的其他逻辑执行完毕后才会被执行。同理,传递给then()和catch()的函数也是被立即加入执行序列,但是它们需要等待executor执行完毕后才会被执行。如下:

let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

promise.then(function() {
    console.log("Resolved.");
});

console.log("Hi!");

输出结果如下:

Hi!
Promise
Resolved

另外,fulfillment和rejection的响应函数总是在executor执行完毕后被加入到执行序列的末尾。

稳定Promise

使用Promise构造函数是创建不稳定Promise的最佳方式,可以充分发挥executor的动态性。但是,如果只是想创建一个代表某个已知值的promise,并不需要一系列繁琐的执行调度。Promise提供了两个方法可以根据具体value快速创建promise。

Promise.resolve()方法接受一个参数,返回一个fulfilled状态的Promise。并未发生任何的执行调度,如果要获取这个Promise代表的value,你需要新增一个或多个fulfillment响应函数。如下:

let promise = Promise.resolve(42);

promise.then(function(value) {
    console.log(value);         // 42
});

上述代码首先创建了一个fulfilled状态的Promise,随后then()方法创建的fulfillment响应函数接受参数value的值为12。本例中的Promise永远不会触发rejection响应。

创建rejection状态Promise需要使用方法Promise.reject()。与Promise.resolve()类似,使用Promise.reject()创建的Promise的状态为rejection,任何rejection响应函数都会被触发:

let promise = Promise.reject(42);

promise.catch(function(value) {
    console.log(value);         // 42
});

使用Promise.resolve()或Promise.reject()创建的Promise是无修正的。

Promise.resolve()和Promise.reject()均可接受一个non-promise thenable对象作为参数。non-promise thenable对象是指实现了then()方法,并且then()方法接受两个参数:resolve和reject。如下:

var thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};

上述代码定义的thenable对象处理then()方法以外,没有任何与promise相关的特性。使用Promise.resolve()可以将它转化为了一个fulfilled状态的promise。如下:

var thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};

var p1 = Promise.resolve(thenable);
p1.then(function(value) {
    console.log(value);     // 42
});

上述代码中,Promise.resolve()调用thenable.then()以确定promise的状态。代码执行了resolve(42),所以thenable的promise状态为fulfilled。随后新创建的fulfilled状态promise沿袭thenable的参数值42,所以p1的fulfillment响应函数的参数value值为42。Promise.reject()的工作模式与之类似:

var thenable = {
    then: function(resolve, reject) {
        reject(42);
    }
};

var p1 = Promise.reject(thenable);
p1.catch(function(value) {
    console.log(value);     // 42
});

利用Promise.resolve()和Promise.reject()的以上工作模式可以更方便的处理thenable对象,而不必提前知道某个对象是否为promise对象。

executor错误捕捉

如果executor内部抛出错误,将会触发promise的rejection响应函数。如下:

let promise = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});

promise.catch(function(error) {
    console.log(error.message);     // "Explosion!"
});

每个executor内部都有一个隐含的try-catch机制,能够捕捉错误并传递给rejection响应函数。也就是说,上述代码等价于以下形式:

// equivalent of previous example
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!"
});

executor内部错误捕捉机制可以简化错误的捕捉和处理。

链式Promise

行文至此,可能部分读者认为promise不仅仅是功能强化版的响应函数和setTimeout(),但promise的功能并不仅限于此。下面我们将讨论如何使用链式promise来实现复杂的异步操作。

每次执行then()或者catch()都会创建并返回一个新的promise。新的promise只会在前一个promise进入fulfilled或者rejected状态后被执行。如下:

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()后返回新的promis。新promise的then()方法定义的fulfillment响应函数在第一个promise执行完毕后被执行。如果不使用链式操作,本例等价于以下形式:

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

// same as

let p2 = p1.then(function(value) {
    console.log(value);
})

p2.then(function() {
    console.log("Finished");
});

同理,p2.then()也会返回一个新promise。

错误捕捉

通过链式操作promise可以捕捉前一个promise的fulfillment和rejection响应函数抛出的错误。如下:

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的fulfillment响应函数抛出错误。随后链式调用catch()方法,也就是使用第二个promise的rejection响应函数捕捉错误。同理,rejection响应函数的错误也可以使用相同的方法捕捉:

let p1 = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});

p1.catch(function(error) {
    console.log(error.message);     // "Explosion!"
    throw new Error("Boom!");
}).catch(function(error) {
    console.log(error.message);     // "Boom!"
});

上述代码中,executor首先抛出错误触发了p1的rejection响应函数。随后p1的rejection响应函数又抛出错误并被第二个promise的rejection响应捕捉到。通过这种机制,promise的链式调用可以捕捉到链条前面的错误并作出相应处理。

笔者建议链式操作promise时,在链条末尾添加rejection响应函数,以确保链条产生的错误被正确处理。

promise链的返回值

链式promise的另一个重要功能是可以从一个promise传递数据至下一个promise。前文提到executor内resolve()的数据可以传递给此promise的fulfillment响应函数。你可以通过fulfillment响应函数的返回值继续传递此数据。如下:

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的fulfillment响应函数返回一个值(value+1)。因为executor传递来的value值为42,所以p1返回的值为43。随后,这个值被传递给第二个promise的fulfillment响应函数并被打印。

同理,rejection响应函数也可以通过返回值的方式传递数据:

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"
});

上述代码中的executor内部调用reject()并传参42。随后这个值被传递给此promise的rejection响应函数,并返回value+1。虽然返回值是由rejection响应函数提供,但是仍然可以在下一个promise的fulfillment响应函数中使用。利用这种机制,可以在必要的情况下使用某个执行失败的promise重新唤醒整个promise链。

与fulfillment响应函数不同的是,如果rejection响应函数没有返回值,那么promise链后面的所有方法均不会被调用。如下:

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

p1.catch(function(value) {
    console.log(value);         // "42"
}).then(function(value) {
    console.log(value);         // Never called
});

上述代码中的第二个console.log(value)不会被执行,因为上游的rejection响应函数没有返回值。换句话说,这条promise链已经断了。

promise链的返回promise

fulfillment和rejection响应函数可以通过返回原始类型在promise之间传递数据,但是如果想传递对象类型怎么办?如果这个对象是一个promise对象呢?这种场景下,便需要一些额外的工作进行处理。请考虑下面这个例子:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var 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的executor执行resolve(42),p1的fulfillment响应函数返回一个fulfilled状态的promise对象p2。随后p2的fulfillment响应函数被调用。如果p2是rejected状态,就会触发rejection响应(如果存在)而不是fulfillment响应函数。

需要注意的是,第二个fulfillment响应函数的归属对象并不是p2,而是第三个promise。前例等价于以下形式:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var p2 = new Promise(function(resolve, reject) {
    resolve(43);
});

var p3 = p1.then(function(value) {
    console.log(value);     // 42
    return p2;
});

p3.then(function(value) {
    console.log(value);     // 43
});

上述代码清晰地表明第二个fulfillment响应函数的归属对象是p3而不是p2。这个细微的差别非常重要,也解释了为何rejected状态p2不会触发第二个fulfillment响应函数。请看下面这个例子:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var p2 = new Promise(function(resolve, reject) {
    reject(43);
});

p1.then(function(value) {
    console.log(value);     // 42
    return p2;
}).then(function(value) {
    console.log(value);     // never called
});

上述代码中,由于p2是rejected状态,所以第二个fulfillment响应函数不会被触发。如果存在rejection响应,就会被正常触发。如下:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var 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
});

上述代码中的rejection响应函数被触发,并且接收p2传递来的数据43。

promise的executor被执行时,通过fulfillment和rejection响应函数返回的thenable对象并不会发生改变。executor按照创建顺序依次执行。thenable对象返回值可以支持开发者定义额外的响应,比如可以在fulfillment响应里异步创建一个新的promise并添加fulfillment响应。如下:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

p1.then(function(value) {
    console.log(value);     // 42

    // create a new promise
    var p2 = new Promise(function(resolve, reject) {
        resolve(43);
    });

    return p2
}).then(function(value) {
    console.log(value);     // 43
});

上述代码中,p1的fulfillment响应函数内部创建一个新的promise。在p2进入fulfilled状态之前,第二个fulfillment响应函数不会被触发。

多重Promise响应

到目前为止,上文所举的例子都是每次只操作一个promise。然而在某些应用场景下,需要同时监控多个promise的运行转态。ES6提供了两个方法满足此需求:Promise.all()和Promise.race()。

Promise.all()

Promise.all()接受一个参数,参数为待监控所有promise组成的数组,并且返回一个promise对象。返回的promise对象只有在参数数组中所有promise执行后才会执行,并且待所有promise进入fulfilled状态后进入fulfilled状态。如下:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var p2 = new Promise(function(resolve, reject) {
    resolve(43);
});

var p3 = new Promise(function(resolve, reject) {
    resolve(44);
});

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

p4.then(function(value) {
    console.log(value);     // [42, 43, 44]
});

上述代码中的p1、p2、p3的executor内部的resolve函数都传递一个数值。调用Promise.all()后创建了一个新promise对象p4,待p1-p3都进入fulfilled状态后,p4也进入fulfilled状态。传递给p4 fulfillment响应函数的参数是一个包含p1-p3所有resolve数值的数组:42、43、44。

一旦任意一个监控的promise进入rejected状态,Promise.all()创建的promise(p4)便会立即进入rejected状态,不会等待全部promise执行完毕。如下:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var p2 = new Promise(function(resolve, reject) {
    reject(43);
});

var p3 = new Promise(function(resolve, reject) {
    resolve(44);
});

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

p4.catch(function(value) {
    console.log(value);     // 43
});

上述代码中,p2的reject函数传递数值43。p4的rejection响应函数立即被调用,没有等待p1和p3执行完毕(p1和p3仍然会执行,但是p4不会等待它们执行完毕)。p4的rejection响应函数接收数据43表明rejected状态是由p2引起。

Promise.race()

与Promise.all()相同,Promise.race()方法也接收一个数组参数,数组元素为待监控的promise,并且返回一个promise对象。与Promise.all()不同的是,一旦第一个promise执行完毕,Promise.race()返回的promise立即进入相应的状态。也就是说,如果参数数组的任何promise进入fulfilled状态,Promise.race()返回的promise立即进入fulfilled状态,不会等待剩余promise的执行结果。如下:

var p1 = Promise.resolve(42);

var p2 = new Promise(function(resolve, reject) {
    resolve(43);
});

var p3 = new Promise(function(resolve, reject) {
    resolve(44);
});

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

p4.then(function(value) {
    console.log(value);     // 42
});

上述代码中,p1是一个创建即fulfilled状态的promise,p2和p3需要执行调度才会进入fulfilled状态。p4的fulfillment响应函数立即被调用并且被传递数值42,而没有等待其余promise执行完毕。Promise.race()参数数组内的promise就像在进行一场竞速,看谁先进入稳定状态。一旦某个promise率先进入fulfilled状态,Promise.race()返回的promise便立即进入fulfilled状态;一旦某个promise率先进入rejected状态,Promise.race()返回的promise便立即进入rejected状态:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var p2 = Promise.reject(43);

var p3 = new Promise(function(resolve, reject) {
    resolve(44);
});

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

p4.catch(function(value) {
    console.log(value);     // 43
});

上述代码中,p4的状态是rejected,因为p2比p1、p3率先进入稳定状态并且进入rejected状态。所以,即使p1和p3都是fulfilled状态,p4仍然与p2的状态保持一致。

异步任务调度

第八章介绍的生成器(generator)可以便于组织异步任务调度:

var fs = require("fs");

var task;

function readConfigFile() {
    fs.readFile("config.json", function(err, contents) {
        if (err) {
            task.throw(err);
        } else {
            task.next(contents);
        }
    });
}

function *init() {
    var contents = yield readConfigFile();
    doSomethingWith(contents);
    console.log("Done");
}

task = init();
task.next();

然而这种工作模式下需要跟踪监控task,并且在每个异步函数中调用适当的方法函数(比如本例中的readConfigFile())。使用promise,只需要确保每个异步操作都返回一个promise对象,便可以很大程度上简化本例的异步代码:

var fs = require("fs");

function run(taskDef) {

    // create the iterator
    var task = taskDef();

    // start the task
    task.next();

    // recursive function to iterate through
    (function step() {

        var result = task.next(),
            promise;

        // if there's more to do
        if (!result.done) {

            // resolve to a promise to make it easy
            promise = Promise.resolve(result.value);
            promise.then(function(value) {
                task.next(value);
                step();
            }).catch(function(error) {
                task.throw(error);
                step();
            });
        }
    }());
}

function readConfigFile() {
    return new Promise(resolve, reject) {
        fs.readFile("config.json", function(err, contents) {
            if (err) {
                reject(err);
            } else {
                resolve(contents);
            }
        });
    });
}

run(function *() {
    var contents = yield readConfigFile();
    doSomethingWith(contents);
    console.log("Done");
});

上述代码中的run()函数用来执行一个生成器并生产一个迭代器(iterator),使用task.next()启动任务,随后递归调用step()直到迭代器执行完毕。在step()函数内部,task.next()返回迭代器的结果。如果迭代器未执行完毕,result.done的值为false。此时result.value是一个promise对象,但是Promise.resolve()只有在value值不是promise对象时被正确执行。随后,fulfillment响应函数减速被传入的promise对象并在递归step()之前将这个promise对象传递给迭代器。同理,rejection响应函数在递归step()之前将包含错误信息的error对象传递给迭代器。

run()函数的工作模式可以在不暴露promise或callback的前提下合理调度生成器的异步任务。

Promise继承

与其他内置类型一样,promise可以作为基础类创建派生类,开发者可以创建自定义的promise变种。比如,如果想使用success()和failure()取代原生promise的then()和catch()方法,可以写成如下形式:

class MyPromise extends Promise {

    // use default constructor

    success(resolve, reject) {
        return this.then(resolve, reject);
    }

    failure(reject) {
        return this.catch(reject);
    }

}

var promise = new MyPromise(function(resolve, reject) {
    resolve(42);
});

promise.success(function(value) {
    console.log(value);             // 42
}).failure(function(value) {
    console.log(value);
});

上述代码中,MyPromise是Promise的派生类型,它有两个自定义方法:success()和failure()。这两个方法内部使用this调用各自映射的Promise方法。创建MyPromise实例的语法和原生Promise相同,区别在于你可以使用success()和failure()代替原生Promise的then()和catch()。

根据class的继承规则,static方法也会被继承,所以MyPromise.resolve(), MyPromise.reject(), MyPromise.race()和MyPromise.all()这些方法仍然可以使用。后两个方法的功能表现与原生Promise对应的方法相同,但是前两个方法存在细微的差别。

无论是否有传参,MyPromise.resolve()和MyPromise.reject()都会返回一个MyPromise的实例对象。所以,如果一个原生Promise对象被传递给这两个方法,它在被resolve或reject之后返回一个MyPromise实例。如下:

var p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

var 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的resolve值被传递给p2的fulfillment响应函数。

如果一个MyPromise实例被传递给MyPromise.resolve()或MyPromise.reject(),将不会被resolve或reject,而是被直接返回。除此之外,这个两个方法的其他功能表现与原生Promise相同。

总结

Promise被提出的初衷是增强JavaScript语言异步编程的能力。鉴于事件驱动和回调函数模式都有各自的局限性,通过promise排列组合异步操作可以增强可控性和可调度性。promise的工作原理是通过执行调度在JavaScript引擎的执行序列中加入延迟执行的任务,并通过另外一条执行序列跟踪promise的fulfillment和rejection状态以便执行合理响应。

Promise有三种状态:pending、fulfilled和rejected。pending是promise的起始状态,随后进入fulfilled状态(执行成功)或rejected状态(执行失败)。promise稳定之后便可以添加每种状态的响应函数。then()方法可以用来添加fulfillment和rejection响应,catch()方法只能添加rejection响应。

promise可以组成promise链,并且可以在promise链中的promise之间传递数据。每次调用then()方法都会创建被返回一个新的promise对象,如果前一个promise被resolve,那么返回的promise对象也被resolve。promise链可以用来触发一系列异步事件的响应。此外,Promise.race()和Promise.all() 可以用来监控多个promise的执行状态,并作出合理响应。

使用生成器和promise可以更方便地调度异步任务。promise提供一个公用接口用来返回异步操作的结果。随后便可以使用生成器和yeild操作等待并处理异步响应。

越来越多的web API建立在promise的基础上,我们可以期待未来promise有更广泛的应用场景。