Rozwiąż obietnicę JavaScript poza zakresem funkcji


279

Korzystam z ES6 Promise.

Zwykle obietnica jest budowana i używana w ten sposób

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

Ale robiłem coś takiego, jak poniżej, aby podjąć decyzję na zewnątrz ze względu na elastyczność.

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

I później

onClick = function(){
    outsideResolve();
}

Działa to dobrze, ale czy jest na to łatwiejszy sposób? Jeśli nie, czy to dobra praktyka?


2
Nie sądzę, że jest inny sposób. Uważam, że jest określone, że przekazane wywołanie zwrotne Promisemusi być wykonywane synchronicznie, aby umożliwić „eksportowanie” dwóch funkcji.
Felix Kling

1
Działa to dla mnie dokładnie tak, jak to napisałeś. Według mnie jest to „kanoniczny” sposób.
Gilad Barner

14
Myślę, że powinien istnieć formalny sposób na osiągnięcie tego w przyszłości. Moim zdaniem ta funkcja jest bardzo potężna, ponieważ możesz poczekać na wartości z innych kontekstów.
Jose

Ilekroć wymyślą właściwe rozwiązanie tego problemu, mam nadzieję, że sprawią, że zadziała również w przypadku zagnieżdżonych obietnic, z których część może się powtórzyć.
Arthur Tarasov

Myślę, że interfejs API Promise „sugeruje”, aby zawsze używać ich jako wartości zwracanych, a nigdy jako obiektów, do których można uzyskać dostęp lub które można wywołać. Innymi słowy, zmusza nas do traktowania ich jako wartości zwracanych zamiast obiektów, do których możemy uzyskać dostęp lub funkcji, które możemy wywołać lub czegoś, do czego możemy się odwołać za pomocą zmiennej lub przekazać jako parametr itp. Jeśli zaczniesz używać obietnic jak każdego innego obiektu, prawdopodobnie to zrobisz w końcu muszę rozwiązać to z zewnątrz, jak w twoim pytaniu ... Biorąc to pod uwagę, uważam również, że powinien istnieć formalny sposób na zrobienie tego ... i Odroczony wydaje mi się tylko obejściem.
cancerbero

Odpowiedzi:


93

Nie, nie ma innego sposobu, aby to zrobić - jedyne, co mogę powiedzieć, to to, że ten przypadek użycia nie jest zbyt powszechny. Jak powiedział Felix w komentarzu - to, co robisz, będzie konsekwentnie działać.

Warto wspomnieć, że powodem, dla którego konstruktor obietnicy zachowuje się w ten sposób, jest bezpieczeństwo rzucania - jeśli wyjątek, którego się nie spodziewałeś, zdarzy się, gdy Twój kod działa wewnątrz konstruktora obietnicy, zmieni się w odrzucenie, ta forma bezpieczeństwa rzucania - konwertuje zgłoszone błędy na odrzucenia są ważne i pomagają utrzymać przewidywalny kod.

Z tego powodu rzutu bezpieczeństwa wybrano konstruktora obietnicy zamiast odroczenia (które są alternatywnym sposobem budowania obietnicy, który pozwala na to, co robisz) - tak jak w przypadku najlepszych praktyk - przekazałbym element i zamiast tego użyłem konstruktora obietnicy:

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

Z tego powodu - w dowolnym momencie można użyć konstruktora obietnica na eksporcie funkcje - polecam zrobić z niego korzystać. Ilekroć możesz uniknąć obu - unikaj obu i łańcucha.

Zauważ, że nigdy nie powinieneś używać konstruktora obietnic do takich rzeczy if(condition), pierwszy przykład można zapisać jako:

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

2
Cześć Benjamin! Czy nie ma obecnie lepszego sposobu na uzyskanie pysznego cukru z obietnicy, jeśli nie wiemy jeszcze, kiedy obietnica się spełni? Jak jakiś asynchroniczny wzorzec oczekiwania / powiadomienia ? Jak na przykład „sklep”, a później wywołać Promisełańcuch? Np. W moim szczególnym przypadku jestem na serwerze i czekam na konkretną odpowiedź klienta (uścisk dłoni typu SYN-ACK, aby upewnić się, że klient pomyślnie zaktualizował stan).
Domi

1
@Domi sprawdź q-connection i RxJS.
Benjamin Gruenbaum

2
Jak mogę zrobić to samo, używając interfejsu API pobierania?
Vinod Sobale,

95
Niezbyt często? Potrzebuję go prawie do każdego projektu.
Tomáš Zato - Przywróć Monikę

1
Jeśli chodzi o przypadek użycia, musisz zrobić coś po uruchomieniu zdarzenia i wydarzyło się coś innego. Chcesz przekształcić wydarzenie w obietnicę i połączyć je z inną obietnicą. Wydaje mi się to ogólnym problemem.
Gherman

130

prosty:

var promiseResolve, promiseReject;

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

promiseResolve();

2
@ruX, jak wspomniano w przyjętej odpowiedzi - została zaprojektowana w ten sposób celowo. Chodzi o to, że jeśli zostanie zgłoszony wyjątek, zostanie on złapany przez konstruktora obietnicy. Ta odpowiedź (podobnie jak moja) może być przyczyną zgłoszenia wyjątku dla wszystkich wywołań kodu promiseResolve(). Semantyka obietnicy polega na tym, że zawsze zwraca wartość. Jest to również funkcjonalnie takie samo jak post OP, nie rozumiem, jaki problem rozwiązuje w sposób wielokrotnego użytku.
Jon Jaques

4
@JonJaques Nie jestem pewien, czy to, co mówisz, jest prawdą. Kod, który wywołuje promiseResolve(), nie zgłosi wyjątku. Możesz zdefiniować a .catchna konstruktorze i bez względu na to, jaki kod go .catchwywoła, wywołane zostaną konstruktory . Oto jsbin pokazujący, jak to działa: jsbin.com/yicerewivo/edit?js,console
carter

Tak, został złapany, ponieważ otoczyłeś go innym konstruktorem obietnic - Właśnie o to mi chodzi. Załóżmy jednak, że masz inny kod, który próbuje wywołać resolver () poza konstruktorem (inaczej obiekt Deferred
Jon Jaques,

8
Nie jestem nawet pewien, czy to zły projekt. Błąd zgłoszony poza obietnicą nie powinien zostać złapany w obietnicę. Być może jest to przykład nieporozumienia lub złego zrozumienia, jeśli projektant rzeczywiście spodziewa się, że błąd zostanie wykryty.
KalEl

3
Ta dokładna konstrukcja jest już wspomniana w pytaniu. Czy w ogóle to przeczytałeś?
Cedric Reichenbach,

103

Trochę za późno na przyjęcie tutaj, ale innym sposobem na to byłoby użycie Odroczonego obiektu. Zasadniczo masz taką samą ilość płyty kotłowej, ale jest to przydatne, jeśli chcesz je przekazać i ewentualnie rozwiązać poza ich definicją.

Naiwne wdrożenie:

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

Wersja 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
Zwróć uwagę na zakres leksykalny tutaj.
Florrie

1
Nie ma praktycznej różnicy, czy resolve|rejectprzypisuje się je leksykalnie, czy poprzez bind. To tylko prosta implementacja obiektu jQuery Deferred, który istnieje od 1.0 (ish). Działa dokładnie jak obietnica, z tym wyjątkiem, że nie ma bezpieczeństwa rzucania. Istotą tego pytania było, jak zapisać kilka wierszy kodu podczas tworzenia obietnic.
Jon Jaques

1
Używanie odroczenia jest zwykłym sposobem, aby to zrobić, nie mam pojęcia, dlaczego nie jest wyższe
BlueRaja - Danny Pflughoeft

1
Doskonała odpowiedź! Szukał odroczonej funkcjonalności, którą oferuje jQuery.
Anshul Koka

2
Jest Deferredprzestarzałe?
Pacerier

19

Rozwiązanie, które wymyśliłem w 2015 roku dla mojego frameworka. Ten rodzaj obietnic nazwałem Zadaniem

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
Dzięki, działało. Ale czym jest handler? Musiałem go usunąć, aby działał.
Sahid

16

Podobała mi się odpowiedź @JonJaques, ale chciałem pójść o krok dalej.

Jeśli powiążesz, thena catchnastępnie Deferredobiekt, to w pełni implementuje Promiseinterfejs API i możesz traktować go jako obietnicę awaiti tak dalej.

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

Metoda pomocnicza złagodziłaby ten dodatkowy koszt i dałaby ci to samo wrażenie jQuery.

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

Wykorzystanie byłoby

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

Który jest podobny do jQuery

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

Chociaż w przypadku użycia ta prosta, natywna składnia jest w porządku

return new Promise((resolve, reject) => {
    displayConfirmationDialog({
        confirm: resolve,
        cancel: reject
    });
});

8

Korzystam z funkcji pomocnika, aby utworzyć coś, co nazywam „płaską obietnicą” -

function flatPromise() {

    let resolve, reject;

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

    return { promise, resolve, reject };
}

I używam tego tak -

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;

}

Zobacz pełny przykład działania -

Edycja: Utworzyłem pakiet NPM o nazwie Flat-Promise, a kod jest również dostępny na GitHub .


7

Możesz zawrzeć Obietnicę w klasie.

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

Wiele odpowiedzi tutaj jest podobnych do ostatniego przykładu w tym artykule . Buforuję wiele obietnic, a funkcje resolve()i reject()można przypisać do dowolnej zmiennej lub właściwości. W rezultacie jestem w stanie uczynić ten kod nieco bardziej kompaktowym:

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

Oto uproszczony przykład użycia tej wersji defer()połączenia FontFaceObietnicy obciążenia z innym procesem asynchronicznym:

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 

Zaktualizuj: 2 alternatywy na wypadek, gdybyś chciał obudować obiekt:

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

i

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

Jeśli używasz tych przykładów w funkcji asynchronicznej, musisz odwołać się do właściwości promise, jeśli chcesz użyć wartości rozwiązanej obietnicy:const result = await deferred.promise;
b00t

6

Zaakceptowana odpowiedź jest nieprawidłowa. Korzystanie z zakresu i odniesień jest dość łatwe, chociaż może oburzyć purystów Obiecujących:

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

Zasadniczo chwytamy odniesienie do funkcji rozstrzygania, gdy obietnica jest tworzona, i zwracamy ją, aby można ją było ustawić zewnętrznie.

W ciągu jednej sekundy konsola wyśle:

> foo

Myślę, że to najlepsze podejście. Jedyną rzeczą jest to, że kod może być nieco mniej szczegółowy.
pie6k

Miły! Sprytny pomysł. +50, gdybym mógł.
Mitya

Tak właśnie zrobił OP. W rzeczywistości wymyślasz odroczony wzorzec nad obietnicami, oczywiście jest to możliwe i twoje podejście działa (jako początkowy kod OP), ale nie jest to najlepsza praktyka ze względu na „powód bezpieczeństwa rzutu” opisany w zaakceptowanej odpowiedzi.
zimą

4

Tak, możesz. Korzystając z CustomEventinterfejsu API dla środowiska przeglądarki. I za pomocą projektu emitera zdarzeń w środowiskach node.js. Ponieważ fragment kodu w pytaniu dotyczy środowiska przeglądarki, oto działający przykład tego samego.

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>

Mam nadzieję, że ta odpowiedź będzie przydatna!


3

Naszym rozwiązaniem było użycie zamknięć do przechowywania funkcji rozstrzygania / odrzucania oraz dodatkowo dołączanie funkcji przedłużającej samą obietnicę.

Oto wzór:

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

I używając go:

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
Świetnie ... Właśnie uczę się obietnic, ale konsekwentnie zastanawia mnie fakt, że nie wydaje się, że można je rozwiązać „gdzie indziej”. Użycie zamknięcia do ukrycia szczegółów implementacji to świetny pomysł ... ale tak naprawdę nie jestem pewien, czy to właśnie zrobiłeś: zamiast mieć „pseudo” zmienne prywatne jestem pewien, że istnieje sposób, aby całkowicie ukryć zmienne co powinno być niedostępne ... co tak naprawdę oznacza zamknięcie ...
Mike Gryzonie

> Zamknięcie to blok kodu, do którego można się odwoływać (i przekazywać) z dostępem do zmiennych otaczającego zakresu. var _resolve, _reject; są obejmującym zakresem.
Steven Spungin

tak, wystarczy. Właściwie wydaje mi się, że moja odpowiedź jest zbyt skomplikowana, a ponadto, że twoja odpowiedź może być uproszczona: po prostu musisz iść promise.resolve_ex = _resolve; promise.reject_ex = _reject;... nadal działa dobrze.
Mike gryzoń

dołącz funkcję przedłużającą samą obietnicę. ” - nie rób tego. Obietnice są wartościami wynikowymi, nie powinny zapewniać możliwości ich rozwiązania. Nie chcesz przekazywać tych rozszerzonych.
Bergi

2
Pytanie brzmiało, jak to rozwiązać poza zakresem. Oto rozwiązanie, które działa, a my w naszej produkcji mieliśmy niezbędny powód, aby to zrobić. Nie rozumiem, dlaczego rozwiązanie podanego problemu zasługuje na opinię.
Steven Spungin

2

W niektórych przypadkach brakuje mi również odroczonego wzoru. Zawsze możesz utworzyć jeden na obietnicy 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

Dziękujemy wszystkim, którzy opublikowali ten wątek. Utworzyłem moduł, który zawiera opisany wcześniej obiekt Defer (), a także kilka innych zbudowanych na nim obiektów. Wszystkie wykorzystują obietnice i zgrabną składnię oddzwaniania Promise do implementacji komunikacji / obsługi zdarzeń w programie.

  • Odrocz: obietnica, którą można rozwiązać zdalnie, nie powiodła się (poza ciałem)
  • Opóźnienie: obietnica, która jest rozpatrywana automatycznie po określonym czasie
  • TimeOut: obietnica, która nie powiedzie się automatycznie po określonym czasie.
  • Cykl: Obietnica z możliwością ponownego uruchomienia do zarządzania zdarzeniami ze składnią Promise
  • Kolejka: kolejka wykonania oparta na łańcuchach Promise.

    rp = require("repeatable-promise")

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


1

Napisałem za to małą bibliotekę. https://www.npmjs.com/package/@inf3rno/promise.exposed

Kiedyś podejście metoda fabryki inni napisał, ale overrode then, catch, finallymetody też, więc można rozwiązać oryginalnego obietnicy przez tych, jak również.

Rozwiązanie Obietnicy bez wykonawcy z zewnątrz:

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

Wyścigi z setem egzekutora Timeout z zewnątrz:

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

Istnieje tryb bez konfliktu, jeśli nie chcesz zanieczyszczać globalnej przestrzeni nazw:

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

1

Stworzyłem bibliotekę o manual-promisetej nazwie, która zastępuje Promise. Żadna z pozostałych odpowiedzi tutaj nie będzie działać jako drop w zastępstwie dlaPromise , ponieważ używają proxy lub opakowania.

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

A może stworzysz funkcję przechwytywania odrzucenia i zwrotu?

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

Złożyłem listę, która spełnia tę funkcję: https://gist.github.com/thiagoh/c24310b562d50a14f3e7602a82b4ef13

oto jak powinieneś go użyć:

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

najpierw włącz --allow-natives-syntax w przeglądarce lub węźle

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

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

0

To kolejne rozwiązanie, które rozwiązuje obietnicę z zewnątrz

 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 }
    }

Stosowanie

let lock = new Lock(... some id ...);
...
lock.get().then(()=>{console.log('resolved/released')})
lock.release()  // Excpected 'resolved/released'
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.