Jak prawidłowo zwracać wiele wartości z obietnicy?

86

Ostatnio kilka razy spotkałem się z pewną sytuacją, której nie wiedziałem, jak poprawnie rozwiązać. Załóżmy następujący kod:

somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )

function afterSomething( amazingData ) {
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
}

Teraz może pojawić się sytuacja, w której chciałbym mieć dostęp do amazingDataw afterSomethingElse.

Oczywistym rozwiązaniem byłoby zwrócenie tablicy lub skrótu afterSomething, ponieważ cóż, z funkcji można zwrócić tylko jedną wartość. Ale zastanawiam się, czy istnieje sposób, aby afterSomethingElsezaakceptować 2 parametry i wywołać je w podobny sposób, ponieważ wydaje się to o wiele łatwiejsze do udokumentowania i zrozumienia.

Zastanawiam się tylko nad tą możliwością, ponieważ istnieje Q.spread, która robi coś podobnego do tego, co chcę.

Der Hochstapler
źródło

Odpowiedzi:

88

Nie możesz rozwiązać obietnicy z wieloma właściwościami, tak jak nie możesz zwrócić wielu wartości z funkcji . Obietnica koncepcyjnie przedstawia wartość w czasie, więc chociaż możesz reprezentować wartości złożone, nie możesz umieścić w obietnicy wielu wartości.

Obietnica z natury rozwiązuje się za pomocą jednej wartości - jest to część tego, jak działa Q, jak działa specyfikacja Promises / A + i jak działa abstrakcja .

Najbliższe możliwe jest użycie Q.spreadi zwrócenie tablic lub użycie destrukturyzacji ES6, jeśli jest obsługiwane lub chcesz użyć narzędzia transpilacji, takiego jak BabelJS.

Jeśli chodzi o przekazywanie kontekstu w dół łańcucha obietnic, zapoznaj się z doskonałym kanonikiem Bergi na ten temat .

Benjamin Gruenbaum
źródło
16
Co jest złego w rozwiązaniu problemu z obiektem, który ma wiele właściwości? Wydaje się, że jest to prosty sposób na uzyskanie wielu wartości z rozwiązania.
jfriend00
6
Jest to całkowicie w porządku
Benjamin Gruenbaum
Możesz również przedłużyć Promise, aby .spread()Bluebird wyświetlał się w tej powiązanej odpowiedzi: stackoverflow.com/a/22776850/1624862
Kevin Ghadyani
Zachowanie Promise.all () wydaje się temu zaprzeczać. Promise.all([a, b, c]).then(function(x, y, z) {...})działa poprawnie we wszystkich nowoczesnych silnikach Javascript z x, y i z obliczanymi do rozstrzygniętych wartości a, b i c. Więc dokładniej jest powiedzieć, że język nie pozwala ci tego zrobić łatwo (lub rozsądnie) z kodu użytkownika (ponieważ możesz zwrócić Obietnicę bezpośrednio z klauzuli then, możesz zawrzeć swoje wartości w obietnicach, a następnie opakować je w Obietnicę .all (), aby uzyskać pożądane zachowanie, choć w zawiły sposób).
Austin Hemmelgarn
3
@AustinHemmelgarn To jest po prostu fałsz, Promise.allwypełnia tablicą . W Promise.all([a,b]).then((a, b) => bjest undefined. Dlatego musisz zrobić, .then(([a, b]) =>co jest zadaniem destrukcyjnym
Benjamin Gruenbaum
38

możesz przekazać tylko jedną wartość, ale może to być tablica z wielokrotnościami w obrębie, na przykład:

function step1(){
  let server = "myserver.com";
  let data = "so much data, very impresive";
  return Promise.resolve([server, data]);
}

z drugiej strony możesz użyć wyrażenia destrukturyzującego dla ES2015, aby uzyskać indywidualne wartości.

function step2([server, data]){
  console.log(server); // print "myserver.com"
  console.log(data);   // print "so much data, very impresive"
  return Promise.resolve("done");
}

nazwać obie obietnicą, łącząc je:

step1()
.then(step2)
.then((msg)=>{
  console.log(msg); // print "done"
})
Alejandro Silva
źródło
5
Nazywa się destrukturyzacją , a nie „dekonstruktorem” i nie jest operatorem: - /
Bergi
3
Przy okazji, możesz użyć destrukturyzacji bezpośrednio w parametrach: function step2([server, data]) { …- w ten sposób unikasz również przypisywania do niejawnych zmiennych globalnych. I naprawdę powinieneś używać konstruktora returnlub Promise.resolvenie new Promisew swoich przykładach.
Bergi
dzięki @Bergi za rekomendacje!
Alejandro Silva
19

Możesz zwrócić obiekt zawierający obie wartości - nie ma w tym nic złego.

Inną strategią jest utrzymanie wartości poprzez domknięcia zamiast przepuszczania jej przez:

somethingAsync().then(afterSomething);

function afterSomething(amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}

Forma pełna, a nie częściowo wstawiana (równoważna, prawdopodobnie bardziej spójna):

somethingAsync().then(function (amazingData) {
  return processAsync(amazingData).then(function (processedData) {
    // both amazingData and processedData are in scope here
  });
}
Kevin Reid
źródło
3
Czy można zwrócić jeden thendo drugiego then? To nie jest anty-wzór ?
robe007
jak powiedział @ robe007, czy nie byłoby to podobne do „piekła zwrotnego”? tutaj zagnieżdżanie, a następnie bloki zamiast funkcji wywołania zwrotnego,
zniweczyłyby
5

Dwie rzeczy możesz zrobić, zwrócić obiekt

somethingAsync()
    .then( afterSomething )
    .then( afterSomethingElse );

function processAsync (amazingData) {
     //processSomething
     return {
         amazingData: amazingData, 
         processedData: processedData
     };
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}

function afterSomethingElse( dataObj ) {
    let amazingData = dataObj.amazingData,
        processedData = dataObj.proccessedData;
}

Użyj lunety!

var amazingData;
somethingAsync()
  .then( afterSomething )
  .then( afterSomethingElse )

function afterSomething( returnedAmazingData ) {
  amazingData = returnedAmazingData;
  return processAsync( amazingData );
}
function afterSomethingElse( processedData ) {
  //use amazingData here
}
jemiloii
źródło
3

Oto, jak moim zdaniem powinieneś postępować.

rozszczepienie łańcucha

Ponieważ obie funkcje będą używać amazingData , warto mieć je w dedykowanej funkcji. Zwykle robię to za każdym razem, gdy chcę ponownie użyć niektórych danych, więc są one zawsze obecne jako argument funkcji.

Ponieważ twój przykład uruchamia jakiś kod, przypuszczam, że jest on w całości zadeklarowany wewnątrz funkcji. Nazwę to toto () . Wtedy będziemy mieli inną funkcję, która będzie działać zarówno afterSomething (), jak i afterSomethingElse () .

function toto() {
    return somethingAsync()
        .then( tata );
}

Zauważysz również, że dodałem oświadczenie o zwrocie , ponieważ zwykle jest to sposób na dotarcie do obietnic - zawsze zwracasz obietnicę, abyśmy mogli nadal łączyć ją w razie potrzeby. Tutaj cośAsync () wygeneruje amazingData i będzie dostępne wszędzie w nowej funkcji.

Od tego, jak będzie wyglądać ta nowa funkcja, zazwyczaj zależy, czy processAsync () jest również asynchroniczna ?

processAsync nie jest asynchroniczna

Nie ma powodu, aby zbytnio komplikować rzeczy, jeśli processAsync () nie jest asynchroniczna. Zrobiłoby to jakiś stary dobry kod sekwencyjny.

function tata( amazingData ) {
    var processed = afterSomething( amazingData );
    return afterSomethingElse( amazingData, processed );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

Zauważ, że nie ma znaczenia, czy afterSomethingElse () robi coś asynchronicznego, czy nie. Jeśli tak się stanie, obietnica zostanie zwrócona i łańcuch może być kontynuowany. Jeśli tak nie jest, zostanie zwrócona wartość wyniku. Ale ponieważ funkcja jest wywoływana z then () , wartość i tak zostanie opakowana w obietnicę (przynajmniej w surowym JavaScript).

processAsync asynchroniczny

Jeśli processAsync () jest asynchroniczna, kod będzie wyglądał nieco inaczej. Tutaj rozważymy, że afterSomething () i afterSomethingElse () nie zostaną ponownie użyte nigdzie indziej.

function tata( amazingData ) {
    return afterSomething()
        .then( afterSomethingElse );

    function afterSomething( /* no args */ ) {
        return processAsync( amazingData );
    }
    function afterSomethingElse( processedData ) {
        /* amazingData can be accessed here */
    }
}

To samo co poprzednio dla afterSomethingElse () . Może być asynchroniczny lub nie. Obietnica zostanie zwrócona lub wartość zawarta w rozwiązanej obietnicy.


Twój styl kodowania jest bardzo zbliżony do tego, czego używam, dlatego odpowiedziałem nawet po 2 latach. Nie jestem wielkim fanem posiadania wszędzie anonimowych funkcji. Trudno mi to czytać. Nawet jeśli jest to dość powszechne w społeczności. Dzieje się tak, gdy zamieniliśmy piekło zwrotne na czyściec obietnicy .

Chciałbym również, aby utrzymać nazwę funkcji w następnie krótki. I tak zostaną zdefiniowane tylko lokalnie. I przez większość czasu będą wywoływać inną funkcję zdefiniowaną w innym miejscu - tak wielokrotnego użytku - aby wykonać zadanie. Robię to nawet dla funkcji z tylko 1 parametrem, więc nie muszę pobierać i wychodzić funkcji, gdy dodaję / usuwam parametr do sygnatury funkcji.

Przykład jedzenia

Oto przykład:

function goingThroughTheEatingProcess(plenty, of, args, to, match, real, life) {
    return iAmAsync()
        .then(chew)
        .then(swallow);

        function chew(result) {
            return carefullyChewThis(plenty, of, args, "water", "piece of tooth", result);
        }

        function swallow(wine) {
            return nowIsTimeToSwallow(match, real, life, wine);
        }
}

function iAmAsync() {
    return Promise.resolve("mooooore");
}

function carefullyChewThis(plenty, of, args, and, some, more) {
    return true;
}

function nowIsTimeToSwallow(match, real, life, bobool) {
}

Nie skupiaj się zbytnio na Promise.resolve () . To tylko szybki sposób na złożenie zdecydowanej obietnicy. Co staram się osiągnąć tego celu jest mieć cały kod używam w jednym miejscu - tuż pod thens . Wszystkie inne funkcje o bardziej opisowej nazwie są wielokrotnego użytku.

Wadą tej techniki jest to, że definiuje wiele funkcji. Ale obawiam się, że jest to ból konieczny, aby uniknąć wszędzie anonimowych funkcji. A jakie to ryzyko w ogóle: przepełnienie stosu? (żart!)


Używanie tablic lub obiektów zdefiniowanych w innych odpowiedziach również zadziałałoby. To w pewnym sensie odpowiedź zaproponowana przez Kevina Reida .

Możesz także użyć bind () lub Promise.all () . Pamiętaj, że nadal będą wymagać podzielenia kodu.

za pomocą bind

Jeśli chcesz, aby twoje funkcje były wielokrotnego użytku, ale tak naprawdę nie musisz przechowywać tego, co jest w środku, wtedy bardzo krótkie, możesz użyć bind () .

function tata( amazingData ) {
    return afterSomething( amazingData )
        .then( afterSomethingElse.bind(null, amazingData) );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( amazingData, processedData ) {
}

Aby było to proste, bind () doda listę argumentów (z wyjątkiem pierwszego) do funkcji, gdy zostanie ona wywołana.

przy użyciu Promise.all

W swoim poście wspomniałeś o użyciu funkcji spread () . Nigdy nie korzystałem z frameworka, którego używasz, ale oto jak powinieneś być w stanie go używać.

Niektórzy uważają, że Promise.all () jest rozwiązaniem wszystkich problemów, więc chyba zasługuje na wzmiankę.

function tata( amazingData ) {
    return Promise.all( [ amazingData, afterSomething( amazingData ) ] )
        .then( afterSomethingElse );
}

function afterSomething( amazingData ) {
    return processAsync( amazingData );
}
function afterSomethingElse( args ) {
    var amazingData = args[0];
    var processedData = args[1];
}

Możesz przekazać dane do Promise.all () - zwróć uwagę na obecność tablicy - tak długo, jak obietnice, ale upewnij się, że żadna z obietnic nie zawiedzie, w przeciwnym razie przestanie przetwarzać.

Zamiast definiować nowe zmienne z argumentu args , powinieneś móc używać funkcji spread () zamiast then () do wszelkiego rodzaju niesamowitych prac.

gabriel
źródło
3

Po prostu utwórz obiekt i wyodrębnij argumenty z tego obiektu.

let checkIfNumbersAddToTen = function (a, b) {
return new Promise(function (resolve, reject) {
 let c = parseInt(a)+parseInt(b);
 let promiseResolution = {
     c:c,
     d : c+c,
     x : 'RandomString'
 };
 if(c===10){
     resolve(promiseResolution);
 }else {
     reject('Not 10');
 }
});
};

Wyciągnij argumenty z obietnicyRozwiązanie.

checkIfNumbersAddToTen(5,5).then(function (arguments) {
console.log('c:'+arguments.c);
console.log('d:'+arguments.d);
console.log('x:'+arguments.x);
},function (failure) {
console.log(failure);
});

źródło
2

Cokolwiek wrócisz z obietnicy, zostanie opakowane w obietnicę, która zostanie rozpakowana na następnym .then()etapie.

Staje się to interesujące, gdy musisz zwrócić jedną lub więcej obietnic obok jednej lub więcej synchronicznych wartości, takich jak;

Promise.resolve([Promise.resolve(1), Promise.resolve(2), 3, 4])
       .then(([p1,p2,n1,n2]) => /* p1 and p2 are still promises */);

W takich przypadkach konieczne byłoby skorzystanie Promise.all()z opcji p1i p2obietnice rozpakowane na następnym .then()etapie, takim jak

Promise.resolve(Promise.all([Promise.resolve(1), Promise.resolve(2), 3, 4]))
       .then(([p1,p2,n1,n2]) => /* p1 is 1, p2 is 2, n1 is 3 and n2 is 4 */);
Redu
źródło
1

Możesz sprawdzić Observable reprezentowane przez Rxjs , pozwala zwrócić więcej niż jedną wartość.

codelovesme
źródło
0

Po prostu zwróć krotkę:

async add(dto: TDto): Promise<TDto> {
console.log(`${this.storeName}.add(${dto})`);
return firebase.firestore().collection(this.dtoName)
  .withConverter<TDto>(this.converter)
  .add(dto)
  .then(d => [d.update(this.id, d.id), d.id] as [any, string])
  .then(x => this.get(x[1]));

}

swissmawi
źródło