在功能范围之外解决Javascript Promise


279

我一直在使用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 2014年

1
就像您写的一样,这对我有效。就我而言,这是“规范”的方式。
吉拉德·巴纳

14
我认为将来应该有一种正式的方法来实现这一目标。我认为该功能非常强大,因为您可以等待其他上下文中的值。
何塞

每当他们提出适当的解决方案时,我希望他们也能使它适用于嵌套的诺言,其中某些诺言可能会再次发生。
亚瑟·塔拉索夫

我认为Promise API“建议”始终将它们用作返回值,而不要用作您可以访问或调用的对象。换句话说,强迫我们将它们视为返回值,而不是我们可以访问的对象或我们可以调用的函数或可以用变量引用或作为参数传递的对象,等等。如果您开始将promise用作其他任何对象,则可能会最终需要像您的问题一样从外部解决此问题。也就是说,我还认为应该有一种正式的方法来进行此操作……而Deferred似乎只是我的一种解决方法。
Cancerbero

Answers:


93

不,没有其他方法可以执行此操作-我唯一可以说的是这种用例不是很常见。就像费利克斯(Felix)在评论中所说-您所做的将始终如一。

值得一提的是,promise构造器以这种方式运行的原因是抛出安全性-如果在您的代码在promise构造器中运行时未预料到的异常发生,它将变成拒绝,这种形式的throw安全-将抛出的错误转换为拒绝很重要,有助于维护可预测的代码。

出于这种抛出安全的原因,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链?例如,在我的特定情况下,我在服务器上,正在等待特定的客户端回复(SYN-ACK-kinda握手以确保客户端成功更新状态)。
米2015年

1
@Domi检查q-connection和RxJS。
本杰明·格林鲍姆

2
使用提取API怎么办?
Vinod Sobale

95
不常见?我最终几乎每个项目都需要它。
托马什Zato -恢复莫妮卡

1
至于用例,请考虑在触发事件并发生其他事情之后需要执行某些操作。您想将事件转换为一个承诺,并将其与另一个承诺合并。在我看来,这是一个普遍的问题。
Gherman

130

简单:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX,正如公认的答案所提到的-它是故意设计的。关键是,如果引发异常,则将由promise构造函数捕获。这个答案(以及我的答案)都有可能为任何代码调用抛出异常promiseResolve()。一个诺言的语义是它总是返回一个值。另外,这在功能上与OP的帖子相同,我不知道这是以可重用的方式解决的问题。
乔恩·贾克斯

4
@JonJaques我不确定您所说的是否正确。调用的代码promiseResolve()不会引发异常。您可以.catch在构造函数上定义a ,无论调用什么代码,构造函数.catch都将被调用。这是jsbin演示其工作原理:jsbin.com/yicerewivo/edit?js,console
carter

是的,它被捕获是因为您在它周围包装了另一个promise构造函数-正是我要提出的观点。但是,可以说您还有一些其他代码正在尝试在构造函数(也称为Deferred对象)之外调用resolve()。它可能会引发异常并且不会被捕获jsbin.com/cokiqiwapo/1/edit?js,console
乔恩·雅克斯

8
我什至不确定这是一个不好的设计。不应在承诺之外引发错误。如果设计人员实际上希望错误会被捕获,那么这可能是误解或理解不充分的一个例子。
KalEl

3
问题中已经提到了这种确切的构造。你看过吗?
Cedric Reichenbach

103

在这里参加聚会有点晚,但是另一种方法是使用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
请注意此处的词汇作用域。
Florrie

1
resolve|reject词汇分配还是通过分配方面没有实际的区别bind。这只是jQuery Deferred对象的一个简单实现,该对象自1.0(ish)开始就存在。除了没有投掷安全性外,它的工作原理完全像一个承诺。这个问题的全部重点是在创建诺言时如何保存几行代码。
乔恩·贾克斯

1
使用延迟是执行此操作的通常方法,我不知道为什么它不更高
BlueRaja-Danny Pflughoeft

1
很好的答案!正在寻找jQuery提供的延迟功能。
Anshul Koka's

2
Deferred弃用了吗?
佩里耶

19

我在2015年针对自己的框架提出了一个解决方案。我称这种诺言任务

function createPromise(handler){
  var _resolve, _reject;

  var promise = new Promise(function(resolve, reject){
    _resolve = resolve; 
    _reject = reject;

    handler(resolve, reject);
  })

  promise.resolve = _resolve;
  promise.reject = _reject;

  return promise;
}

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

promise.resolve(200) // resolve from outside

4
谢谢,这工作。但是什么是处理程序?我必须将其删除才能正常工作。
Sahid

16

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

如果绑定thencatch随后的Deferred对象,那么它完全实现了PromiseAPI,你可以把它当作承诺,await它与这样的。

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


10

一个辅助方法将减轻这种额外的开销,并给您同样的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
    });
});

8

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

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;

}

查看完整的工作示例-

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


7

您可以将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.

6

这里的许多答案与本文的最后一个示例相似。我正在缓存多个Promises,并且resolve()and reject()函数可以分配给任何变量或属性。结果,我可以使这段代码更加紧凑:

function defer(obj) {
    obj.promise = new Promise((resolve, reject) => {
        obj.resolve = resolve;
        obj.reject  = reject;
    });
}

这是使用此版本的defer()来将FontFace负载承诺与另一个异步过程结合在一起的简化示例:

function onDOMContentLoaded(evt) {
    let all = []; // array of Promises
    glob = {};    // global object used elsewhere
    defer(glob);
    all.push(glob.promise);
    // launch async process with callback = resolveGlob()

    const myFont = new FontFace("myFont", "url(myFont.woff2)");
    document.fonts.add(myFont);
    myFont.load();
    all.push[myFont];
    Promise.all(all).then(() => { runIt(); }, (v) => { alert(v); });
}
//...
function resolveGlob() {
    glob.resolve();
}
function runIt() {} // runs after all promises resolved 

更新:如果要封装对象,有两种选择:

function defer(obj = {}) {
    obj.promise = new Promise((resolve, reject) => {
        obj.resolve = resolve;
        obj.reject  = reject;
    });
    return obj;
}
let deferred = defer();

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

如果要在异步函数中使用这些示例,const result = await deferred.promise;
则要

6

接受的答案是错误的。使用范围和引用非常容易,尽管这可能会使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);

创建诺言时,我们实质上是在获取对resolve函数的引用,然后将其返回,以便可以在外部进行设置。

一秒钟后,控制台将输出:

> foo

我认为这是最好的方法。唯一的事情是,代码可能不那么冗长。
pie6k

真好!聪明的主意。如果可以的话,+ 50。
Mitya

这就是OP所做的。实际上,您实际上是在Promises上重新发明Deferred模式,这当然是可行的,并且您的方法可行(作为初始OP代码),但这不是最佳实践,因为已接受的答案中描述了“抛出安全原因”。
dhilt

4

是的你可以。通过将CustomEventAPI用于浏览器环境。并在node.js环境中使用事件发射器项目。由于问题中的代码段是针对浏览器环境的,因此这里是一个可行的示例。

function myPromiseReturningFunction(){
  return new Promise(resolve => {
    window.addEventListener("myCustomEvent", (event) => {
       resolve(event.detail);
    }) 
  })
}


myPromiseReturningFunction().then(result => {
   alert(result)
})

document.getElementById("p").addEventListener("click", () => {
   window.dispatchEvent(new CustomEvent("myCustomEvent", {detail : "It works!"}))
})
<p id="p"> Click me </p>

我希望这个答案是有用的!


3

我们的解决方案是使用闭包存储解析/拒绝函数,并附加一个函数来扩展promise本身。

这是模式:

function getPromise() {

    var _resolve, _reject;

    var promise = new Promise((resolve, reject) => {
        _reject = reject;
        _resolve = resolve;
    });

    promise.resolve_ex = (value) => {
       _resolve(value);
    };

    promise.reject_ex = (value) => {
       _reject(value);
    };

    return promise;
}

并使用它:

var promise = getPromise();

promise.then(value => {
    console.info('The promise has been fulfilled: ' + value);
});

promise.resolve_ex('hello');  
// or the reject version 
//promise.reject_ex('goodbye');

2
太好了……我只是在学习Promises,但一直困扰着您似乎无法“在其他地方”解决它们的事实。使用闭包来隐藏实现细节是一个好主意...但是实际上我不确定那是您所做的:不是拥有“伪”私有变量,我很确定有一种方法可以完全隐藏变量这应该是无法访问的……这实际上就是关闭的含义……
mike rodent

>闭包是可以访问(并传递)访问封装范围变量的代码块。var _resolve,_reject; 是封闭范围。
史蒂芬·斯潘金

是的,足够公平。实际上,在我看来,我的答案过于复杂,而且您的答案可以简化:您只需要继续promise.resolve_ex = _resolve; promise.reject_ex = _reject;...仍然可以正常工作。
麦克啮齿动物

附加功能以扩展承诺本身。 ”-不要那样做。承诺是结果值,它们不应该提供解决它们的能力。您不想传递那些扩展的内容。
Bergi

2
问题是如何在范围之外解决它。这是一个可行的解决方案,在我们的生产中,我们实际上有必要这样做。我不明白为什么解决上述问题值得一票。
史蒂芬·斯潘金

2

我发现自己在某些情况下也缺少Deferred模式。您始终可以在ES6 Promise的顶部创建一个:

export default class Deferred<T> {
    private _resolve: (value: T) => void = () => {};
    private _reject: (value: T) => void = () => {};

    private _promise: Promise<T> = new Promise<T>((resolve, reject) => {
        this._reject = reject;
        this._resolve = resolve;
    })

    public get promise(): Promise<T> {
        return this._promise;
    }

    public resolve(value: T) {
        this._resolve(value);
    }

    public reject(value: T) {
        this._reject(value);
    }
}

2

感谢在此主题中发布的每个人。我创建了一个模块,其中包括前面所述的Defer()对象以及在其上构建的其他一些对象。它们都利用Promises和简洁的Promise回调语法在程序内实现通信/事件处理。

  • 推迟:可以远程解决的承诺失败(在主体外部)
  • 延迟:在给定时间后自动解决的承诺
  • 超时:在给定时间后自动失败的承诺。
  • 周期:可重触发的Promise语法来管理事件
  • 队列:基于Promise链接的执行队列。

    rp = require("repeatable-promise")

    https://github.com/CABrouwers/repeatable-promise


1

我为此写了一个小库。https://www.npmjs.com/package/@inf3rno/promise.exposed

我用的是工厂方法的方法别人写的,但我推翻了thencatchfinally方法也一样,这样你就可以解决这些以及原来的承诺。

在没有外部执行者的情况下解决Promise:

const promise = Promise.exposed().then(console.log);
promise.resolve("This should show up in the console.");

从外部与执行者的setTimeout赛跑:

const promise = Promise.exposed(function (resolve, reject){
    setTimeout(function (){
        resolve("I almost fell asleep.")
    }, 100000);
}).then(console.log);

setTimeout(function (){
    promise.resolve("I don't want to wait that much.");
}, 100);

如果您不想污染全局名称空间,则有一个无冲突模式:

const createExposedPromise = require("@inf3rno/promise.exposed/noConflict");
const promise = createExposedPromise().then(console.log);
promise.resolve("This should show up in the console.");

1

我制作了一个名为的库manual-promise,该库可以代替PromisePromise由于使用代理或包装器,此处的其他答案都不能作为替代的替代品。

yarn add manual-promise

npn install manual-promise


import { ManualPromise } from "manual-promise";

const prom = new ManualPromise();

prom.resolve(2);

// actions can still be run inside the promise
const prom2 = new ManualPromise((resolve, reject) => {
    // ... code
});


new ManualPromise() instanceof Promise === true

https://github.com/zpxp/manual-promise#readme


0

如何创建劫持拒绝并返回的函数?

function createRejectablePromise(handler) {
  let _reject;

  const promise = new Promise((resolve, reject) => {
    _reject = reject;

    handler(resolve, reject);
  })

  promise.reject = _reject;
  return promise;
}

// Usage
const { reject } = createRejectablePromise((resolve) => {
  setTimeout(() => {
    console.log('resolved')
    resolve();
  }, 2000)

});

reject();

0

我已经汇总了完成该工作的要点:https : //gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

使用方法如下:

import ExternalizedPromiseCreator from '../externalized-promise';

describe('ExternalizedPromise', () => {
  let fn: jest.Mock;
  let deferredFn: jest.Mock;
  let neverCalledFn: jest.Mock;
  beforeEach(() => {
    fn = jest.fn();
    deferredFn = jest.fn();
    neverCalledFn = jest.fn();
  });

  it('resolve should resolve the promise', done => {
    const externalizedPromise = ExternalizedPromiseCreator.create(() => fn());

    externalizedPromise
      .promise
      .then(() => deferredFn())
      .catch(() => neverCalledFn())
      .then(() => {
        expect(deferredFn).toHaveBeenCalled();
        expect(neverCalledFn).not.toHaveBeenCalled();
        done();
      });

    expect(fn).toHaveBeenCalled();
    expect(neverCalledFn).not.toHaveBeenCalled();
    expect(deferredFn).not.toHaveBeenCalled();

    externalizedPromise.resolve();
  });
  ...
});

0

首先在浏览器或节点上启用--allow-natives-syntax

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

onClick = function () {
    %ResolvePromise(p, value)
}

0

另一个从外部解决Promise的解决方案

 class Lock {
        #lock;  // Promise to be resolved (on  release)
        release;  // Release lock
        id;  // Id of lock
        constructor(id) {
            this.id = id
            this.#lock = new Promise((resolve) => {
                this.release = () => {
                    if (resolve) {
                        resolve()
                    } else {
                        Promise.resolve()
                    }
                }
            })
        }
        get() { return this.#lock }
    }

用法

let lock = new Lock(... some id ...);
...
lock.get().then(()=>{console.log('resolved/released')})
lock.release()  // Excpected 'resolved/released'
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.