Разрешить Javascript Promise вне области действия функции


280

Я использую ES6 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должен выполняться синхронно, чтобы можно было «экспортировать» две функции.
Феликс Клинг

1
Это работает для меня так же, как вы написали это. Насколько я понимаю, это «канонический» путь.
Гилад Барнер

14
Я думаю, что должен быть формальный способ достичь этого в будущем. На мой взгляд, эта функция очень мощная, так как вы можете ждать значений из других контекстов.
Хосе

Всякий раз, когда они находят правильное решение этой проблемы, я надеюсь, что они также заставят ее работать для вложенных обещаний, некоторые из которых могут повториться.
Артур Тарасов

Я думаю, что Promise API «предлагает» всегда использовать их как возвращаемые значения, а не как объекты, к которым вы можете обращаться или вызывать. Другими словами, вынуждают нас обращаться с ними как с возвращаемыми значениями, а не с объектами, к которым мы можем обращаться, или с функциями, которые мы можем вызывать, или с тем, что мы можем ссылаться на переменную или передавать в качестве параметра и т. Д. в конечном итоге нужно решить ее извне, как в вашем вопросе ... При этом я также думаю, что должен быть формальный способ сделать это ... и Отложенный кажется мне просто обходным путем.
Cancebero

Ответы:


93

Нет, другого способа сделать это нет - единственное, что я могу сказать, это то, что этот вариант использования не очень распространен. Как сказал Феликс в комментарии - то, что вы делаете, будет работать последовательно.

Стоит отметить, что причина, по которой конструктор обещания ведет себя таким образом, заключается в безопасности выброса - если исключение, которое вы не ожидали, произойдет, когда ваш код выполняется внутри конструктора обещания, оно превратится в отклонение, эта форма безопасности броска - преобразование брошенных ошибок в отклонение важно и помогает поддерживать предсказуемый код.

По этой причине безопасности броска конструктор обещания был выбран из отложенных (которые являются альтернативным способом построения обещания, позволяющим делать то, что вы делаете), - что касается передовых методов - я бы пропустил элемент и вместо этого использовал конструктор обещания:

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

По этой причине - всякий раз, когда вы можете использовать конструктор обещаний вместо экспорта функций - я рекомендую вам использовать его. Всякий раз, когда вы можете избежать обоих - избегайте обоих и цепей.

Обратите внимание, что вы никогда не должны использовать конструктор обещания для таких вещей, как if(condition)первый пример:

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

2
Привет, Бенджамин! В настоящее время нет лучшего способа получить вкусное обещание сахара, если мы еще не знаем, когда обещание будет выполнено? Как какой-то асинхронный шаблон ожидания / уведомления ? Как, например, «магазин», а позже вызвать Promiseцепочку? Например, в моем конкретном случае я нахожусь на сервере, ожидая ответа конкретного клиента (SYN-ACK - своего рода рукопожатие, чтобы убедиться, что клиент успешно обновил состояние).
Доми

1
@Domi проверь q-соединение и RxJS.
Бенджамин Грюнбаум

2
Как я могу сделать то же самое, используя API выборки?
Винод Собале

97
Необычно? Я нуждаюсь в этом почти в каждом проекте.
Томаш Зато - Восстановить Монику

1
Что касается варианта использования, подумайте, что вам нужно что-то делать после того, как событие инициировано и что-то еще произошло. Вы хотите превратить событие в обещание и объединить его с другим обещанием. Похоже, общая проблема для меня.
Герман

130

просто:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX, как говорится в принятом ответе - так было задумано специально. Дело в том, что если выдается исключение, оно будет перехвачено конструктором обещания. Этот ответ (как и мой) имеет опасность того, что он может вызвать исключение для любых вызовов кода promiseResolve(). Семантика обещания состоит в том, что оно всегда возвращает значение. Кроме того, это функционально так же, как пост ОП, я не понимаю, какую проблему это решает многократно.
Джон Жак

4
@JonJaques Я не уверен, правда ли то, что ты говоришь. Код, который вызывает promiseResolve(), не будет генерировать исключение. Вы можете определить .catchконструктор, и независимо от того, какой код его вызывает, конструктор .catchбудет вызван. Вот jsbin демонстрирующее , как это работает: jsbin.com/yicerewivo/edit?js,console
картер

Да, он пойман, потому что вы обернули вокруг него еще один конструктор обещаний - именно то, что я пытаюсь сделать. Однако, допустим, у вас есть какой-то другой код, который пытается вызвать resol () вне конструктора (он же Deferred object) ... Он может вызвать исключение и не быть пойманным jsbin.com/cokiqiwapo/1/edit?js,console
Джон Жак

8
Я даже не уверен, что это плохой дизайн. Ошибка, выданная вне обещания, не должна быть поймана в обещании. Возможно, это пример неправильного представления или плохого понимания, если дизайнер действительно ожидает, что ошибка будет обнаружена внутри.
KalEl

3
Эта точная конструкция уже упоминается в вопросе. Вы даже читали это?
Седрик Райхенбах

103

Немного опоздал на вечеринку, но другой способ сделать это - использовать отложенный объект. По сути, у вас такое же количество шаблонов, но это удобно, если вы хотите передать их и, возможно, решить за пределами их определения.

Наивная реализация:

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
Обратите внимание на лексическую область видимости здесь.
Флорри

1
Нет никакой практической разницы в том resolve|reject, назначены ли лексически или через bind. Это просто простая реализация отложенного объекта jQuery , существующего с версии 1.0 (ish). Это работает так же, как обещание, за исключением того, что нет безопасности броска. Весь вопрос в том, как сохранить несколько строк кода при создании обещаний.
Джон Жак

1
Использование отложенного является обычным способом сделать это, я понятия не имею, почему это не выше
BlueRaja - Danny Pflughoeft

1
Отличный ответ! Искал отложенную функциональность, которую предлагает jQuery.
Аншул Кока

2
Не Deferredрекомендуется?
Pacerier

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
Спасибо, это сработало. Но что такое обработчик? Я должен был удалить это, чтобы заставить это работать.
Сахид

16

Мне понравился ответ @JonJaques, но я хотел сделать еще один шаг вперед.

Если вы связываете, thenа catchзатем 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;

}

Смотрите полный рабочий пример -

Редактировать: я создал пакет NPM под названием « плоское обещание», и код также доступен на GitHub .


7

Вы можете обернуть Обещание в классе.

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

Многие из ответов здесь аналогичны последнему примеру в этой статье . Я кэширование нескольких Обещания, а resolve()и reject()функции могут быть назначены на любой переменной или свойства. В результате я могу сделать этот код немного более компактным:

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

Вот упрощенный пример использования этой версии defer()для объединения FontFaceзагрузки Promise с другим асинхронным процессом:

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 

Обновление: 2 варианта, если вы хотите инкапсулировать объект:

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

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

По сути, мы получаем ссылку на функцию разрешения при создании обещания и возвращаем его, чтобы его можно было установить извне.

Через секунду консоль выдаст:

> foo

Я думаю, что это лучший подход. Единственное, что код может быть немного менее многословным.
pie6k

Ницца! Умная идея +50 если бы мог.
Митя

Это именно то, что сделал OP. На самом деле вы заново изобретаете отложенный шаблон над обещаниями, конечно, это возможно, и ваш подход работает (как исходный код 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

Нашим решением было использовать замыкания для хранения функций разрешения / отклонения и дополнительно добавить функцию для расширения самого обещания.

Вот образец:

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
Отлично ... Я только изучаю Обещания, но меня постоянно озадачивает тот факт, что вы, кажется, не можете решить их "где-то еще". Использование замыкания для скрытия деталей реализации - отличная идея ... но на самом деле я не уверен, что именно это вы и сделали: вместо того, чтобы иметь "псевдо" закрытые переменные, я уверен, что есть способ полностью скрыть переменные который должен быть недоступен ... что на самом деле означает закрытие ...
Майк Грызун

> Замыкание - это блок кода, на который можно ссылаться (и передавать) с доступом к переменным окружения. var _resolve, _reject; являются ограждающей областью.
Стивен Спунгин

да, достаточно справедливо. На самом деле мне кажется, что мой ответ слишком усложняет вещи, и, кроме того, ваш ответ может быть упрощен: вам просто нужно идти promise.resolve_ex = _resolve; promise.reject_ex = _reject;... все еще работает отлично.
Майк Грызун

« приложите функцию, чтобы продлить само обещание. » - не делайте этого. Обещания являются значениями результата, они не должны предоставлять возможность их разрешать. Вы не хотите передавать эти расширенные.
Берги

2
Вопрос заключался в том, как решить эту проблему за рамками. Вот решение, которое работает, и в нашем производстве у нас действительно была необходимая причина для этого. Я не понимаю, почему решение поставленной проблемы заслуживает отрицательного ответа.
Стивен Спунгин

2

Я обнаружил, что пропускаю паттерн Deferred в некоторых случаях. Вы всегда можете создать один поверх Обещания ES6:

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 (), описанный ранее, а также несколько других объектов, построенных на нем. Все они используют Обещания и аккуратный синтаксис обратного вызова Promise для реализации связи / обработки событий в программе.

  • Defer: обещание, которое может быть решено неудачно удаленно (вне его тела)
  • Задержка: обещание, которое разрешается автоматически через определенное время
  • TimeOut: Обещание, которое не выполняется автоматически по истечении заданного времени.
  • Цикл: повторно запускаемое обещание управлять событиями с синтаксисом обещания
  • Очередь: Очередь выполнения, основанная на цепочке Promise.

    rp = require("repeatable-promise")

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


1

Я написал небольшую библиотеку для этого. https://www.npmjs.com/package/@inf3rno/promise.exposed

Я использовал метод завода подход других написал, но я перегрузил then, catch, finallyметоды, так что вы можете решить оригинальное обещание тем , как хорошо.

Разрешение Обещания без исполнителя со стороны:

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которая функционирует как замена для Promise. Ни один из других ответов здесь не будет работать как замена Promise, так как они используют прокси или оболочки.

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-синтаксис в браузере или узле

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'
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.