创建一个(ES6)承诺而不开始解决它

使用 ES6承诺,如何在不定义解决它的逻辑的情况下创建承诺?下面是一个基本的例子(一些 TypeScript) :

var promises = {};
function waitFor(key: string): Promise<any> {
if (key in promises) {
return promises[key];
}
var promise = new Promise(resolve => {
// But I don't want to try resolving anything here :(
});


promises[key] = promise;
return promise;
}


function resolveWith(key: string, value: any): void {
promises[key].resolve(value); // Not valid :(
}

使用其他承诺库很容易做到这一点,例如 JQuery:

var deferreds = {};
function waitFor(key: string): Promise<any> {
if (key in promises) {
return deferreds[key].promise();
}
var def = $.Deferred();
deferreds[key] = def;
return def.promise();
}


function resolveWith(key: string, value: any): void {
deferreds[key].resolve(value);
}

我能想到的唯一方法就是把这个函数存储在承诺执行者的某个地方,但是这看起来很混乱,而且我不确定这个函数是在什么时候运行的——它总是在构造的时候立即运行吗?

谢谢。

41850 次浏览

Good question!

The resolver passed to the promise constructor intentionally runs synchronous in order to support this use case:

var deferreds = [];
var p = new Promise(function(resolve, reject){
deferreds.push({resolve: resolve, reject: reject});
});

Then, at some later point in time:

 deferreds[0].resolve("Hello"); // resolve the promise with "Hello"

The reason the promise constructor is given is that:

  • Typically (but not always) resolution logic is bound to the creation.
  • The promise constructor is throw safe and converts exceptions to rejections.

Sometimes it doesn't fit and for that it the resolver runs synchronously. Here is related reading on the topic.

How about a more comprehensive approach?

You could write a Constructor that returns a new Promise decorated with .resolve() and .reject() methods.

You would probably choose to name the constructor Deferred - a term with a lot of precedence in [the history of] javascript promises.

function Deferred(fn) {
fn = fn || function(){};


var resolve_, reject_;


var promise = new Promise(function(resolve, reject) {
resolve_ = resolve;
reject_ = reject;
fn(resolve, reject);
});


promise.resolve = function(val) {
(val === undefined) ? resolve_() : resolve_(val);
return promise;//for chainability
}
promise.reject = function(reason) {
(reason === undefined) ? reject_() : reject_(reason);
return promise;//for chainability
}
promise.promise = function() {
return promise.then(); //to derive an undecorated promise (expensive but simple).
}


return promise;
}

By returning a decorated promsie rather than a plain object, all the promise's natural methods/properties remain available in addition to the decorations.

Also, by handling fn, the revealer pattern remains availble, should you need/choose to use it on a Deferred.

DEMO

Now, with the Deferred() utility in place, your code is virtually identical to the jQuery example.

var deferreds = {};
function waitFor(key: string): Promise<any> {
if (key in promises) {
return deferreds[key].promise();
}
var def = Deferred();
deferreds[key] = def;
return def.promise();
}

I want to add my 2 cents here. Considering exactly the question "Creating a es6 Promise without starting resolve it" I solved it creating a wrapper function and calling the wrapper function instead. Code:

Let's say we have a function f which returns a Promise

/** @return Promise<any> */
function f(args) {
return new Promise(....)
}


// calling f()
f('hello', 42).then((response) => { ... })

Now, I want to prepare a call to f('hello', 42) without actually solving it:

const task = () => f('hello', 42) // not calling it actually


// later
task().then((response) => { ... })

Hope this will help someone :)


Referencing Promise.all() as asked in the comments (and answered by @Joe Frambach), if I want to prepare a call to f1('super') & f2('rainbow'), 2 functions that return promises

const f1 = args => new Promise( ... )
const f2 = args => new Promise( ... )


const tasks = [
() => f1('super'),
() => f2('rainbow')
]


// later
Promise.all(tasks.map(t => t()))
.then(resolvedValues => { ... })

Things are slowly getting better in JavaScript land, but this is one case where things are still unnecessarily complicated. Here's a simple helper to expose the resolve and reject functions:

Promise.unwrapped = () => {
let resolve, reject, promise = new Promise((_resolve, _reject) => {
resolve = _resolve, reject = _reject
})
promise.resolve = resolve, promise.reject = reject
return promise
}


// a contrived example


let p = Promise.unwrapped()
p.then(v => alert(v))
p.resolve('test')

Apparently there used to be a Promise.defer helper, but even that insisted on the deferred object being separate from the promise itself...

What makes this kind of issues look complicated is the procedural nature of Javascript I guess. To resolve this, I created a simple class. Here how it looks:

class PendingPromise {


constructor(args) {
this.args = args;
}


execute() {
return new Promise(this.args);
}
}

This promise will only be executed when you call execute(). For example:

function log() {
return new PendingPromise((res, rej) => {
console.log("Hello, World!");
});
}


log().execute();

CPomise allows you to resolve your promises outside, but this is an antipattern since it breaks Promise incapsulation model. (Live demo)

import CPromise from "c-promise2";


const promise = new CPromise(() => {});


promise.then((value) => console.log(`Done: ${value}`)); //123


setTimeout(() => promise.resolve(123));