ECMAScript 6 promises(下):谈谈 API(二)

ε祈祈猫儿з 提交于 2019-11-29 11:47:10

原文地址: http://www.2ality.com/2014/10/es6-promises-api.html

原文作者:Dr. Axel Rauschmayer

译者:倪颖峰

原博客已经标明:本博客文档已经过时,可进一步阅读“Exploring ES6”中的 “Promises for asynchronous programming”。仔细对比了下,两者的确存在一些差异。本文是在原来的译文基础上修订的。

    博文下半部分为纯干活内容,包括介绍的 ES6 中 Promise API,以及其简单的实现方式与思想,以及一些拓展内容。

9. 速查表:ES6的 Promise API

    这里简单的给出一下 ES6 Promise 的API 简述,详细描述请看规范http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects

9.1. 术语

  Promise 的 API 就是关于异步获取结果。一个Promise 对象(简称Promise)是一个独立的对象,会通过该对象传递结果。

    状态:

        一个Promise处于以下三个(互斥)状态中的某一个状态

            1. 结果未完成时,Promise 状态为 pending。

            2. 得到结果时,Promise 状态为 fulfilled。

            3. 当错误发生时,Promise 状态为 rejected。

        一个Promise 一旦被设置(settled)则表示“事情完成”(包括 fulfilled 或者 rejected)。

        一个Promise只能设置一次,之后便不能再改变。

    状态变化反应:

        Promise reactions 就是用 Promise 方法 then( ) 来注册的回调函数,用来通知fulfillment 或 rejection。

        一个 then对象(thenable)即为一个含有Promise 风格的then( )方法的对象。当一个API只对settlement的通知感兴趣,那么它只需要then对象。

    状态改变:
        
有两个操作会改变Promise 状态。一旦你调用了其中的一个,那么进一步调用将会无效。

        。Rejecting一个Promise 表示这个promise状态变为rejected。

        。Resolving一个Promise 有两种情况,取决于以什么值来解析:

            1. 以一个普通值(非 then对象)来解析,则fulfillment给 Promise。

            2. 以一个 then对象T来解析Promise P ,意味着P 不再可以被解析,而要随着T的状态走,即T fulfillment或rejection的值。一旦T被设置时P的reactions就被触发(或者如果T已经被设置则立即触发)。

9.2. Promise构造函数

    Promise 的构造函数这样调用:

let p = new Promise(function(resolve, reject) {...} );

    构造函数的回调函数被称为executor,executor使用参数来resolve或者reject new出来的Promise p:

        。resolve(x),以 x 来解析 p:

            如果 x 是 then对象,那么它的settlement会转给 p(包括通过 then() 注册的reactions)。

            否者,p 就被赋值为 x。

        。reject(x),以 e 值来拒绝 p(e通常为一个 Error实例)。

9.3. 静态方法

    所有的Promise 的静态方法都支持子类化(subclassing):

    。通过species pattern来创建新的Promise实例

        .  缺省使用receiver (this)作为构造函数。

        . 缺省方式可以被子类中property [Symbol.species]覆盖。

    。还可以通过species pattern(不是通过Promise或this)访问其他静态方法。

9.3.1 创建 Promises

        下面两个静态方法创建接收器的新实例。

        1. Promise.resolve(x): 将任意值转换为Promise对象。

            。如果 x 是 then对象,它将转换为一个Promise(接收器的一个实例),这里没有用species pattern。

            。如果 x 已经是接收器的一个实例,则不改变返回。

            。否则,将返回接收器的一个新实例,并且以 x 来填充。

        2. Promise.reject(reason):创建接收器的一个新实例(通过species pattern来配置),并且以 reason 值来处理拒绝状态。

9.3.2 组合 promises

        直观来说,静态方法 Promise.all() 和 Promise.race() 将可遍历的Promises 组合为一个Promise。即:

            1. 它们接受一个可遍历的对象iterableiterable的元素通过 this.resolve()被转换为Promises。

            2. 它们返回一个新的Promise。这个Promise 是接收器的一个新实例(通过species pattern来配置)

    下面是这两个方法

        1. Promise.all(iterable):返回一个Promise

            。如果iterable每一个元素的状态均为 fulfilled,那么将其状态设置为 fulfilled。

                fullfillment值:由每一个填充值组成的数组。            

             。当元素中有任何一个状态为rejected时,那么将其状态设置为 rejected。

                rejection值:第一个rejection值。

        2. Promise.race(iterable):iterable中第一个被设置的元素就是返回的Promise。

9.4. Promise.prototype方法

9.4.1 Promise.prototype.then(onFulfilled, onRejected)

        。回调函数 onFulfilled 和 onRejected 被称为 reactions。

        。如果Promise状态已经被设为 fulfilled,或者一旦变成 fulfilled,那么回调函数 onFulfilled 就会被立即执行。类似地,onRejected是在状态变为rejected时触发。

        。then()方法返回一个新的Promise Q(通过接收器构造函数的species创建):

            . 如果有一个reaction返回结果,Q 便使用其来resolve。

            . 如果有一个reaction抛出异常,Q 便使用其来reject。

        。被忽略的reactions:

            . 如果 onFulfilled被忽略,那么接收器的fulfillment值就会被转发到 then() 的结果中。

            . 如果 onRejected被忽略,那么接收器的rejection值就会被转发到 then() 的结果中。

    被忽略的reactions的默认值可以以下方式来实现:

function defaultOnFulfilled(x){
    return x;
}
function defaultOnRejected(e){
    throw e;
}

9.4.2 Promise.prototype.catch(onRejected)

        p.catch(onRejected)等同于p.then(null, onRejected)。

10. promises 的优势与劣势

10.1. 优势

    统一的异步 APIs

        Promises 的一个重要的优点就是被越来越多的浏览器异步 API 使用,统一了当前多样化和不兼容的各种模式和约定。看一下即将出现的两种基于Promise 的API。

        基于 Promise 的 fetch API 替代 XMLHttpRequest:

fetch(url)
.then(request => request.text())
.then(str => ...)

        fetch() 会对实际的请求返回一个 Promise,text() 会返回一个内容为字符串的Promise

        ECMAScript 6 API 编程式导入模块也是基于 Promise:

System.import('some_modle.js')
.then(some_module => {
    ...
})

    Promises VS events

        对比 events,Promises 更适合做一次性结果的监听。不管是在结果计算之前还是之后注册,都不会影响监听到结果。这是 Promises根本上的优势。另一方面,你不能用它来处理一些反复性的事件。链式是 Promise 的另一个优势,但是每次只能添加一个。

    Promises VS callbacks

        对比回调函数,Promises 具有更简洁的函数(方法)参数。在回调函数中,参数包含输入参数和输出参数:

fs.readFile(name, opts?, (err, string | Buffer) => void)

        而 Promise 中所有的参数均只是输入参数:

readFilePromisified(name, opts?) : Promise<string | Buffer>

       此外,Promises 的优势还包括更好的错误处理机制(集成了异常处理,组合更容易(因为你可以重用一些同步工具,比如 Array.prototype.map())。

10.2. 劣势

    Promises 对于单一的异步结果能处理的很好。但是它不擅长于:

        1. 反复性事件如果你对此感兴趣,可以看一下 reactive programminghttp://reactive-extensions.github.io/RxJS/,关于链式处理普通事件处理的巧妙方式。

        2. 数据流支持数据流的标准尚在开发中。

    ECMAScript 6 的 Promises 缺少以下两个某些时候很有用的功能点:

        1. 你无法取消。

        2. 你无法确定他们发生多长时间了(例如在客户端用户界面展现进度条)。

    Q Promises 库支持后者,而且Promises/A+也将计划添加这两个功能点 。

11. Promises与生成器

    下面使用控制流库co(https://github.com/tj/co)异步获取两个JSON文件。注意在行A,一个执行块在等待直到得到Promise.all()的结果。这使得代码看起来类似于同步而实际上在执行异步操作。

co(function* () {
    try {
        let [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        let croftJson = JSON.parse(croftStr);
        let bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

这段代码的细节在生成器章节中解释。

12. 调试 promises

    浏览器中开始慢慢出现可调试Promise的工具。可快速看下最新Google Chrome中的表现,下面是一段代码演示两个常见的Promise问题:

<body>
    <script>
        // Unhandled rejection
        Promise.reject(new Error())
        .then(function (x) { return 'a'})
        .then(function (x) { return 'b'})

        // Unsettled Promise
        new Promise(function () {});
    </script>
</body>

   第一个问题是没有处理rejection,第二个问题是Promise未被设置。Chrome开发工具可协助检查这两个问题:

 在console中可以看到未处理的rejection:

 同时在源码检查器中也会高亮显示:

  

   而且,还有针对Promises的特殊检查器,记录web页面创建的所有Promises。它会记录:

  • 哪些Promises状态是fulfilled (第一列的绿球), rejected (红球)和pending (灰球)

  • Promises是如何链在一起的


13. promises 内部窥秘

    本章节,我们将从不同的角度来看一下Promises:不再学习如何使用 promise API,我们将了解一下它的简单实现。这个视角将帮助我们更好地理解Promises。

    下面这个Promise 的实现被称为 DemoPromise 和 在 GitHub 上可用的实例https://github.com/rauschma/demo_promise)。为了更容易理解,将不会完全匹配 API。但是它将足够使你了解在实现Promises 过程会中面对的各种难题。

    DemoPromises 是一个含有三个原型方法的类:

        1. DemoPromises.prototype.resolve(value)

        2. DemoPromises.prototype.reject(reason)

        3. DemoPromises.prototype.then(onFulfilled, onRejected)

    在此,resolve 和 reject 为方法(而不是传递给构造函数作为参数的回调函数)。

13.1 一个独立的Promise

    首先我们需要实现一个最少功能的独立的Promise :

        1. 你可以创建一个Promise。

        2. 你可以resolve或者reject一个Promise 并且只能处理它一次。

        3. 你可以通过 then() 来注册 reactions(回调函数)。该方法不支持链式(该处应该是支持链式),就是说,其不返回任何东西。不管Promise 是否已经被处理该方法都可以被调用。

    以下是第一个实现:

let dp = new DemoPromise();
dp.resolve('abc');
dp.then(function(value){
    console.log(value); // abc
});

    下面图说明了我们第一个 DemoPromise 是如何实现的:


13.1.1 DemoPromise.prototype.then()

    让我们首先研究下 then()。它必须处理两个情况:

        1. 如果Promise 状态还是pending,那么就排队onFulfilled 与onRejected的调用,直到Promise被处理完毕(settled)之后调用。

        2. 如果Promise状态变为fulfilled或者rejected,则onFulfilled 或者 onRejected 将被立即调用。

then(onFulfilled, onRejected) {
    let self = this;
    let fulfilledTask = function () {
        onFulfilled(self.promiseResult);
    };
    let rejectedTask = function () {
        onRejected(self.promiseResult);
    };
    switch (this.promiseState) {
        case 'pending':
            this.fulfillReactions.push(fulfilledTask);
            this.rejectReactions.push(rejectedTask);
            break;
        case 'fulfilled':
            addToTaskQueue(fulfilledTask);
            break;
        case 'rejected':
            addToTaskQueue(rejectedTask);
            break;
    }
}

function addToTaskQueue(task) {
    setTimeout(task, 0);
}

13.1.2 DemoPromise.prototype.resolve()

     resolve() 工作原理如下:如果Promise 已经被处理完毕(settled),将不做任何事情(确保一个Promise 只被处理一次)。否则,Promise 的状态转变为 fulfilled 并且结果缓存到 this.promiseResult。队列中所有的fulfillment reactions 将被立即执行。

resolve(value) {
    if (this.promiseState !== 'pending') return;
    
    this.promiseState = 'fulfilled';
    this.promiseResult = value;
    this._clearAndEnqueueReactions(this.fulfillReactions);
    return this; // enable chaining
}

_clearAndEnqueueReactions(reactions) {
    this.fulfillReactions = undefined;
    this.rejectReactions = undefined;
    reactions.map(addToTaskQueue);
}

    reject() 与 resolve() 类似。

13.2 链式(注意,这部分已经为下一步扁平化做好了基础)

    接下来,我们将实现链式:

        1. then() 返回的 Promise 实际上是 onFulfilled 或者 onRejected 被执行后所返回的。

        2. 如果 onFulfilled 或者 onRejectecd 缺失,那么不管它们所传递的是什么,Promise 通过 then() 返回出来。


        

    显然,只需要调整下 then():

then(onFulfilled, onRejected) {
    let returnValue = new Promise(); // (A)
    let self = this;

    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            let r = onFulfilled(self.promiseResult);
            returnValue.resolve(r); // (B)
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult); // (C)
        };
    }

    let rejectedTask;
    if (typeof onRejected === 'function') {
        rejectedTask = function () {
            let r = onRejected(self.promiseResult);
            returnValue.resolve(r); // (D)
        };
    } else {
        rejectedTask = function () {
            // `onRejected` has not been provided
            // => we must pass on the rejection
            returnValue.reject(self.promiseResult); // (E)
        };
    }
    ···
    return returnValue; // (F)
}

    then() 创建并返回一个新的Promise(在 A 处和 F 处)。此外,fulfilledTask 和 rejectedTask 以不同方式设置:在处理结束时。

        1. onFulfilled 的结果被用于resolve returnValue(在 B 处)。如果 onFulfilled缺失,我们使用fulfillment值来resolve  returnValue(在 C 处)。

        2. onRejected 的结果被用于resolve(并非reject) returnValue(在 D 处)。如果onRejected 缺失,我们就传递rejection值给 returnValue(在 E 处)。

13.3 扁平化

    扁平化(Flattening)思想主要是使链式方式更为便捷:一般,将一个 reaction 返回的值传递到下一个 then()。如果我们返回一个Promise,不包裹的形式是最好的,像下面的例子:

asyncFunc1()
.then(function (value1){
    return asyncFunc2(); // A
})
.then(function (value2){
    // value2 为 asyncFunc2() Promise的fulfillment值。
    console.log(value2);
});

    我们在 A 处返回一个Promise,就没有必要在当前方法中嵌套 then() 的调用,我们可以对方法的结果进行 then() 的调用。因此,不嵌套 then(),就保持扁平化。

    下面的实现使 resolve() 方法扁平化

        1. 以一个Promise Q 来resolve 一个Promise P, 意味着 Q 的处理结果(settlement)要要转发给 P 的 reactions。

        2. P变成被锁定在 Q 中:它不能被resolved(以及rejected)。并且它的状态和结果和 Q 的保持同步。

    如果我们使Q成为then对象(而非一个Promise),便能以更通用的形式实现扁平化。


    

    为实现锁定,我们需要使用一个新的布尔值标签 this.alreadyResolved。一旦值为 true,this 将被锁住,不能再被resolved。注意this状态仍然可以是pending,因为它的状态是和锁住它的Promise 是一致的。

resolve(value) {
    if (this.alreadyResolved) return;
    
    this.alreadyResolved = true;
    this._doResolve(value);
    return this; // enable chaining
}

    真正实现解析是在私有方法 _doResolve() 中实现:

_doResolve(value) {
    let self = this;
    // Is `value` a thenable?
    if (typeof value === 'object' && value !== null && 'then' in value) {
        // Forward fulfillments and rejections from `value` to `this`.
        // Added as a task (vs. done immediately) to preserve async semantics.
        addToTaskQueue(function () { // (A)
            value.then(
                function onFulfilled(result) {
                    self._doResolve(result);
                },
                function onRejected(error) {
                    self._doReject(error);
                });
        });
    } else {
        this.promiseState = 'fulfilled';
        this.promiseResult = value;
        this._clearAndEnqueueReactions(this.fulfillReactions);
    }
}

    扁平化处理在 A 处进行:如果 value 为fulfilled状态,那么当前 self 为fulfilled状态,如果 value 为rejected状态,我们就希望 self 为rejected状态。转发是通过私有方法 _doResolve 和 _doReject 来做的,通过 alreadyResolved 来绕开阻挡。

13.4. 更详细的 Promise 状态

   由于链式形式,Promises 的状态变得更为复杂。


    
    如果你只是使用Promises,你只需要以简单的视角来看待,忽略锁定就好。最主要的状态相关概念是“settledness”: 一个Promise 状态如果变为fulfilled或者rejected的时候被设置完毕(is settled)。在一个Promise 被设置之后,将不再变化(状态和fullfillment或者rejection值都不再变)。

    如果你希望实现Promsie,那么与"resolving"相关的内容,现在就更难理解:

        1. 直观的说,‘resolved’(已处理)意味着不能再次被(直接)处理。当一个Promise 被设置或者被锁定时,那么就被称为resolved。引用下规范:一个未被处理的Promise总是处于pending 状态。一个已处理的Promise 可能是pending,fulfilled或者rejected状态。

        2. 'resolving'(处理中)不一定会变为settling(被设置):你可能用状态总为pending的另一个Promise来resolve一个Promise。

        3. 'resolving'处理中现在包含rejecting(拒绝):你可以用状态为rejected的另一个Promise来处理一个Promise,这样就可以reject这个Promise。

13.5. 异常

    作为最后一个功能点,我们希望在 user code 中将异常处理为rejections。目前来说,user code 只代表 then 中的两个回调函数参数。

    

    下面代码片段展现了我们将 onFulfilled 内部的异常变为rejections - 通过 A 处调用时的 try-catch 封装。

then(onFulfilled, onRejected) {
    ···
    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            try {
                let r = onFulfilled(self.promiseResult); // (A)
                returnValue.resolve(r);
            } catch (e) {
                returnValue.reject(e);
            }
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult);
        };
    }
    ···
}

13.6. 揭示构造函数模式

    如果我们希望将 DemoPromise 转变为切实可用的Promise,还需要实现revealing constructor pattern(揭示构造函数模式):ES6 Promises 不是通过方法来resolved或者rejected,而是通过监听在构造函数的回调函数参数executor 上的函数实现的。

    
     如果executor抛出一个异常,那么该Promise状态一定为rejected。

14. 两个常用的Promise 附加方法

    本段介绍一下两个在 ES6 中添加到Promsie 的方法。大部分Promise 库都支持他们。

14.1. done()

    当你将数个 Promise 方法链式调用时,你可能会无心之中忽略错误。看实例:

function doSomeThing(){
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2); // A
}

    如果在 A 处的 then() 生成拒绝,那么将不会被处理。 Promise 库 Q 提供了一个方法 done(),放置在链式最后一个方法调用的后面。其或者替换最后一个 then()(有一个到两个参数):

function doSomeThing(){
    asyncFunc()
    .then(f1)
    .catch(r1)
    .done(f2);
}

    或者仅仅是插到最后一个 then() 之后(无参数):

function doSomeThing(){
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2)
    .done();
}

    引用 Q 的文档:

        done 与 then 使用的黄金规则:当返回一个Promise,或者结束链式调用,那么使用 done 来终止。使用 catch 来终止不是很好,因为 catch handler可能自己会抛出错误。

    这也是在 ES6 中实现  done 的方式:

Promise.prototype.done = function(onFulfilled, onRejected){
    this.then(onFulfilled, onRejected)
    .catch(function(reason){
        // Throw an exception globally
        setTimeout( ()=>{ throw reason }, 0 );
    });
};

    虽然 done 功能极为有用,但是还没有添加到 ES6 中,因为在将来这种检测可以被引擎自动调用(在第12章中已经看到过)。

14.2. finally()

    有时你希望不管是否出现错误,还是继续执行某个动作。例如在使用完一个资源后清理它。这便是Promise 方法 finally() 所做的,极为类似于异常处理机制中的 finally 。其回调方法不接受参数,但可以通过resolution或rejection来通知它。

createResource(···)
.then(function (value1) {
    // Use resource
})
.then(function (value2) {
    // Use resource
})
.finally(function () {
    // Clean up
});

    这是 Domenic Denicola 建议的finally()实现

Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    // We don’t invoke the callback in here,
    // because we want then() to handle its exceptions
    return this.then(
        // Callback fulfills => continue with receiver’s fulfillment or rejection
        // Callback rejects => pass on that rejection (then() has no 2nd paramet\er!)
        value  => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
    );
};

    回调函数决定了 接收器(this)如何处理settlement:

        1. 如果回调函数抛出一个异常或者返回一个rejected Promise,那么变为rejection值。

        2. 否则,接收器的settlement (fulfillment 或 rejection)变为 finally( ) 返回的 promise 的settlement。通过这种方式从链式方法取出 finally() 的结果。

    例一(by Jake Archibald):使用 finally() 来隐藏 spinner:

showSpinner();
fetchGalleryData()
.then(data => updateGallery(data) )
.catch(showNoDataError)
.finnally(hideSpinner);

    例二(by Kris Kowal):使用 finally 来清除测试

let HTTP = require("q-io/http");
let server = HTTP.Server(app);
return server.listen(0)
.then(function () {
    // run test
})
.finally(server.stop);

15. ES6 兼容的Promise 库

    下面有一些Promise 库,下面这些库都遵循ES6 API,意味着你现在就可以使用它们,而且也容易迁移到将来的原生 ES6 代码。

  • "RSVP.js" 来自 Stefen Penner,为 ES6 promise API 的超集。

       。“ES6-Promises”来自Jake Archibald, 从RSVP.js只提取出ES6 API。

  • "Native Promise Only (NPO)"来自 Kyle Simpson,是原生 ES6 promise 的 polyfill,尽可能接近而不是扩展严格规范定义

  • "Lie" 来自 Calvin Metcalf, 小巧,完善的 promise 库,遵循 Promises/A+ 规范。

  • Q.promise 来自 Kris Kowal,实现 ES6 API。

  •  最近,来自 Paul Millr 包含 promise 的 "ES6 Shim"。

16. 与传统异步代码交互

    当你使用一个Promsie 库时,有时需要使用基于不支持Promsie 的异步代码。本章节讲述一下 Node.js 风格的异步函数与 jQuery deferreds。

16.1 与Node.js交互

    Promise 库 Q 有几个工具方法,可以将使用 Node.js 风格(err, result)的回调的函数 转换 为返回Promise的函数(还有一些做相反动作的函数,将Promise函数转换为一些接受回调的函数)。例如:

let readFile = Q.denodeify(FS.readFile);

readFile('foo.txt', 'utf-8')
.then(function (text) {
    ···
});

    denodify 为一个符合 ES6 Promise API ,仅提供 denodification 功能的一个微型库。

16.2. 与jQuery交互

    jQuery 的 deferreds 类似于Promise,但是也有几个兼容性方面的不同。方法 then() 类似于ES6 Promises(主要不同之处是:不能捕获reactions 中的错误)。我们可以通过 Promise.resolve() 将 一个 jQuery deferred 转换为 一个ES6 Promise:

Promise.resolve(
    jQuery.ajax({
        url: 'somefile.html',
        type: 'GET'
    }))
.then(function (data) {
    console.log(data);
})
.catch(function (reason) {
    console.error(reason);
});

17. 延伸阅读

    [1] "Promises/A+"http://promisesaplus.com/), Brian Cavalier 与 Domenic Denicola 编辑(JS Promises事实标准)

    [2] "Javascript Promises:再次强势归来"http://www.html5rocks.com/en/tutorials/es6/promises/, 来自 Jake Archibad(挺不错的 promise 简要介绍)

    [3] "Promsie 反模式"http://taoofcode.net/promise-anti-patterns/, 来自代码之道(要点与技术)

    [4] "Promise 模式"https://www.promisejs.org/patterns/来自 Forbes Lindeasy

    [5] "Revealing构造函数模式"http://domenic.me/2014/02/13/the-revealing-constructor-pattern/, 来自 Domenic Denicola(为 Promise 构造函数使用的模式)

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!