理解Promises/A +规范

Aad*_*hah 1 javascript promise

承诺/ A +规范是最小规范之一.因此,实施它是理解它的最佳方式.Forbes Lindesay的以下答案向我们介绍了实现Promises/A +规范,Basic Javascript promise实现尝试的过程.但是,当我测试它时,结果并不令人满意:

? 109 tests passed
? 769 tests failed
Run Code Online (Sandbox Code Playgroud)

很明显,Promises/A +规范并不像看起来那么容易实现.您将如何实现规范并向新手解释您的代码?Forbes Lindesay在解释他的代码方面表现非常出色,但遗憾的是他的实现不正确.

Aad*_*hah 11

什么是承诺?

promise是一种thenable符合Promises/A +规范的行为.

A thenable是具有then方法的任何对象或函数.

这是承诺的样子:

var promise = {
    ...
    then: function (onFulfilled, onRejected) { ... },
    ...
};
Run Code Online (Sandbox Code Playgroud)

这是我们从一开始就了解承诺的唯一事情(不包括其行为).

理解Promises/A +规范

Promises/A +规范分为3个主要部分:

  1. 承诺国家
  2. then方法
  3. 承诺解决程序

规范没有提到如何创建,履行或拒绝承诺.

因此,我们将从创建这些功能开始:

function deferred() { ... } // returns an object { promise, resolve, reject }

function fulfill(promise, value) { ... } // fulfills promise with value
function reject(promise, reason) { ... } // rejects promise with reason
Run Code Online (Sandbox Code Playgroud)

虽然没有标准的创建承诺的方法,但测试要求我们deferred无论如何都要公开一个函数.因此,我们只会deferred用来创造新的承诺:

  • deferred():创建一个对象,包括{ promise, resolve, reject }:

    • promise 是一个目前处于待定状态的承诺.
    • resolve(value)解决了承诺value.
    • reject(reason)具有拒绝原因将承诺从待决状态转移到被拒绝状态reason.

这是deferred函数的部分实现:

function deferred() {
    var call = true;

    var promise = {
        then: undefined,
        ...
    };

    return {
        promise: promise,
        resolve: function (value) {
            if (call) {
                call = false;
                resolve(promise, value);
            }
        },
        reject: function (reason) {
            if (call) {
                call = false;
                reject(promise, reason);
            }
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

NB

  1. promise对象then目前只有一个属性undefined.我们仍然需要决定then函数应该是什么以及promise对象应具有的其他属性(即promise对象的形状).该决定还将影响fulfillreject功能的实施.
  2. resolve(promise, value)reject(promise, value)职能应该只调用一次,如果我们调用一个那么我们不应该能够调用其他.因此,我们将它们包装在一个闭包中,并确保它们只在它们之间被调用一次.
  3. 我们deferred在承诺解决程序的定义中引入了一个新功能resolve(promise, value).规范表示此功能为[[Resolve]](promise, x).该功能的实现完全由规范决定.因此,我们接下来将实施它.
function resolve(promise, x) {
// 2.3.1. If promise and x refer to the same object,
//        reject promise with a TypeError as the reason.
    if (x === promise) return reject(promise, new TypeError("Self resolve"));
// 2.3.4. If x is not an object or function, fulfill promise with x.
    var type = typeof x;
    if (type !== "object" && type !== "function" || x === null)
        return fulfill(promise, x);
// 2.3.3.1. Let then be x.then.
// 2.3.3.2. If retrieving the property x.then results in a thrown exception e,
//          reject promise with e as the reason.
    try {
        var then = x.then;
    } catch (e) {
        return reject(promise, e);
    }
// 2.3.3.4. If then is not a function, fulfill promise with x.
    if (typeof then !== "function") return fulfill(promise, x);
// 2.3.3.3. If then is a function, call it with x as this, first argument
//          resolvePromise, and second argument rejectPromise, where:
// 2.3.3.3.1. If/when resolvePromise is called with a value y,
//            run [[Resolve]](promise, y).
// 2.3.3.3.2. If/when rejectPromise is called with a reason r,
//            reject promise with r.
// 2.3.3.3.3. If both resolvePromise and rejectPromise are called,
//            or multiple calls to the same argument are made,
//            the first call takes precedence, and any further calls are ignored.
// 2.3.3.3.4. If calling then throws an exception e,
// 2.3.3.3.4.1. If resolvePromise or rejectPromise have been called, ignore it.
// 2.3.3.3.4.2. Otherwise, reject promise with e as the reason.
    promise = deferred(promise);
    try {
        then.call(x, promise.resolve, promise.reject);
    } catch (e) {
        promise.reject(e);
    }
}
Run Code Online (Sandbox Code Playgroud)

NB

  1. 我们省略了2.3.2节,因为它是一种取决于promise对象形状的优化.我们将在最后重新审视此部分.
  2. 如上所述,2.3.3.3节的描述比实际代码长得多.这是因为聪明的hack promise = deferred(promise)允许我们重用deferred函数的逻辑.这保证了promise.resolvepromise.reject只调用一次,在两者之间.我们只需要对deferred函数进行一些小改动就可以使这个hack工作.
function deferred(promise) {
    var call = true;

    promise = promise || {
        then: undefined,
        ...
    };

    return /* the same object as before */
}
Run Code Online (Sandbox Code Playgroud)

承诺状态和then方法

我们已经延迟了长期决定promise对象形状的问题,但是我们不能再拖延了,因为它们fulfillreject函数的实现都依赖于它.是时候阅读规范对承诺状态的说法了:

承诺必须处于以下三种状态之一:待处理,履行或拒绝.

  1. 等待时,承诺:
    1. 可以过渡到已履行或被拒绝的状态.
  2. 满足后,承诺:
    1. 不得过渡到任何其他州.
    2. 必须有一个值,不能改变.
  3. 被拒绝时,承诺:
    1. 不得过渡到任何其他州.
    2. 必须有一个不能改变的理由.

在这里,"绝不能改变"意味着不可改变的身份(即===),但并不意味着深刻的不变性.

我们如何知道目前承诺的状态?我们可以这样做:

var PENDING   = 0;
var FULFILLED = 1;
var REJECTED  = 2;

var promise = {
    then:  function (onFulfilled, onRejected) { ... },
    state: PENDING | FULFILLED | REJECTED, // vertical bar is not bitwise or
    ...
};
Run Code Online (Sandbox Code Playgroud)

但是,有一个更好的选择.由于promise的状态只能通过它的then方法观察(即取决于then方法行为不同的promise的状态),我们可以创建三个then对应于三种状态的专用函数:

var promise = {
    then: pending | fulfilled | rejected,
    ...
};

function pending(onFulfilled, onRejected) { ... }
function fulfilled(onFulfilled, onRejected) { ... }
function rejected(onFulfilled, onRejected) { ... }
Run Code Online (Sandbox Code Playgroud)

此外,我们还需要一个属性来保存承诺的数据.当许未决的数据是队列onFulfilledonRejected回调.当履行承诺时,数据是承诺的价值.当承诺被拒绝时,数据就是承诺的原因.

当我们创建一个新的promise时,初始状态是挂起的,初始数据是一个空队列.因此,我们可以deferred按如下方式完成该功能的实现:

function deferred(promise) {
    var call = true;

    promise = promise || {
        then: pending,
        data: []
    };

    return /* the same object as before */
}
Run Code Online (Sandbox Code Playgroud)

另外,既然我们知道了promise对象的形状,我们最终可以实现fulfillreject函数:

function fulfill(promise, value) {
    setTimeout(send, 0, promise.data, "onFulfilled", value);
    promise.then = fulfilled;
    promise.data = value;
}

function reject(promise, reason) {
    setTimeout(send, 0, promise.data, "onRejected", reason);
    promise.then = rejected;
    promise.data = reason;
}

function send(queue, callback, data) {
    for (var item of queue) item[callback](data);
}
Run Code Online (Sandbox Code Playgroud)

我们需要使用setTimeout因为根据规范的第2.2.4节,onFulfilled或者onRejected在执行上下文堆栈仅包含平台代码之前不得调用.

接下来,我们需要实现pending,fulfilledrejected功能.我们将从将pending函数onFulfilledonRejected回调推送到队列并返回新promise 的函数开始:

function pending(onFulfilled, onRejected) {
    var future = deferred();

    this.data.push({
        onFulfilled: typeof onFulfilled === "function" ?
            compose(future, onFulfilled) : future.resolve,
        onRejected:  typeof onRejected  === "function" ?
            compose(future, onRejected)  : future.reject
    });

    return future.promise;
}

function compose(future, fun) {
    return function (data) {
        try {
            future.resolve(fun(data));
        } catch (reason) {
            future.reject(reason);
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

我们需要测试是否onFulfilledonRejected是函数,因为根据2.2.1节的规范它们是可选的参数.如果onFulfilled并且onRejected提供了它们,则根据2.2.7.1 和规范的2.2.7.2 节,它们由延迟值组成.否则,它们被短路按照部分2.2.7.3节2.2.7.4说明书.

最后,我们实现fulfilledrejected功能如下:

function fulfilled(onFulfilled, onRejected) {
    return bind(this, onFulfilled);
}

function rejected(onFulfilled, onRejected) {
    return bind(this, onRejected);
}

function bind(promise, fun) {
    if (typeof fun !== "function") return promise;
    var future = deferred();
    setTimeout(compose(future, fun), 0, promise.data);
    return future.promise;
}
Run Code Online (Sandbox Code Playgroud)

有趣的是,承诺是monad,可以在bind上面恰当命名的函数中看到.有了这个,我们现在完成了Promises/A +规范的实现.

unit功能

如果承诺是monad,那么它们也必须具有一个unit功能.令人惊讶的是,承诺有两个单元功能:一个用于解决的承诺,一个用于拒绝承诺.由于测试需要它们,我们将它们添加到导出界面:

exports.resolved = function (data) {
    return {
        then: fulfilled,
        data: data
    };
};

exports.rejected = function (data) {
    return {
        then: rejected,
        data: data
    };
};

exports.deferred = deferred;
Run Code Online (Sandbox Code Playgroud)

优化 resolve

规范的2.3.2节描述了resolve(promise, x)x确定为承诺时对函数的优化.这是优化的resolve功能:

function resolve(promise, x) {
    if (x === promise) return reject(promise, new TypeError("Self resolve"));

    var type = typeof x;
    if (type !== "object" && type !== "function" || x === null)
        return fulfill(promise, x);

    try {
        var then = x.then;
    } catch (e) {
        return reject(promise, e);
    }

    if (typeof then !== "function") return fulfill(promise, x);
// 2.3.2.1. If x is pending, promise must remain pending until x is
//          fulfilled or rejected.
    if (then === pending) return void x.data.push({
        onFulfilled: function (value) {
            resolve(promise, value);
        },
        onRejected: function (reason) {
            reject(promise, reason);
        }
    });
// 2.3.2.2. If/when x is fulfilled, fulfill promise with the same value.
    if (then === fulfilled) return resolve(promise, x.data);
// 2.3.2.3. If/when x is rejected, reject promise with the same reason.
    if (then === rejected) return reject(promise, x.data);

    promise = deferred(promise);

    try {
        then.call(x, promise.resolve, promise.reject);
    } catch (e) {
        promise.reject(e);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,虽然规范说"如果/何时x满足,但要满足promise相同的值",我们必须使用resolve而不是fulfill.我不确定这是否是规范中的错误.

把它们放在一起

该代码可作为要点提供.您只需下载并运行测试套件即可:

$ npm install promises-aplus-tests -g
$ promises-aplus-tests promise.js
Run Code Online (Sandbox Code Playgroud)

不用说,所有的测试都通过了.

  • 我们需要一个类似于stackoverflow的网站,供喜欢分享知识的人(以及喜欢学习的人)而不需要提问.无论如何,这个大工作+1 :-) (5认同)
  • 一个艰巨的努力确定,但问题是什么回答? (3认同)
  • 仅供参考,现在有一种标准方法可以在[ES6规范](http://www.ecma-international.org/ecma-262/6.0/#sec-promise-constructor)中创建一个承诺,所以你的陈述*虽然有没有标准的创建承诺的方式*可以编辑. (3认同)