如何实现承诺/延迟库?[关闭]


74

如何实现像q这样的promise / defer库?我试图阅读源代码,但发现它很难理解,所以我认为如果有人可以从较高层次上向我解释在单线程JS环境中实现promise的技术,那就太好了。例如Node和浏览器。


Answers:


149

我发现比说明示例更难解释了,因此这是延迟/承诺的一种非常简单的实现。

免责声明:这不是功能性实现,并且Promise / A规范的某些部分丢失了,这只是在解释承诺的基础。

tl; dr:转到“创建类和示例”部分以查看完整的实现。

诺言:

首先,我们需要创建一个带有回调数组的Promise对象。我将开始处理对象,因为它更清晰:

var promise = {
  callbacks: []
}

现在使用以下方法添加回调:

var promise = {
  callbacks: [],
  then: function (callback) {
    callbacks.push(callback);
  }
}

我们也需要错误回调:

var promise = {
  okCallbacks: [],
  koCallbacks: [],
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
}

延后:

现在创建将具有承诺的defer对象:

var defer = {
  promise: promise
};

延迟需要解决:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },
};

并需要拒绝:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

请注意,在超时中将调用回调,以使代码始终异步。

这就是基本的延迟/承诺实现所需要的。

创建类和示例:

现在让我们将两个对象都转换成类,首先是promise:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
};

现在推迟:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

这是一个使用示例:

function test() {
  var defer = new Defer();
  // an example of an async call
  serverCall(function (request) {
    if (request.status === 200) {
      defer.resolve(request.responseText);
    } else {
      defer.reject(new Error("Status code was " + request.status));
    }
  });
  return defer.promise;
}

test().then(function (text) {
  alert(text);
}, function (error) {
  alert(error.message);
});

如您所见,基本部分既简单又小巧。当您添加其他选项(例如多重承诺解决方案)时,它会增加:

Defer.all(promiseA, promiseB, promiseC).then()

或承诺链接:

getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);

要阅读有关规范的更多信息:CommonJS Promise Specification。请注意,主要库(Q,when.js,rsvp.js,node-promise等)遵循Promises / A规范。

希望我足够清楚。

编辑:

如评论中的要求,我在此版本中添加了两件事:

  • 无论其处于何种状态,都可以调用承诺。
  • 兑现承诺的可能性。

为了能够在解决时调用promise,您需要将状态添加到promise,然后在调用then时检查该状态。如果状态已解决或被拒绝,则只需使用其数据或错误执行回调。

为了能够链接承诺,您需要为每次调用生成一个新的defer,then并在解决/拒绝承诺时,使用回调的结果来解决/拒绝新的promise。因此,在完成承诺后,如果回调函数返回新的承诺,则该绑定到与一起返回的承诺then()。如果不是,则使用回调结果来解决promise。

这是承诺:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  status: 'pending',
  error: null,

  then: function (okCallback, koCallback) {
    var defer = new Defer();

    // Add callbacks to the arrays with the defer binded to these callbacks
    this.okCallbacks.push({
      func: okCallback,
      defer: defer
    });

    if (koCallback) {
      this.koCallbacks.push({
        func: koCallback,
        defer: defer
      });
    }

    // Check if the promise is not pending. If not call the callback
    if (this.status === 'resolved') {
      this.executeCallback({
        func: okCallback,
        defer: defer
      }, this.data)
    } else if(this.status === 'rejected') {
      this.executeCallback({
        func: koCallback,
        defer: defer
      }, this.error)
    }

    return defer.promise;
  },

  executeCallback: function (callbackData, result) {
    window.setTimeout(function () {
      var res = callbackData.func(result);
      if (res instanceof Promise) {
        callbackData.defer.bind(res);
      } else {
        callbackData.defer.resolve(res);
      }
    }, 0);
  }
};

和推迟:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    var promise = this.promise;
    promise.data = data;
    promise.status = 'resolved';
    promise.okCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, data);
    });
  },

  reject: function (error) {
    var promise = this.promise;
    promise.error = error;
    promise.status = 'rejected';
    promise.koCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, error);
    });
  },

  // Make this promise behave like another promise:
  // When the other promise is resolved/rejected this is also resolved/rejected
  // with the same data
  bind: function (promise) {
    var that = this;
    promise.then(function (res) {
      that.resolve(res);
    }, function (err) {
      that.reject(err);
    })
  }
};

如您所见,它已经增长了很多。


1
链接CommonJS Promise / A提案可能是个好主意...对于那些了解并希望进一步了解这种模式的人:)
gustavohenke

1
谢谢。添加了所有规格的链接。
Kaizo

2
您的示例绝对不起作用。Promise状态既不会成立(您可以同时满足和拒绝您),也无法在解析工作后添加回调,也不会then返回另一个Promise(这是必不可少的)。
Bergi

2
这是一个极端简单的示例,而不是功能齐全的实现。我知道规范的某些部分尚未实施,可能无法正常工作,但这只是为了解释承诺的基础。
Kaizo

是的,我看到了,需要简化才能得到一个好的答案(我的既不是完全符合要求的人)。但是我认为它缺少承诺实现的主要部分。一遍又一遍地重复相同的代码只会使答案不再更长:-(
Bergi 2013年

7

就实现而言,Q是一个非常复杂的Promise库,因为它旨在支持流水线和RPC类型的方案。我在这里Promises / A +规范的基本实现。

原则上,这很简单。在兑现/解决承诺之前,您可以通过将所有回调或错误回调推入数组来保留它们的记录。兑现诺言后,您将调用适当的回调或错误回复,并记录兑现诺言的结果(以及实现还是拒绝)。解决之后,您只需使用存储的结果调用回调或错误回调即可。

这使您接近的语义done。要构建then,只需要返回一个新的promise,并通过调用回调/ errbacks即可解决。

如果您对全面支持承诺实现的背后开发的全部理由感兴趣,该实现支持像Q这样的RPC和流水线,则可以在此处阅读kriskowal的推理。这是一种非常不错的渐进方法,如果您正在考虑兑现承诺,我就不能推荐它。即使您只是要使用Promise库,也可能值得一读。


2
哇,@ KrisKowal的文章很棒。他应该将其发布为答案,以获得数十个投票:-)
Bergi

1
确实,它很棒,我计划将其重新格式化为合适的网页,以便在某些时候更好地格式化。
ForbesLindesay 2013年

为KrisKowal文章+1。伟大的阅读。
Derek Chiang

链接似乎已断开...
TJ

6

正如福布斯(Forbes)在他的回答中提到的,我记述了制作像Q这样的库所涉及的许多设计决策,请参见https://github.com/kriskowal/q/tree/v1/design。可以说,promise库有多个级别,并且许多库在各个级别处停止。

在第一层,由Promises / A +规范捕获,promise是最终结果的代理,并且适合于管理“本地异步”。也就是说,它适合于确保工作以正确的顺序进行,并且适合于简单而直接地聆听操作的结果,而不管其是否已经解决或将来会发生。这也使一个或多个参与方订阅最终结果变得一样简单。

正如我已经实现的那样,Q提供的承诺是最终,远程或最终+远程结果的代理。为此,它的设计是倒置的,对诺言有不同的实现方式-延后的诺言,已兑现的诺言,拒绝的诺言和对远程对象的诺言(最后一个在Q-Connection中实现)。它们都共享相同的接口,并通过发送和接收消息(例如“ then”(对于Promises / A +足够))以及“ get”和“ invoke”来工作。因此,Q与“分布式异步”有关,并且存在于另一层。

但是,Q实际上是从更高的层取下来的,在那层层,promise用于管理分布式异步 相互可疑的各方(例如您,商人,银行,Facebook,政府)-不是敌人,甚至是朋友,但有时存在冲突利益。我实现的Q设计为与强化的安全承诺(这是分隔promise和的原因resolve)兼容的API ,希望它将人们引入承诺,培训他们使用此API并允许他们获取其代码如果他们将来需要在安全的mashup中使用Promise。

当然,在层上移动时通常需要在速度上进行权衡。因此,promise实现也可以设计为共存。这就是“ thenable”概念的来源。可以将每一层的Promise库设计为使用来自任何其他层的Promise,因此可以实现多种实现并存,并且用户只能购买所需的东西。

综上所述,没有难读的借口。Domenic和我正在开发一个Q的版本,该版本将更加模块化和易用,其一些分散注意力的依赖性和变通办法已移至其他模块和软件包中。值得庆幸的是,像《福布斯》克罗克福德等人,通过简化图书馆来填补了教育空白。


链接似乎已断开...
TJ

4

首先,请确保您了解Promises应该如何工作。看一下CommonJs Promises提案Promises / A +规范

可以用几行简单的代码就可以实现两个基本概念:

  • Promise确实会异步解决结果。添加回调是透明的操作-与是否已经解决了承诺无关,一旦可用,它们就会被调用。

    function Deferred() {
        var callbacks = [], // list of callbacks
            result; // the resolve arguments or undefined until they're available
        this.resolve = function() {
            if (result) return; // if already settled, abort
            result = arguments; // settle the result
            for (var c;c=callbacks.shift();) // execute stored callbacks
                c.apply(null, result);
        });
        // create Promise interface with a function to add callbacks:
        this.promise = new Promise(function add(c) {
            if (result) // when results are available
                c.apply(null, result); // call it immediately
            else
                callbacks.push(c); // put it on the list to be executed later
        });
    }
    // just an interface for inheritance
    function Promise(add) {
        this.addCallback = add;
    }
    
  • 承诺有一种then允许链接它们的方法。我接受了一个回调并返回了一个新的Promise,该Promise将在使用第一个promise的结果被调用后用该回调的结果来解决。如果回调返回一个Promise,它将被同化而不是嵌套。

    Promise.prototype.then = function(fn) {
        var dfd = new Deferred(); // create a new result Deferred
        this.addCallback(function() { // when `this` resolves…
            // execute the callback with the results
            var result = fn.apply(null, arguments);
            // check whether it returned a promise
            if (result instanceof Promise)
                result.addCallback(dfd.resolve); // then hook the resolution on it
            else
                dfd.resolve(result); // resolve the new promise immediately 
            });
        });
        // and return the new Promise
        return dfd.promise;
    };
    

进一步的概念将是维护单独的错误状态(带有附加的回调)并在处理程序中捕获异常,或者保证回调的异步性。添加这些后,您将获得功能齐全的Promise实现。

这是写出的错误内容。不幸的是,它很重复。您可以通过使用额外的闭包来做得更好,但是真的很难理解。

function Deferred() {
    var callbacks = [], // list of callbacks
        errbacks = [], // list of errbacks
        value, // the fulfill arguments or undefined until they're available
        reason; // the error arguments or undefined until they're available
    this.fulfill = function() {
        if (reason || value) return false; // can't change state
        value = arguments; // settle the result
        for (var c;c=callbacks.shift();)
            c.apply(null, value);
        errbacks.length = 0; // clear stored errbacks
    });
    this.reject = function() {
        if (value || reason) return false; // can't change state
        reason = arguments; // settle the errror
        for (var c;c=errbacks.shift();)
            c.apply(null, reason);
        callbacks.length = 0; // clear stored callbacks
    });
    this.promise = new Promise(function add(c) {
        if (reason) return; // nothing to do
        if (value)
            c.apply(null, value);
        else
            callbacks.push(c);
    }, function add(c) {
        if (value) return; // nothing to do
        if (reason)
            c.apply(null, reason);
        else
            errbacks.push(c);
    });
}
function Promise(addC, addE) {
    this.addCallback = addC;
    this.addErrback = addE;
}
Promise.prototype.then = function(fn, err) {
    var dfd = new Deferred();
    this.addCallback(function() { // when `this` is fulfilled…
        try {
            var result = fn.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was thrown
            dfd.reject(e);
        }
    });
    this.addErrback(err ? function() { // when `this` is rejected…
        try {
            var result = err.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was re-thrown
            dfd.reject(e);
        }
    } : dfd.reject); // when no `err` handler is passed then just propagate
    return dfd.promise;
};

没有解释它的实现方式,也没有显示延迟/承诺实现。
Kaizo

@Kaizo:谢谢,OP并没有具体询问延期问题。说明已添加并切换到Deferred接口。还有没有相关的东西?
Bergi

我花了一些时间来理解它的工作原理,尽管它很小而且很受评论。您的版本比我的聪明版本更完整,但也更复杂。我唯一想念的“重要”是拒绝选项和错误回调。
Kaizo

@Kaizo:是的,我故意省略了它,因为当简洁地编写代码时,代码只会变得更加复杂和难以理解:-)看看我的编辑…
Bergi

在哪种情况下addCallbackPromise类中的方法将被多次调用?该then方法将仅返回一个新Promise实例,那么为什么要在Deferred类中保留一个回调数组?
拖曳

1

您可能想查看有关Adehun的博客文章

Adehun是一种非常轻量级的实现(大约166 LOC),对于学习如何实现Promise / A +规范非常有用。

免责声明:我写了博客文章,但博客文章确实解释了有关Adehun的全部内容。

过渡功能–状态过渡的关守

关守功能;确保在满足所有必需条件时发生状态转换。

如果满足条件,此功能将更新承诺的状态和价值。然后,它触发过程功能以进行进一步处理。

过程功能根据转换(例如,待完成)执行正确的操作,稍后将进行说明。

function transition (state, value) {
  if (this.state === state ||
    this.state !== validStates.PENDING ||
    !isValidState(state)) {
      return;
    }

  this.value = value;
  this.state = state;
  this.process();
}

然后功能

then函数接受两个可选参数(onFulfill和onReject处理程序),并且必须返回一个新的Promise。两个主要要求:

  1. 基本的promise(然后被称为基础promise)需要使用传入的处理程序创建一个新的promise;基本承诺还存储了对此创建的承诺的内部引用,因此一旦基本承诺实现/拒绝,就可以调用它。

  2. 如果基本诺言得以履行(即履行或拒绝),则应立即调用适当的处理程序。Adehun.js通过在then函数中调用process来处理这种情况。

``

function then(onFulfilled, onRejected) {
    var queuedPromise = new Adehun();
    if (Utils.isFunction(onFulfilled)) {
        queuedPromise.handlers.fulfill = onFulfilled;
    }

    if (Utils.isFunction(onRejected)) {
        queuedPromise.handlers.reject = onRejected;
    }

    this.queue.push(queuedPromise);
    this.process();

    return queuedPromise;
}`

流程功能–处理过渡

状态转换后或调用then函数时将调用此方法。因此,由于可能已从then函数调用了它,因此需要检查未完成的promise。

Process对所有内部存储的承诺(即通过then函数附加到基本承诺的承诺)运行Promise Resolution过程,并执行以下Promise / A +要求:

  1. 使用Utils.runAsync帮助程序(围绕setTimeout的精简包装(setImmediate也将起作用))异步调用处理程序。

  2. 如果缺少onSuccess和onReject处理程序,则为其创建后备处理程序。

  3. 根据承诺状态(例如已实现或拒绝)选择正确的处理函数。

  4. 将处理程序应用于基本承诺的价值。该操作的值传递给Resolve函数以完成promise处理周期。

  5. 如果发生错误,则附加的承诺将立即被拒绝。

    function process(){var that = this,performFallBack = function(value){返回值;},rejectFallBack = function(reason){抛出原因;};

    if (this.state === validStates.PENDING) {
        return;
    }
    
    Utils.runAsync(function() {
        while (that.queue.length) {
            var queuedP = that.queue.shift(),
                handler = null,
                value;
    
            if (that.state === validStates.FULFILLED) {
                handler = queuedP.handlers.fulfill ||
                    fulfillFallBack;
            }
            if (that.state === validStates.REJECTED) {
                handler = queuedP.handlers.reject ||
                    rejectFallBack;
            }
    
            try {
                value = handler(that.value);
            } catch (e) {
                queuedP.reject(e);
                continue;
            }
    
            Resolve(queuedP, value);
        }
    });
    

    }

解决功能–解决承诺

这可能是Promise实现中最重要的部分,因为它可以处理Promise解决方案。它接受两个参数-Promise及其解析值。

尽管要检查各种可能的分辨率值;有趣的解决方案有两种-涉及传入的承诺和thenable(具有then值的对象)。

  1. 传递承诺值

如果解决方案值是另一个承诺,则该承诺必须采用此解决方案值的状态。由于此解决方案值可以挂起或结算,因此最简单的方法是将新的then处理程序附加到该解决方案值并处理其中的原始承诺。无论何时达成,最初的承诺都会被解决或拒绝。

  1. 传递合理的价值

这里的问题是,thenable值的then函数必须仅被调用一次(对于函数编程的一次包装器来说,这是很好的用法)。同样,如果对then函数的检索抛出异常,则将立即拒绝诺言。

像以前一样,then函数将使用最终解析或拒绝promise的函数来调用,但此处的区别是在第一个调用上设置的被调用标志,而随后的调用则变为无操作。

function Resolve(promise, x) {
  if (promise === x) {
    var msg = "Promise can't be value";
    promise.reject(new TypeError(msg));
  }
  else if (Utils.isPromise(x)) {
    if (x.state === validStates.PENDING){
      x.then(function (val) {
        Resolve(promise, val);
      }, function (reason) {
        promise.reject(reason);
      });
    } else {
      promise.transition(x.state, x.value);
    }
  }
  else if (Utils.isObject(x) ||
           Utils.isFunction(x)) {
    var called = false,
        thenHandler;

    try {
      thenHandler = x.then;

      if (Utils.isFunction(thenHandler)){
        thenHandler.call(x,
          function (y) {
            if (!called) {
              Resolve(promise, y);
              called = true;
            }
          }, function (r) {
            if (!called) {
              promise.reject(r);
              called = true;
            }
       });
     } else {
       promise.fulfill(x);
       called = true;
     }
   } catch (e) {
     if (!called) {
       promise.reject(e);
       called = true;
     }
   }
 }
 else {
   promise.fulfill(x);
 }
}

无极的建设者

这是将所有内容整合在一起的方式。履行和拒绝功能是语法糖,通过无操作功能来解决和拒绝。

var Adehun = function (fn) {
 var that = this;

 this.value = null;
 this.state = validStates.PENDING;
 this.queue = [];
 this.handlers = {
   fulfill : null,
   reject : null
 };

 if (fn) {
   fn(function (value) {
     Resolve(that, value);
   }, function (reason) {
     that.reject(reason);
   });
 }
};

我希望这有助于对诺言的工作方式有更多的了解。


请在您的答案中发布博客的内容,或者至少发布其要旨。链接不是答案
Bergi 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.