2016-03-03 12 views
6

Chcę przetworzyć wiele obietnic w sekwencji. Mam poniżej working piece of code, ale zastanawiam się, czy zbyt skomplikowałem łańcuchy obietnic. Wydaje mi się, że tworzę wiele nowych zamknięć i podrapuję się w głowę, zastanawiając się, czy czegoś brakuje.Sekwencja JavaScript Promise

Czy istnieje lepszy sposób, aby napisać tę funkcję:

'use strict'; 
addElement("first") 
.then(x => {return addElement("second")}) 
.then(x => { return addElement("third")}) 
.then(x => { return addElement("fourth")}) 

function addElement(elementText){ 
    var myPromise = new Promise(function(resolve,reject){ 
     setTimeout(function(){ 
      var element=document.createElement('H1'); 
      element.innerText = `${elementText} ${Date.now()}`; 
      document.body.appendChild(element); 
      resolve(); 
     }, Math.random() * 2000); 
    }); 
return myPromise; 
} 
+3

twoje funkcje strzałek mogłyby zostać uproszczone - '.Następnie (x => addElement („drugi”))' - podobnie mogło być w użyciu funkcje strzałek w 'addElement' - ale nie jestem pewien, dlaczego myślisz tworzysz "wiele nowych zamknięć" –

+0

Wpadłem również na ten problem i skończyło się na używaniu 'bind' zamiast tego, chociaż wydaje się być tak samo nieprzyzwoite (ale unika się dodatkowych wrapperów funkcji):' .then (addElement.bind (null, "second")) ', itp. –

+0

Zastanawiam się, czy istnieją tutaj nadmiarowe obiekty obietnic. Coś takiego jak Ty stworzysz 6 obiektów obietnic, gdy 3 będzie wystarczające? Czy stworzyłeś już obiekt obietnicy, którego nie możesz użyć ponownie? Pozwól mi pomyśleć, że mogę się mylić. – Nishant

Odpowiedz

3

Twój kod wygląda blisko najlepiej można uzyskać tutaj. Obietnice mogą być dziwną strukturą, do której można się przyzwyczaić, zwłaszcza że pisanie obiecanego kodu często kończy się wbudowaniem funkcji w inną funkcję. Jak widać here, jest to całkiem popularne frazowanie do użycia. Możliwe są tylko dwie zmiany stylistyczne. Po pierwsze, myPromise jest niepotrzebny i służy tylko do dodania mylącej dodatkowej linii kodu. Łatwiej jest tylko zwrócić obietnicę bezpośrednio. Po drugie, możesz użyć funkcji bindowania, aby uprościć połączenia na początku. Może nie znajdować się wewnątrz samej funkcji, ale eliminuje kilka zamknięć. Obie zmiany są przedstawione poniżej:

'use strict'; 
addElement("first") 
.then(addElement.bind(null,"second")) 
.then(addElement.bind(null,"third")) 
.then(addElement.bind(null,"fourth")) 

function addElement(elementText){ 
    return new Promise(function(resolve,reject){ 
     setTimeout(function(){ 
      var element=document.createElement('H1'); 
      element.innerText = `${elementText} ${Date.now()}`; 
      document.body.appendChild(element); 
      resolve(); 
     }, Math.random() * 2000); 
    }); 
} 

Warto zwrócić uwagę, że jeśli byli gotowi do restrukturyzacji trochę, nieco bardziej atrakcyjny design miałaby postać:

'use strict'; 
var myWait = waitRand.bind(null,2000); 
myWait 
    .then(addElement.bind(null, "first")) 
    .then(myWait) 
    .then(addElement.bind(null, "second")) 
    .then(myWait) 
    .then(addElement.bind(null, "third")) 

function waitRand(millis) { 
    return new Promise((resolve, reject) => { 
    setTimeout(resolve, Math.random() * millis); 
    } 
} 

function addElement(elementText) { 
    var element = document.createElement('h1'); 
    element.innerText = `${elementText} ${Date.now()}`; 
    document.body.appendChild(element); 
} 

ta handluje długość łańcucha obietnicy dla jasności, a także mając nieco mniej zagnieżdżonych poziomów.

6

@TheToolBox ma dla Ciebie dobrą odpowiedź.

Dla przyjemności pokażę Wam alternatywną technikę wykorzystującą generatory, która czerpie inspirację z coroutines.

Promise.prototype.bind = Promise.prototype.then; 

const coro = g => { 
    const next = x => { 
    let {done, value} = g.next(x); 
    return done ? value : value.bind(next); 
    } 
    return next(); 
} 

Korzystanie że Twój kod będzie wyglądać następująco

const addElement = elementText => 
    new Promise(resolve => { 
    setTimeout(() => { 
     var element = document.createElement('H1'); 
     element.innerText = `${elementText} ${Date.now()}`; 
     document.body.appendChild(element); 
     resolve(); 
    }, Math.random() * 2000); 
    }); 

coro(function*() { 
    yield addElement('first'); 
    yield addElement('second'); 
    yield addElement('third'); 
    yield addElement('fourth'); 
}()); 

Istnieje kilka bardzo ciekawych rzeczy można zrobić za pomocą generatorów z obietnic. Nie są tu od razu widoczne, ponieważ obietnica addElement nie rozwiązuje żadnych rzeczywistych wartości.


Jeśli rzeczywiście resolve pewne wartości, można zrobić coś takiego

// sync 
const appendChild = (x,y) => x.appendChild(y); 

// sync 
const createH1 = text => { 
    var elem = document.createElement('h1'); 
    elem.innerText = `${text} ${Date.now()}`; 
    return elem; 
}; 

// async 
const delay = f => 
    new Promise(resolve => { 
    setTimeout(() => resolve(f()), Math.random() * 2000); 
    }); 

// create generator; this time it has a name and accepts an argument 
// mix and match sync/async as needed 
function* renderHeadings(target) { 
    appendChild(target, yield delay(() => createH1('first'))); 
    appendChild(target, yield delay(() => createH1('second'))); 
    appendChild(target, yield delay(() => createH1('third'))); 
    appendChild(target, yield delay(() => createH1('fourth'))); 
} 

// run the generator; set target to document.body 
coro(renderHeadings(document.body)); 

Warto zauważyć, createH1 i appendChild funkcje są synchroniczne. To podejście skutecznie pozwala łączyć normalne funkcje i zamazywać linie między tym, co jest synchronizowane, a tym, co jest asynchroniczne. Działa również/zachowuje się dokładnie tak, jak pierwotnie opublikowany kod.

Tak, ten ostatni przykład kodu może być nieco bardziej interesujący.


Wreszcie

Jeden wyraźną przewagę współprogram ma nad .then łańcuchowych, jest to, że wszystkie obietnice rozwiązanych można uzyskać wewnątrz tego samego zakresu.

Porównaj .then łańcuchy ...

op1() 
    .then(x => op2(x)) 
    .then(y => op3(y)) // cannot read x here 
    .then(z => lastOp(z)) // cannot read x or y here 

do współprogram ...

function*() { 
    let x = yield op1(); // can read x 
    let y = yield op2(); // can read x and y here 
    let z = yield op3(); // can read x, y, and z here 
    lastOp([x,y,z]);  // use all 3 values ! 
} 

Oczywiście istnieją workarounds za to za pomocą obietnic, ale oh boy robi się brzydkie szybko ...


Jeśli jesteś zainteresowany wykorzystaniem generatorów w ten sposób, bardzo polecam Ci zrealizowanie projektu co.

A oto artykuł, , od twórcy co, @tj.

W każdym razie, mam nadzieję, że zabawa uczenia się o innych technikach ^__^

3

Można uprościć korzystanie z funkcji poprzez addElement() powrót funkcji zamiast więc może być bezpośrednio włożona .then() koparki bez konieczność utworzenia anonimowej funkcji:

'use strict'; 
addElement("first")() 
    .then(addElement("second")) 
    .then(addElement("third")) 
    .then(addElement("fourth")) 

function addElement(elementText){ 
    return function() { 
     return new Promise(function(resolve){ 
      setTimeout(function(){ 
       var element=document.createElement('H1'); 
       element.innerText = `${elementText} ${Date.now()}`; 
       document.body.appendChild(element); 
       resolve(); 
      }, Math.random() * 2000); 
     }); 
    } 
} 
1

Niewiele można zrobić, jeśli chodzi o liczbę zamknięć. Zagnieżdżanie funkcji jest czymś, do czego przyzwyczaisz się z js, a kod w pytaniu naprawdę nie jest taki zły.

Jak powiedzieli inni, pisanie addElement() w celu zwrócenia funkcji tworzy lepszy, główny łańcuch obietnic.

Idąc nieco dalej, możesz rozważyć napisanie zwróconej funkcji z wewnętrznym łańcuchem obietnicy, umożliwiając (nieznaczne) rozdzielenie obietnicy od wstawienia elementu DOM. Nie tworzy to więcej i nie zamyka zamknięć, ale jest bardziej składniowo uporządkowany, w szczególności umożliwiając pisanie setTimeout(resolve, Math.random() * 2000);.

'use strict'; 
addElement("first") 
.then(addElement("second")) 
.then(addElement("third")) 
.then(addElement("fourth")); 

function addElement(elementText) { 
    return function() { 
     return new Promise(function(resolve, reject) { 
      setTimeout(resolve, Math.random() * 2000); 
     }).then(function() { 
      var element = document.createElement('H1'); 
      document.body.appendChild(element); 
      element.innerText = `${elementText} ${Date.now()}`; 
     }); 
    }; 
} 

Może to tylko ja, ale uważam to o wiele bardziej przyjemne dla oka, choć kosztem dodatkowego .Następnie(), stąd dodatkowa obietnica, za addElement().

Uwaga: Jeśli chcesz rozwiązać obietnicę z wartością, nadal możesz skorzystać z tej możliwości, zwracając wartość z wywołania zwrotnego związanego z łańcuchem.

Idąc jeszcze dalej, jeśli chcesz wstawione elementy pojawiają się w zażądał zamówienie, nie kolejność ustalona przez osady obietnicy, to można utworzyć/wstawić elementy synchronicznie, i wypełnić je asynchronicznie:

function addElement(elementText) { 
    var element = document.createElement('H1'); 
    document.body.appendChild(element); 
    return function() { 
     return new Promise(function(resolve, reject) { 
      setTimeout(resolve, Math.random() * 2000); 
     }).then(function() { 
      element.innerText = `${elementText} ${Date.now()}`; 
     }); 
    }; 
} 

Wszystko, co było konieczne, to przesunięcie dwóch linii w obrębie addElement(), aby zmienić czas wstawiania, pozostawiając linię element.innerText = ... tam, gdzie była. Jest to możliwe niezależnie od tego, czy wybierzesz wewnętrzny łańcuch obietnic.

+0

Twoje pierwsze wywołanie funkcji 'addElement()' wymaga innego '()' po tym, aby wywołać funkcję wewnętrzną (jak pokazano w mojej odpowiedzi). I nie potrzebujesz tej dodatkowej obietnicy, aby przedmioty zostały umieszczone w żądanym zamówieniu. Jest to już zrobione przez inne rozwiązania. Wewnętrzne funkcje są już wywoływane w żądanym zamówieniu. – jfriend00

3

Nie jestem pewien, dlaczego inni pominąć proste wyjście, można po prostu użyć tablicę i reduce metodę

let promise, inputArray = ['first', 'second', 'third', 'fourth']; 

promise = inputArray.reduce((p, element) => p.then(() => addElement(element)), Promise.resolve()); 
+1

Nie trzeba przypisywać 'p = p.then (...' –

0

Napisałem dwie metody tutaj:

Sequence = { 
    all(steps) { 
     var promise = Promise.resolve(), 
      results = []; 

     const then = i => { 
      promise = promise.then(() => { 
       return steps[ i ]().then(value => { 
        results[ i ] = value; 
       }); 
      }); 
     }; 

     steps.forEach((step, i) => { 
      then(i); 
     }); 

     return promise.then(() => Promise.resolve(results)); 
    }, 
    race(steps) { 
     return new Promise((resolve, reject) => { 
      var promise = Promise.reject(); 

      const c = i => { 
       promise = promise.then(value => { 
        resolve(value); 
       }).catch(() => { 
        return steps[ i ](); 
       }); 
      }; 

      steps.forEach((step, i) => { 
       c(i); 
      }); 

      promise.catch(() => { 
       reject(); 
      }); 
     }); 
    } 
}; 

Sequence.all będzie uruchom funkcje w sekwencji, dopóki wszystkie obietnice w argumentach nie zostaną rozwiązane. I zwróć inny obiekt Promise z argumentami jako tablicą wypełnioną wszystkimi rozwiązanymi wartościami w sekwencji.

Sequence.all([() => { 
    return Promise.resolve('a'); 
},() => { 
    return Promise.resolve('b'); 
} ]).then(values => { 
    console.log(values); // output [ 'a', 'b' ] 
}); 

Sequence.race będzie uruchamiać funkcje w sekwencji i przestanie działać, gdy jeden obiekt obietnicy zostanie rozwiązany.

Sequence.race([() => { 
    return Promise.reject('a'); 
},() => { 
    return Promise.resolve('b'); 
},() => { 
    return Promise.resolve('c'); 
} ]).then(values => { 
    console.log(values); // output [ 'a' ] 
});