Promise/延迟库是如何实现的?

IT技术 javascript promise
2021-01-13 11:14:49

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

5个回答

我发现解释比展示一个例子更难,所以这里有一个非常简单的 defer/promise 实现。

免责声明:这不是一个功能实现,缺少 Promise/A 规范的某些部分,这只是为了解释 Promise 的基础。

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

Promise:

首先,我们需要创建一个带有回调数组的 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);
    }
  }
}

推迟:

现在创建一个具有Promise的 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实现所需要的。

创建类和示例:

现在让我们将两个对象都转换为类,首先是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);
});

正如您所看到的,基本部件既简单又小巧。当您添加其他选项时,它会增长,例如多个Promise解析:

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

或Promise链接:

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

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

希望我说得够清楚了。

编辑:

正如评论中所问,我在这个版本中添加了两件事:

  • 调用 then 的可能性,无论它处于什么状态。
  • 链接Promise的可能性。

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

为了能够链接Promise,您需要为每次调用生成一个新的延迟,then并且当Promise被解决/拒绝时,使用回调的结果解决/拒绝新的Promise。所以当promise完成后,如果回调返回一个新的promise,它就会绑定到返回的promise上then()如果不是,则使用回调的结果解决Promise。

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

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

这是一个极其简单的例子,而不是一个功能齐全的实现。我知道规范的某些部分没有实现并且它可能不起作用,但这只是为了解释Promise的基础。
2021-03-22 11:14:49
你的例子绝对行不通。Promise状态既没有解决(您的状态可以同时被满足和拒绝),也不会在解析工作后添加回调,也不会then返回另一个Promise(这是必不可少的)。
2021-04-05 11:14:49
是的,我看到了这一点,并且需要简化才能得到一个好的答案(我的既不是一个完全一致的答案)。但是我认为它缺少promise 实现主要部分。一遍又一遍地重复相同的代码只会使答案更长而不是更好:-(
2021-04-07 11:14:49
谢谢。添加了所有规范的链接。
2021-04-08 11:14:49
链接 CommonJS Promises/A 提案可能会很好......对于那些理解并想要更深入了解这种模式的人:)
2021-04-09 11:14:49

Q 在实现方面是一个非常复杂的 promise 库,因为它旨在支持流水线和 RPC 类型的场景。我有我自己的非常裸露的骨头实现的Promise/ A +规格在这里

原则上很简单。在Promise被解决/解决之前,您可以通过将它们推送到数组中来记录任何回调或错误返回。当 promise 被解决时,你调用适当的回调或 errbacks 并记录 promise 的解决结果(以及它是被实现还是被拒绝)。确定后,您只需使用存储的结果调用回调或错误返回。

这为您提供了大致的语义done要构建,then您只需要返回一个新的Promise,该Promise通过调用回调/errbacks 的结果得到解决。

如果您对开发支持 RPC 和管道(如 Q)的完整Promise实现背后的推理感兴趣,您可以在此处阅读 kriskowal 的推理这是一种非常好的渐进式方法,如果您正在考虑实施Promise,我不能高度推荐。即使您只是打算使用 Promise 库,它也可能值得一读。

确实,它很棒,我计划将其重新格式化为适当的网页,以便在某些时候格式化更好一些。
2021-03-15 11:14:49
哇,@KrisKowal 的那篇文章太棒了。他应该将其发布为获得数十个赞的答案:-)
2021-04-07 11:14:49
+1 为 KrisKowal 文章。很棒的阅读。
2021-04-08 11:14:49
链接好像坏了。。。
2021-04-10 11:14:49

正如福布斯在他的回答中提到的那样,我在https://github.com/kriskowal/q/tree/v1/design中记录了制作像 Q 这样的库所涉及的许多设计决策可以这么说,promise 库有多个级别,并且有很多库停在不同级别。

在第一层,由 Promises/A+ 规范捕获,promise 是最终结果的代理,适用于管理“本地异步”也就是说,它适用于确保工作以正确的顺序发生,并适用于确保无论操作的结果是已经解决还是将来会发生,都可以简单直接地监听操作的结果。它还使一方或多方订阅最终结果变得同样简单。

Q,正如我已经实现的那样,提供了作为最终、远程或最终+远程结果的代理的Promise。为此,它的设计是颠倒的,有不同的Promise实现——延迟的Promise、履行的Promise、拒绝的Promise和远程对象的Promise(最后一个在 Q-Connection 中实现)。它们都共享相同的接口,并通过发送和接收诸如“then”(这对于 Promises/A+ 就足够了)以及“get”和“invoke”之类的消息来工作。所以,Q是关于“分布式异步”的,存在于另一层。

然而,Q 实际上是从更高层撤下的,在那里 Promise 用于管理相互怀疑的各方(例如您、商人、银行、Facebook、政府)之间的分布式异步- 不是敌人,甚至可能是朋友,但有时会发生冲突兴趣。我实现的 Q 被设计成与强化安全Promise的 API 兼容(这就是将promise分开的原因resolve),希望它能向人们介绍Promise,训练他们使用这个 API,并允许他们使用他们的代码如果他们将来需要在安全的 mashup 中使用 promise,请与他们联系。

当然,当您向上移动层时,通常会在速度方面进行权衡。因此,promise 实现也可以设计为共存。这就是“thenable”的概念出现的地方每一层的 Promise 库都可以设计为消费来自任何其他层的 Promise,因此多个实现可以共存,用户可以只购买他们需要的东西。

综上所述,没有任何理由难以阅读。Domenic 和我正在开发一个更加module化和易于使用的 Q 版本,其中一些分散注意力的依赖项和变通方法被转移到其他module和包中。值得庆幸的是,像ForbesCrockford和其他人这样的人通过制作更简单的图书馆填补了教育空白。

链接好像坏了。。。
2021-03-24 11:14:49

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

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

  • 一个 Promise 会异步地得到结果。添加回调是一个透明的操作——与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;
    }
    
  • Promise 有一种then方法可以将它们链接起来。我接受一个回调并返回一个新的 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;
};
在什么情况下addCallbackPromise类中方法会被多次调用?then方法只会返回一个新Promise实例,那么为什么要在Deferred类中保留一个回调数组呢?
2021-03-17 11:14:49
我花了一些时间来理解它是如何工作的,尽管它很小并且有评论。你的版本比我的更完整,也很聪明,但也更复杂。我唯一想念的“重要”是拒绝选项和错误回调。
2021-03-26 11:14:49
@Kaizo:谢谢,没有明白 OP 专门询问了延期。添加说明并切换到延迟界面。还缺少什么相关的东西吗?
2021-04-09 11:14:49
没有解释它是如何实现的,也没有显示延迟/Promise实现。
2021-04-10 11:14:49
@Kaizo:是的,我故意省略了这一点,因为如果写得简洁,代码只会变得更加复杂和难以理解:-) 看看我的编辑......
2021-04-10 11:14:49

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

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

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

转换功能——状态转换的守门人

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

如果满足条件,此函数会更新Promise的状态和值。然后它会触发流程功能以进行进一步处理。

流程功能根据转换(例如待完成)执行正确的操作,稍后解释。

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;基础还存储了对这个创建的Promise的内部引用,因此一旦基础Promise被实现/拒绝就可以调用它。

  2. 如果基本Promise已解决(即完成或拒绝),则应立即调用适当的处理程序。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 函数时调用的。因此它需要检查挂起的Promise,因为它可能是从 then 函数中调用的。

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

  1. 使用 Utils.runAsync 帮助程序(一个围绕 setTimeout 的薄包装器(setImmediate 也可以工作))异步调用处理程序。

  2. 如果 onSuccess 和 onReject 处理程序丢失,则为它们创建回退处理程序。

  3. 根据Promise状态(例如已完成或已拒绝)选择正确的处理程序函数。

  4. 将处理程序应用于基本Promise的值。这个操作的值传递给Resolve函数来完成promise处理循环。

  5. 如果发生错误,则附加的Promise会立即被拒绝。

    function process() { var that = this,fulfillback = function(value) { return value; } }, rejectFallBack = function(reason) { throw 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);
        }
    });
    

    }

Resolve 函数——解析Promise

这可能是 promise 实现中最重要的部分,因为它处理 promise 解析。它接受两个参数——promise 和它的分辨率值。

虽然对各种可能的分辨率值进行了大量检查;有趣的解决方案有两种——涉及传入的Promise和 thenable(具有 then 值的对象)。

  1. 传递一个 Promise 值

如果分辨率值是另一个Promise,那么Promise必须采用这个分辨率值的状态。由于此解析值可以挂起或结算,因此最简单的方法是将新的 then 处理程序附加到解析值并处理其中的原始Promise。每当它解决时,原始Promise将被解决或拒绝。

  1. 传递一个 thenable 值

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

像以前一样, 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);
 }
}

Promise 构造器

而这就是将这一切结合在一起的一个。履行和拒绝函数是语法糖,通过无操作函数来解决和拒绝。

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

我希望这有助于更多地了解 Promise 的工作方式。

请在您的答案中发布博客的内容,或者至少是其要点。链接不是答案
2021-03-23 11:14:49