【问题标题】:Understanding the Promises/A+ specification了解 Promises/A+ 规范
【发布时间】:2016-07-11 14:28:41
【问题描述】:

Promises/A+ 规范是最小的规范之一。因此,实施它是理解它的最佳方式。 Forbes Lindesay 的以下回答将引导我们完成实现 Promises/A+ 规范的过程,Basic Javascript promise implementation attempt。但是,当我testedresults 并不令人满意:

✔ 109 tests passed
✘ 769 tests failed

显然,Promises/A+ 规范并不像看起来那么容易实现。您将如何实现规范并向新手解释您的代码? Forbes Lindesay 很好地解释了他的代码,但不幸的是他的实现不正确。

【问题讨论】:

  • 看,自己回答问题就可以了。但问题本身必须达到正常的质量标准。您的问题非常广泛,没有需要解决的具体问题。
  • @JK。确实。我花更多的时间在答案上,而不是在问题上。现在我需要回去重新思考这个问题。玩Jeopardy! 比看起来更难。

标签: javascript promise


【解决方案1】:

什么是承诺?

promise 是一个thenable,其行为符合Promises/A+ 规范。

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

这是一个承诺的样子:

var promise = {
    ...
    then: function (onFulfilled, onRejected) { ... },
    ...
};

这是我们从一开始就知道的关于承诺的唯一事情(不包括它的行为)。

了解 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

虽然没有创建承诺的标准方法,但tests 仍然要求我们公开deferred 函数。因此,我们只会使用 deferred 来创建新的 Promise:

  • deferred():创建一个由{ promise, resolve, reject }组成的对象:

    • promise 是一个当前处于待处理状态的 Promise。
    • resolve(value) 使用 value 解析承诺。
    • reject(reason) 将 Promise 从挂起状态移动到拒绝状态,拒绝原因为 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);
            }
        }
    };
}

注意

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

注意

  1. 我们省略了section 2.3.2,因为它是一种取决于promise 对象形状的优化。我们将在接近尾声时重温本节。
  2. 如上所见,section 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 */
}

Promise 状态和then 方法

我们已经将决定 Promise 对象形状的问题推迟了很长时间,但我们不能再拖延了,因为 fulfillreject 函数的实现都依赖于它。是时候阅读规范中关于承诺状态的内容了:

promise 必须处于以下三种状态之一:待处理、已完成或已拒绝。

  1. 待处理时,承诺:
    1. 可能会转换为已完成或已拒绝状态。
  2. 当实现时,一个承诺:
    1. 不得转换到任何其他状态。
    2. 必须有一个不能改变的值。
  3. 当被拒绝时,一个承诺:
    1. 不得转换到任何其他状态。
    2. 必须有原因,不能改变。

在这里,“不得更改”是指不可变的身份(即===),但并不意味着深度不变。

我们如何知道 Promise 当前处于哪个状态?我们可以这样做:

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
    ...
};

但是,还有更好的选择。由于promise 的状态只能通过then 方法观察到(即,根据promise 的状态,then 方法的行为不同),我们可以创建三个专门的then 函数对应于这三种状态:

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

function pending(onFulfilled, onRejected) { ... }
function fulfilled(onFulfilled, onRejected) { ... }
function rejected(onFulfilled, onRejected) { ... }

此外,我们还需要一个属性来保存 Promise 的数据。当承诺待处理时,数据是onFulfilledonRejected 回调的队列。当承诺兑现时,数据就是承诺的价值。当 promise 被拒绝时,数据就是 promise 的原因。

当我们创建一个新的 Promise 时,初始状态是挂起的,初始数据是一个空队列。因此,我们可以如下完成deferred函数的实现:

function deferred(promise) {
    var call = true;

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

    return /* the same object as before */
}

此外,现在我们知道了 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);
}

我们需要使用setTimeout,因为根据规范onFulfilledonRejectedsection 2.2.4,在执行上下文堆栈仅包含平台代码之前不得调用。

接下来,我们需要实现pendingfulfilledrejected函数。我们将从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);
        }
    };
}

我们需要测试onFulfilledonRejected 是否是函数,因为根据规范的section 2.2.1,它们是可选参数。如果提供了onFulfilledonRejected,则它们按照规范的section 2.2.7.1section 2.2.7.2 与延迟值组合。否则按照规范的section 2.2.7.3section 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;
}

有趣的是,promises are monads 可以在上面恰当命名的 bind 函数中看到。至此,我们的 Promises/A+ 规范的实现就完成了。

优化resolve

规范的Section 2.3.2 描述了当x 被确定为承诺时对resolve(promise, 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) {
            fulfill(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 fulfill(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);
    }
}

把它们放在一起

代码以gist 的形式提供。您可以简单地下载它并运行测试套件:

$ npm install promises-aplus-tests -g
$ promises-aplus-tests promise.js

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

【讨论】:

  • 这是一项艰巨的努力,但要回答的问题是什么?
  • 仅供参考,现在ES6 specification 中有一种创建承诺的标准方法,因此您的声明虽然还没有创建承诺的标准方法可以被编辑。
  • 受您的启发,我写了一个并通过了测试:github.com/chaoyangnz/promise/blob/master/src/promise.js
  • 这个答案不正确。承诺是用于表示价值生产和价值消费之间合同的一方的术语,而另一方是生产者,代表总体上可能无法保证立即可用的价值,粗略地说。这是一个概念,Promise/A+ 是该概念的一种实现。如果你实现的 Promise 未能通过 Promise/A+ 测试,但仍然可以作为 Promise 工作,那么它们仍然是 Promise。
  • 你的开场白不正确,换成“什么是Promises/A+?”然后解释他们是一个承诺的实现。耐克是鞋,但并非所有鞋都是耐克。
猜你喜欢
  • 2015-06-20
  • 1970-01-01
  • 2016-03-21
  • 1970-01-01
  • 2015-06-08
  • 2018-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多