在Promise构造函数范围之外解决Javascript Promise

513

我一直在使用ES6 Promise。

通常,Promise的构造和使用方式如下所示:

new Promise(function(resolve, reject){
    if (someCondition){
        resolve();
    } else {
        reject();
    } 
});

但是我一直在做以下类似的事情,为了灵活性而将解决方案转移到外部。

var outsideResolve;
var outsideReject;
new Promise(function(resolve, reject) { 
    outsideResolve = resolve; 
    outsideReject = reject; 
});

之后

onClick = function(){
    outsideResolve();
}

这个方法可以正常工作,但有没有更简单的方法?如果没有,这是一个好的做法吗?


2
我认为没有其他方法。我相信规定Promise传递的回调必须同步执行,以允许“导出”这两个函数。 - Felix Kling
1
这对我来说完全像你写的那样有效。就我而言,这是“规范”的方式。 - Gilad Barner
47
我认为未来应该有一种正式的方式来实现这一点。在我看来,这个功能非常强大,因为你可以等待来自其他上下文的值。 - Jose
1
我认为Promise API“建议”我们始终将它们用作返回值,而不是可以访问或调用的对象。换句话说,强制我们将它们视为返回值,而不是我们可以访问或调用的对象、我们可以使用变量引用或作为参数传递的函数等等。如果您开始像使用其他对象一样使用promise,那么您最终可能需要从外部解决它,就像在您的问题中一样... 话虽如此,我也认为应该有一种正式的方法来做到这一点... 而Deferred对我来说只是一个解决方法。 - cancerbero
@Jose 对我来说,有几次它非常有用,当我需要一个 Promise 在另一个 Promise 之后立即解决时,在 Promise.all 或 Promise.race 不起作用的情况下。 - JulianSoto
显示剩余2条评论
27个回答

264

简单:

var promiseResolve, promiseReject;

var promise = new Promise(function(resolve, reject){
  promiseResolve = resolve;
  promiseReject = reject;
});

promiseResolve();

3
作为被接受的回答所提到的,这个设计是故意这样的。关键在于,如果抛出异常,它将被 promise 构造函数捕获。这个答案(以及我的)可能会有一个缺陷,即对于调用 promiseResolve() 的任何代码可能会抛出异常。Promise 的语义是它 总是 返回一个值。此外,从功能上讲,这与 OP 的帖子相同,我不明白这解决了什么可重用的问题。 - Jon Jaques
5
@JonJaques 我不确定你所说的是否正确。调用promiseResolve()的代码不会引发异常。您可以在构造函数上定义.catch,无论什么代码调用它,构造函数的.catch都将被调用。这是演示其工作原理的jsbin链接:https://jsbin.com/yicerewivo/edit?js,console - carter
3
没错,这是因为你在它周围又包装了一个 Promise 构造函数 - 这正是我试图说明的要点。然而,假设你有一些其他的代码尝试在构造函数外部调用 resolve() ... 它可能会抛出异常并且无法被捕获。 https://jsbin.com/cokiqiwapo/1/edit?js,console - Jon Jaques
13
我甚至不确定这是否是个糟糕的设计。在承诺外抛出的错误不应在承诺内被捕获。如果设计者实际上_期望_错误在承诺内被捕获,那么这可能是误解或不良理解的例子。 - KalEl
57
这个确切的结构在问题中已经提到了,你有阅读过吗? - Cedric Reichenbach
显示剩余3条评论

162
有点晚了,但另一种方法是使用Deferred对象。基本上你需要相同数量的样板文件,但如果你想要传递它们并可能在定义之外解决它们,这很方便。
天真的实现:
class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject)=> {
      this.reject = reject
      this.resolve = resolve
    })
  }
}

function asyncAction() {
  var dfd = new Deferred()

  setTimeout(()=> {
    dfd.resolve(42)
  }, 500)

  return dfd.promise
}

asyncAction().then(result => {
  console.log(result) // 42
})

ES5版本:

function Deferred() {
  var self = this;
  this.promise = new Promise(function(resolve, reject) {
    self.reject = reject
    self.resolve = resolve
  })
}

function asyncAction() {
  var dfd = new Deferred()

  setTimeout(function() {
    dfd.resolve(42)
  }, 500)

  return dfd.promise
}

asyncAction().then(function(result) {
  console.log(result) // 42
})

1
请注意这里的词法作用域。 - Nebula
4
使用“deferred”是通常的做法,我不知道为什么这个方法没有更高的评价。 - BlueRaja - Danny Pflughoeft
2
非常好的答案!我正在寻找jQuery提供的延迟功能。 - Anshuul Kai
2
Deferred 已经被弃用了吗? - Pacerier
1
不知道现在是否被接受,但它很好用,所以我没问题。到目前为止还没有出现过任何问题。 - beatcoder
显示剩余5条评论

126

不,没有其他方法可以做到这一点——我唯一能说的是,这种用例并不常见。就像Felix在评论中所说的那样,你所做的事情将始终有效。

值得一提的是,promise构造函数表现出这种方式是为了避免抛出异常——如果你的代码在promise构造函数内运行时发生了你没有预料到的异常,它会变成一个拒绝状态。这种抛出安全性——将抛出的错误转换为拒绝状态——是重要的,有助于保持可预测的代码。

基于这种抛出安全性的原因,promise构造函数被选择而不是deferreds(一种可实现您正在进行的操作的替代promise构建方式)——至于最佳实践——我会传递元素并使用promise构造函数:

var p = new Promise(function(resolve, reject){
    this.onclick = resolve;
}.bind(this));

因此,每当你可以使用 Promise 构造函数而不是导出函数时,我建议你使用它。如果两者都能避免,那就都避免并链式调用。

请注意,像 if(condition) 这样的事情永远不应该使用 Promise 构造函数,第一个示例可以编写为:

var p = Promise[(someCondition)?"resolve":"reject"]();

2
嗨,本杰明!如果我们还不知道承诺何时得以实现,那么有没有更好的获取美味Promise Sugar的方法?例如某种类似于异步等待/通知模式的东西?比如说,“store”并稍后调用Promise链?例如,在我的特定情况下,我正在服务器上等待特定客户端的回复(一种SYN-ACK类型的握手,以确保客户端成功更新状态)。 - Domi
2
我该如何使用fetch API来实现同样的功能? - Vinod Sobale
154
不常见?我几乎每个项目都需要用到它。 - Tomáš Zato
2
@BenjaminGruenbaum - 另一个用例是如果您正在与Web Worker通信。如果您希望通过Web Worker消息(以未知顺序)接收多个信息片段,则最好为每个信息片段创建一个Promise p [i],以便该信息的消费者可以等待该Promise或通过p [i] .then(callme)注册回调函数。此Promise需要由worker.onmessage解决,而不是由在Promise创建时提供的代码解决(或由worker.onerror中的代码拒绝)。基本上,任何时候异步过程触发多个无序回调,您都需要OP所说的内容。 - Matt
6
如果你能这样做,那将非常方便:var p = new Promise(); p.resolve() - br4nnigan
显示剩余12条评论

38

我喜欢@JonJaques的答案,但我想更进一步。

如果你将thencatch绑定到Deferred对象上,那么它就完全实现了Promise API,你可以把它当作Promise并使用await等方式处理它。

⚠️编辑注:我不再推荐这种模式,因为在写作时,Promise.prototype.finally还不存在,然后它成为了一个存在的方法... 这种情况可能会发生在其他方法中,所以我建议您改为使用resolvereject函数来增强promise实例:

function createDeferredPromise() {
  let resolve
  let reject

  const promise = new Promise((thisResolve, thisReject) => {
    resolve = thisResolve
    reject = thisReject
  })

  return Object.assign(promise, {resolve, reject})
}

支持一下其他人的答案吧。

class DeferredPromise {
  constructor() {
    this._promise = new Promise((resolve, reject) => {
      // assign the resolve and reject functions to `this`
      // making them usable on the class instance
      this.resolve = resolve;
      this.reject = reject;
    });
    // bind `then` and `catch` to implement the same interface as Promise
    this.then = this._promise.then.bind(this._promise);
    this.catch = this._promise.catch.bind(this._promise);
    this.finally = this._promise.finally.bind(this._promise);
    this[Symbol.toStringTag] = 'Promise';
  }
}

const deferred = new DeferredPromise();
console.log('waiting 2 seconds...');
setTimeout(() => {
  deferred.resolve('whoa!');
}, 2000);

async function someAsyncFunction() {
  const value = await deferred;
  console.log(value);
}

someAsyncFunction();


我真的很喜欢这个。谢谢你。我正在将其用作我的Express应用程序中的自定义定义组件,但如果您愿意创建一个NPM模块,那将是非常棒的,或者如果需要的话,我也可以创建。这种方法是新的async / await和旧的Parse平台处理promises的巧妙结合。https://en.wikipedia.org/wiki/Parse_(platform) - Michael Kubler
1
不要忘记 Promise.prototype.finally - Константин Ван
很好的发现@КонстантинВан,我已经有一段时间没有看到这个答案了,我不再推荐这个了。我已经更新了答案以反映最新情况。 - Rico Kahler
如果你担心 Promise 方法未来可能发生的变化,你也可以通过循环遍历 Promise 的属性来泛化映射工作,对吧? - Константин Ван

30

2015年我为自己的框架想出了一种解决方案。我把这种承诺称为任务

function createPromise(handler){
  var resolve, reject;

  var promise = new Promise(function(_resolve, _reject){
    resolve = _resolve; 
    reject = _reject;
    if(handler) handler(resolve, reject);
  })
  
  promise.resolve = resolve;
  promise.reject = reject;
  return promise;
}


// create
var promise = createPromise()
promise.then(function(data){ alert(data) })

// resolve from outside
promise.resolve(200)

5
谢谢,这个方法有效。但是什么是handler?我不得不删除它才能让它正常工作。 - Sahid
@Sahid 当你运行 createPromise() 函数时,你需要将一个函数作为参数传递给它,否则代码将无法正常工作。你可以使用 if 语句来检查处理程序参数的有效性,然后再调用它。 - Michael Mammoliti
谢谢你的代码!但是,在回调函数设置它之前,其他代码是否有可能调用你的.resolve呢?我习惯于常规线程,而不是异步事件,所以可能会有点困惑。 - adentinger

22

采纳的答案是错误的。使用作用域和引用很容易,尽管这可能会让 Promise 纯粹主义者 愤怒:

const createPromise = () => {
    let resolver;
    return [
        new Promise((resolve, reject) => {
            resolver = resolve;
        }),
        resolver,
    ];
};

const [ promise, resolver ] = createPromise();
promise.then(value => console.log(value));
setTimeout(() => resolver('foo'), 1000);

我们基本上是在创建 Promise 时获取 resolve 函数的引用,然后将其返回以便可以在外部设置。

一秒钟后控制台将输出:

> foo

我认为这是最好的方法。唯一的问题是代码可能有点啰嗦。 - Adam Pietrasiak
不错!聪明的想法。如果可以的话,加50分。 - Mitya
这就是OP所做的事情。实际上,您正在重新发明Promises上的Deferred模式,当然,这是可能的,并且您的方法可以工作(就像最初的OP代码),但由于“抛出安全原因”在接受的答案中描述,这不是最佳实践。 - dhilt

16

如果有人在寻找简化此任务的typescript版本,请参考以下工具:

export const deferred = <T>() => {
  let resolve!: (value: T | PromiseLike<T>) => void;
  let reject!: (reason?: any) => void;
  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return {
    resolve,
    reject,
    promise,
  };
};

这可以被用于例如:
const {promise, resolve} = deferred<string>();

promise.then((value) => console.log(value)); // nothing

resolve('foo'); // console.log: foo


15

一个帮助方法可以减轻这个额外的开销,并让您获得相同的 jQuery 感觉。

function Deferred() {
    let resolve;
    let reject;
    const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
    });
    return { promise, resolve, reject };
}

使用方式将会是:

const { promise, resolve, reject } = Deferred();
displayConfirmationDialog({
    confirm: resolve,
    cancel: reject
});
return promise;

类似于jQuery

const dfd = $.Deferred();
displayConfirmationDialog({
    confirm: dfd.resolve,
    cancel: dfd.reject
});
return dfd.promise();
虽然在这种简单的情况下,原生语法是可以接受的。
return new Promise((resolve, reject) => {
    displayConfirmationDialog({
        confirm: resolve,
        cancel: reject
    });
});

13

我正在使用一个辅助函数来创建我所谓的“扁平承诺” -

function flatPromise() {

    let resolve, reject;

    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });

    return { promise, resolve, reject };
}

而我是这样使用它的 -

function doSomethingAsync() {

    // Get your promise and callbacks
    const { resolve, reject, promise } = flatPromise();

    // Do something amazing...
    setTimeout(() => {
        resolve('done!');
    }, 500);

    // Pass your promise to the world
    return promise;

}
请提供需要翻译的完整内容,以便我能够为您提供准确的翻译。
function flatPromise() {

    let resolve, reject;

    const promise = new Promise((res, rej) => {
        resolve = res;
        reject = rej;
    });

    return { promise, resolve, reject };
}

function doSomethingAsync() {
    
    // Get your promise and callbacks
    const { resolve, reject, promise } = flatPromise();

    // Do something amazing...
    setTimeout(() => {
        resolve('done!');
    }, 500);

    // Pass your promise to the world
    return promise;
}

(async function run() {

    const result = await doSomethingAsync()
        .catch(err => console.error('rejected with', err));
    console.log(result);

})();

编辑: 我创建了一个名为flat-promise的NPM包,该代码也可以在GitHub上获得


13

你可以将Promise包装在一个类中。

class Deferred {
    constructor(handler) {
        this.promise = new Promise((resolve, reject) => {
            this.reject = reject;
            this.resolve = resolve;
            handler(resolve, reject);
        });

        this.promise.resolve = this.resolve;
        this.promise.reject = this.reject;

        return this.promise;
    }
    promise;
    resolve;
    reject;
}

// How to use.
const promise = new Deferred((resolve, reject) => {
  // Use like normal Promise.
});

promise.resolve(); // Resolve from any context.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接