2016-04-19 9 views
7

Podczas testów odkryłem, że obietnice JavaScript są zawsze asynchroniczne niezależnie od tego, czy zawierają asynchroniczne funkcje w swoim łańcuchu.Dlaczego obietnice javascript obietnice są asynchroniczne podczas wywoływania tylko funkcji synchronicznych?

Oto kod, który pokazuje kolejność operacji w konsoli. Jeśli go uruchomisz, zobaczysz, że chociaż każda funkcja jest synchroniczna, dane wyjściowe pokazują, że oba połączenia aPromise() są uruchamiane równolegle, a przed zakończeniem drugiego biegu dzieje się tak, jak przed rozpoczęciem drugiego.

function aPromise() { 
 
    return new Promise(function(resolve, reject) { 
 
    console.log("making promise A") 
 
    resolve(bPromise()); 
 
    console.log("promise A resolved") 
 
    }); 
 
} 
 

 

 
function bPromise() { 
 
    return new Promise(function(resolve, reject) { 
 
    console.log("making and resolving promise B") 
 
    resolve(); 
 
    }); 
 
} 
 

 
aPromise().then(function() { 
 
    console.log("finish run 1"); 
 
}).then(function() { 
 
    console.log("surprisingly this happens after run 2 finishes"); 
 
}); 
 
aPromise().then(function() { 
 
    console.log("finish run 2"); 
 
})

wyjścia do konsoli:

making promise A 
making and resolving promise B 
promise A resolved 
making promise A 
making and resolving promise B 
promise A resolved 
finish run 1 
finish run 2 
surprisingly this happens after run 2 finishes 

Więc Dlaczego JavaScript obiecuje asynchroniczny Dzwoniąc tylko funkcje synchronicznych? Co dzieje się za kulisami, które prowadzą do tego zachowania?


P.S. Aby to lepiej zrozumieć, zaimplementowałem swój własny system obietnic i odkryłem, że sprawienie, by funkcje synchroniczne odbywały się w oczekiwanej kolejności było łatwe, ale ich równoległe wykonanie było czymś, co mogłem osiągnąć tylko przez ustawienie setTimeout() o kilku milisekundach na każdym kroku. rozwiązać (Domyślam się, że to nie jest to, co dzieje się z obietnicami wanilii i że są one faktycznie wielowątkowe).

To był mały problem dla jednego z moich programów, w którym przechodzę drzewo, stosując szereg funkcji dla każdego węzła i umieszczając funkcje w kolejce, jeśli ten węzeł ma już działającą funkcję asynchroniczną. Większość funkcji jest zsynchronizowana, więc kolejka jest rzadko używana, ale po przełączeniu z wywołań zwrotnych (piekło) na obietnice miałem problem, w którym koleje są używane prawie zawsze w wyniku obietnic nigdy nie działających synchronicznie. To nie jest duży problem, ale jest to trochę koszmarny debugowanie.

1 Rok Później EDIT

skończyło się na pisanie kodu, aby sobie z tym poradzić. Nie jest to zadziwiająco dokładne, ale użyłem go z powodzeniem, aby rozwiązać problem, który miałem.

var SyncPromise = function(fn) { 
    var syncable = this; 
    syncable.state = "pending"; 
    syncable.value; 

    var wrappedFn = function(resolve, reject) { 
     var fakeResolve = function(val) { 
      syncable.value = val; 
      syncable.state = "fulfilled"; 
      resolve(val); 
     } 

     fn(fakeResolve, reject); 
    } 

    var out = new Promise(wrappedFn); 
    out.syncable = syncable; 
    return out; 
} 

SyncPromise.resolved = function(result) { 
    return new SyncPromise(function(resolve) { resolve(result); }); 
} 

SyncPromise.all = function(promises) { 
    for(var i = 0; i < promises.length; i++) { 
     if(promises[i].syncable && promises[i].syncable.state == "fulfilled") { 
      promises.splice(i, 1); 
      i--; 
     } 
     // else console.log("syncable not fulfilled" + promises[i].syncable.state) 
    } 

    if(promises.length == 0) 
     return SyncPromise.resolved(); 

    else 
     return new SyncPromise(function(resolve) { Promise.all(promises).then(resolve); }); 
} 

Promise.prototype.syncThen = function (nextFn) { 
    if(this.syncable && this.syncable.state == "fulfilled") { 
      // 
     if(nextFn instanceof Promise) { 
      return nextFn; 
     } 
     else if(typeof nextFn == "function") { 
      var val = this.syncable.value; 
      var out = nextFn(val); 
      return new SyncPromise(function(resolve) { resolve(out); }); 
     } 
     else { 
      PINE.err("nextFn is not a function or promise", nextFn); 
     } 
    } 

    else { 
     // console.log("default promise"); 
     return this.then(nextFn); 
    } 
} 
+2

Ktoś inny może prawdopodobnie wykopać konkretnych rozmów, ale ogólna filozofia była to konsekwencja/przewidywalność jest cnotą w projektowaniu interfejsu. Niektóre wcześniejsze wdrożenia Promise nie zachowywały się w ten sposób, ale zmieniły się nastroje, gdy ludzie zostali ukąszeni przez nieoczekiwane błędy. Jest to * dużo * łatwiejsze do zrozumienia o obietnicach w ogóle, jeśli wiesz, że zawsze będą obsługiwane asynchronicznie. –

+0

Czy wiesz, jakie rodzaje błędów? –

+2

Szybkie spojrzenie na [** specyfikację **] (http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects), a sformułowanie brzmi "Natychmiast ** będzie się zamykać ** zadanie, do którego należy zadzwonić ", najwcześniejszy moment, w który można wywołać zdarzenie, następuje natychmiast po zakończeniu bieżącej funkcji. 'nowa Promise (r => r()). then then (() => console.log (" yay ")); let i = 10; while (--i) console.log (i); 'logs 9..1 before the yay –

Odpowiedz

9

przekazany do konstruktora Obietnica zwrotna jest zawsze nazywa się synchronicznie, ale wywołania zwrotne przekazywane do then są zawsze nazywane asynchronicznie (można użyć setTimeout z opóźnieniem 0 w realizacji przestrzeni użytkownika w celu osiągnięcia tego).

Uproszczenie swój przykład (i podając nazwiska anonimowa funkcja jest więc mogę odnieść się do nich) do:

Promise.resolve().then(function callbackA() { 
    console.log("finish run 1"); 
}).then(function callbackB() { 
    console.log("surprisingly this happens after run 2 finishes"); 
}); 

Promise.resolve().then(function callbackC() { 
    console.log("finish run 2"); 
}) 

nadal daje wyjście w tej samej kolejności:

finish run 1 
finish run 2 
surprisingly this happens after run 2 finishes 

wydarzenia toczyły się w ten zamówienie:

  1. Pierwsza obietnica została rozwiązana (synchronicznie)
  2. callbackA jest dodawany do kolejki pętli zdarzeń za
  3. Druga obietnica jest rozwiązany
  4. callbackC jest dodawany do kolejki pętli zdarzeń za
  5. Nie ma nic do zrobienia, więc pętla zdarzenie jest dostępne, callbackA jest pierwszym w kolejka, więc jest wykonywana, nie zwraca obietnicy, więc pośrednia obietnica callbackB jest natychmiastowo rozwiązywana synchronicznie, która dołącza callbackB do kolejki pętli zdarzeń.
  6. Nie ma już nic do zrobienia, więc pętla zdarzeń jest dostępna, callbackC jest pierwszy w kolejce, więc jest wykonywany.
  7. Nie ma już nic do zrobienia, więc pętla zdarzeń jest dostępna, callbackB jest pierwszy w kolejce, więc jest wykonywany.

Najprostszym sposobem mogę myśleć obejścia problemu jest użycie biblioteki, która ma Promise.prototype.isFulfilled funkcji można użyć, aby zdecydować, czy zadzwonić do drugiego zwrotnego synchronicznie, czy nie. Na przykład:

var Promise = require('bluebird');                               

Promise.prototype._SEPH_syncThen = function (callback) { 
    return (
     this.isPending() 
     ? this.then(callback) 
     : Promise.resolve(callback(this.value())) 
    ); 
} 

Promise.resolve()._SEPH_syncThen(function callbackA() { 
    console.log("finish run 1"); 
})._SEPH_syncThen(function callbackB() { 
    console.log("surprisingly this happens after run 2 finishes"); 
}); 

Promise.resolve()._SEPH_syncThen(function callbackC() { 
    console.log("finish run 2"); 
}) 

Ten Wyjścia:

finish run 1 
surprisingly this happens after run 2 finishes 
finish run 2 
+0

Kolejka zdarzeń sprawia, że ​​obietnice zawsze działają asynchronicznie. To ma sens. Czy wiesz, dlaczego dokonano wyboru tego projektu? Było to dla mnie trochę sprzeczne z intuicją. –

+4

@SephReed krótka odpowiedź: konsystencja. Długa odpowiedź: kwestionowanie wyboru projektu programisty języka jest poza zakresem Stack Overflow. –

+2

@SephReed Wierzę, że istnieje standardowy interfejs/API, który zawsze będzie miał standardowe zachowanie, niezależnie od tego, jaki kod wykonuje. Mutowanie jego funkcji w zależności od aplikacji może spowodować nieoczekiwane zachowanie, złożoność i dezorientację. – ste2425

0

Kod jest w porządku, to chcesz, aby Twoje obietnice uruchomić niezależnie i niech wykonać na swój własny sposób, bez względu na to każdy jeden kompletny pierwszy. Jak tylko twój kod jest asynchroniczny, nie możesz przewidzieć, który z nich zostanie ukończony jako pierwszy (ze względu na asynchroniczną naturę event loop).

Jednak jeśli chcesz złapać wszystkie obietnice po ich ukończeniu, powinieneś użyć Promise.all (co jest odpowiednikiem $.when jest jQuery). Patrz: