是如何实现一个缺陷/延迟库的?

如何实现像 这样的承诺/延迟库?我试图阅读源代码,但发现它非常难以理解,所以我想如果有人可以向我解释,从高层次,是什么技术用于实现单线程 JS 环境的承诺,如节点和浏览器。

20585 次浏览

我发现解释起来比演示一个例子更困难,所以这里有一个非常简单的实现推迟/承诺可能是什么。

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

转到 创建类和示例部分查看完整的实现。

承诺:

首先,我们需要创建一个承诺对象,其中包含一个回调数组。我将开始处理对象,因为它更清晰:

var promise = {
callbacks: []
}

now add callbacks with the method then:

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

请注意,回调是在超时时间内调用的,以允许代码始终是异步的。

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

创建类和示例:

现在让我们把这两个对象都转换成类,首先是承诺:

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

要了解更多关于规范的信息,请注意主库(Q、 when. js、 rsvp.js、 node-晃动,...)遵循 承诺/答应规范。

希望我说得够清楚了。

编辑:

正如评论中提到的,我在这个版本中增加了两点:

  • 无论承诺的地位如何,都有可能实现。
  • 连锁承诺的可能性。

为了能够在解决问题时调用承诺,需要将状态添加到承诺中,并在调用状态时检查该状态。如果状态被解析或拒绝,只需执行带有其数据或错误的回调。

为了能够连接承诺,您需要为每个对 then的调用生成一个新的延迟,并且,当承诺被解决/拒绝时,解决/拒绝新的承诺,并且回调的结果。因此,当承诺完成时,如果回调返回一个新的承诺,那么它就与 then()返回的承诺绑定在一起。如果没有,则使用回调的结果解决承诺。

承诺如下:

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

如你所见,它已经长大了不少。

首先确保你明白承诺是如何起作用的。看看 承诺提案承诺/A + 规范

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

  • 结果确实会异步地解析一个  。添加回调是一个透明的操作——与承诺是否已经解决无关,一旦结果可用,它们将被调用。

    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;
    }
    
  • Promises have a then method that allows chaining them. I takes a callback and returns a new Promise which will get resolved with the result of that callback after it was invoked with the first promise's result. If the callback returns a Promise, it will get assimilated instead of getting nested.

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

Further concepts would be maintaining a separate error state (with an extra callback for it) and catching exceptions in the handlers, or guaranteeing asynchronity for the callbacks. Once you add those, you've got a fully functional Promise implementation.

Here is the error thing written out. It unfortunately is pretty repetitive; you can do better by using extra closures but then it get's really really hard to understand.

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

就实现而言,Q 是一个非常复杂的承诺库,因为它旨在支持流水线和 RPC 类型场景。我有自己的非常基本的 承诺/A + 规范 给你的实现。

原则上很简单。在确定/解决承诺之前,您通过将任何回调或错误推入数组来保存它们的记录。当承诺达成时,你可以调用适当的回调或重复,并记录承诺达成的结果(以及是否实现或被拒绝)。解决此问题之后,您只需调用带有存储结果的回调或 errbacks。

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

如果您对开发完全承诺实现(支持 RPC 和 Q 之类的流水线)背后的原因感兴趣,可以阅读 kriskowal 的推理 给你。如果你正在考虑兑现承诺的话,这是一个非常好的渐进式方法,我无法给你足够的推荐。它可能值得一读,即使你只是要使用一个承诺库。

正如福布斯在他的回答中所提到的,我记录了许多设计决策,这些决策都与构建像 Q 这样的库有关,这里是 https://github.com/kriskowal/q/tree/v1/design。简而言之,承诺库有不同的级别,许多库停留在不同的级别。

在承诺/A + 规范所捕获的第一个层次上,承诺是最终结果的代理,适合于管理 “本地异步”。也就是说,它适合于确保工作按正确的顺序进行,并确保简单而直接地听取一项操作的结果,而不管它是否已经解决或将在未来发生。它还使一方或多方订阅最终结果变得同样简单。

Q,正如我已经实现的那样,提供了一些承诺,它们是最终、远程或最终 + 远程结果的代理。为了达到这个目的,它的设计是颠倒的,对于承诺有不同的实现ーー延迟承诺、实现承诺、拒绝承诺和远程对象的承诺(最后一个在 Q-Connection 中实现)。它们都共享相同的接口,并通过发送和接收诸如“ then”(这足以满足承诺/A +)以及“ get”和“调用”之类的消息来工作。因此,Q 大约是 “分布式异步”,并且存在于另一个层上。

然而,Q 实际上是从更高的层次上下来的,在这个层次上,承诺被用来管理 相互怀疑的一方之间的分布式异步,比如你、商家、银行、 Facebook、政府ーー不是敌人,甚至可能是朋友,但有时会有利益冲突。我实现的 Q 被设计成与强化的安全承诺兼容(这就是分离 promiseresolve的原因) ,希望它能引导人们接受承诺,训练他们使用这个 API,并允许他们在将来需要在安全 mashup 中使用承诺时带上自己的代码。

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

所有这些说明,没有理由难以阅读。Domenic 和我正在开发 Q 的一个版本,它将更加模块化和易于使用,其中的一些分散注意力的依赖关系和解决方案将移植到其他模块和软件包中。值得庆幸的是,像 福布斯克罗克福德和其他人已经通过建立更简单的库来填补教育空白。

你可能想看看 Adehun 的 博客文章

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

免责声明 : 我写了这篇博客文章,但是这篇博客文章确实解释了关于 Adehun 的一切。

转换函数-状态转换的守门人

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

如果满足条件,此函数将更新承诺的状态和值。然后触发 process 函数进行进一步处理。

流程函数根据转换执行正确的操作(例如挂起到完成) ,稍后将对此进行解释。

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


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

Then 函数

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

  1. 基本承诺(然后被调用的那个承诺)需要使用传入的处理程序创建一个新承诺; 基本承诺还存储对这个创建的承诺的内部引用,以便在基本承诺实现/拒绝后可以调用它。

  2. 如果基本承诺已经解决(例如,实现或拒绝) ,那么应该立即调用适当的处理程序。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 函数调用的。

流程对所有内部存储的承诺(即那些通过 then 函数附加到基础承诺的承诺)运行承诺决议过程,并强制执行以下的承诺/A + 要求:

  1. 使用 Utils.runAsync 助手异步调用处理程序(setTimeout 周围的一个瘦包装器(setInstate 也可以工作))。

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

  3. 根据承诺状态选择正确的处理函数,例如,完成或拒绝。

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

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

    函数进程() 这个, Fulfill FallBack = 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);
    }
    });
    

    }

解决功能——解决承诺

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

虽然对各种可能的解析值有很多检查,但有趣的解析场景有两种——一种是传入一个承诺,另一种是可变对象(一个具有 then 值的对象)。

  1. 传入一个允诺值

如果分辨率值是另一个承诺,则承诺必须采用该分辨率值的状态。由于这个解析值可以挂起或解决,所以最简单的方法是将一个新的 then 处理程序附加到解析值,并处理其中的原始承诺。无论何时解决,原来的承诺都将被解决或拒绝。

  1. 传入可变的值

这里需要注意的是,可变值的 then 函数必须只调用一次(对于函数式编程中的曾经的包装器来说,这是一个很好的用法)。同样,如果 then 函数的检索抛出一个 Exception,则承诺将立即被拒绝。

和以前一样,then 函数也是通过最终解析或拒绝承诺的函数来调用的,但这里的区别在于调用的标志是在第一次调用时设置的,并将后续调用转换为 no ops。

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

我希望这有助于更多地揭示承诺的工作方式。