Вирішіть Javascript Promise за межами функції


279

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

Щоразу, коли вони знайдуть правильне рішення цієї проблеми, я сподіваюся, що вони також змусять це працювати для вкладених обіцянок, деякі з яких можуть повторитися.
Артур Тарасов

Я думаю, що API Promise "пропонує" завжди використовувати їх як значення повернення, а ніколи як об'єкти, до яких можна отримати доступ або викликати. Іншими словами, змушуйте нас ставитися до них як до повернених значень замість об'єктів, до яких ми можемо отримати доступ, або до функцій, до яких ми можемо викликати, або до чогось, що ми можемо посилатися зі змінною або передаватись як параметр тощо. Якщо ви почнете використовувати обіцянки як будь-який інший об’єкт, ймовірно, ви врешті-решт потрібно вирішити це ззовні, як у вашому запитанні ... Враховуючи це, я також думаю, що повинен бути офіційний спосіб зробити це ... і відкладене здається для мене просто вирішенням.
ракберо

Відповіді:


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 добування?
Вінод Собале

95
Не поширені? У кінцевому підсумку мені потрібен майже кожен проект.
Томаш Зато - Відновіть Моніку

1
Що стосується футляру, ви вважаєте, що вам потрібно щось зробити після того, як подія запускається і щось інше сталося. Ви хочете перетворити подію на обіцянку і об'єднати її з іншою обіцянкою. Здається, це загальна проблема для мене.
Герман

130

просто:

var promiseResolve, promiseReject;

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

promiseResolve();

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

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

Так, це спіймано, тому що ти обернув навколо нього ще одного конструктора обіцянок - саме те, що я намагаюся зробити. Однак, скажімо, у вас є якийсь інший код, який намагається викликати rješava () поза конструктором (він же відкладений об'єкт) ... Він може кинути виняток і не бути спійманим jsbin.com/cokiqiwapo/1/edit?js,console
Джон Жак

8
Я навіть не впевнений, що це поганий дизайн. Помилка, викинута поза обіцянкою, не повинна потрапляти в обіцянку. Це, мабуть, приклад помилкового уявлення або поганого розуміння, якщо дизайнер насправді очікує, що помилка виявиться всередині.
KalEl

3
Цей конкретний конструкт уже згадується у питанні. Ви навіть це читали?
Седрік Райхенбах

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
Зауважте тут лексичне обстеження.
Флорі

1
Немає жодної практичної різниці в resolve|rejectпризначенні лексично чи через bind. Це просто проста реалізація об'єкта jQuery Deferred , який існує приблизно з 1,0 (ish). Це працює точно як обіцянка, за винятком того, що немає безпеки при киданні. Вся суть цього питання полягала в тому, як зберегти кілька рядків коду при створенні обіцянок.
Джон Жак

1
Використання відкладеного є звичайним способом зробити це, я не маю уявлення, чому це не вище
BlueRaja - Danny Pflughoeft

1
Відмінна відповідь! Шукав відкладену функціональність, яку пропонує jQuery.
Аншул Кока

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
Дякую, це спрацювало. Але що таке обробник? Мені довелося її зняти, щоб спрацювати.
Сахід

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

Прийнята відповідь неправильна. Використовувати сферу застосування та посилання досить просто, хоча це може розлютити пуристів :

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, якби міг.
Мітя

Це саме те, що зробили ОП. Насправді ви заново вигадуєте «Відкладений шаблон» над «Обіцянками», звичайно, це можливо, і ваш підхід працює (як початковий код ОП), але це не найкраща практика через «причину безпеки кидання», описану в прийнятій відповіді.
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

Я вважаю, що у деяких випадках відсутня схема "Відкладений". Ви завжди можете створити його на верхній частині обіцянки 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 для здійснення комунікації / події в рамках програми.

  • Відстрочка: Обіцяйте, що це може бути вирішено не вдалося дистанційно (поза його корпусом)
  • Затримка: Обіцяння, яке вирішується автоматично через певний час
  • TimeOut: Обіцяйте, що автоматично не вдасться пройти заданий час.
  • Цикл: Повторна спроба обіцяти керувати подіями за допомогою синтаксису Promise
  • Черга: Черга на виконання на основі ланцюга 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.");

Гонки з набором виконавця Час виходу ззовні:

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

Перше включення - синтаксис синтаксису в браузері або на вузлі

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

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

0

Ще одне рішення для вирішення обіцянки ззовні

 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.