Το αντικείμενο Promise αντιπροσωπεύει την τελική ολοκλήρωση (ή την αποτυχία) μιας ασύγχρονης λειτουργίας και την τιμή που προκύπτει από αυτή.
Αυτό είναι ένα δύσκολο θέμα για πολλούς ανθρώπους, ειδικά αν ξέρεις προγραμματισμό σε μια γλώσσα που είναι εντελώς σύγχρονη. Αν νιώθεις να πνίγεσαι, ή αν θα ήθελες να μάθεις περισσότερα για concurrency και parallelism, δες (μέσω go.dev) ή δες απευθείας στο vimeo και διάβασε τις διαφάνειες της εξαιρετικής ομιλίας "Concurrency is not parallelism".
Ένα Promise έχει τρεις καταστάσεις:
Όταν δημιουργείται, ένα Promise είναι pending. Κάποια στιγμή στο μέλλον μπορεί να resolve ή να reject. Μόλις ένα Promise γίνει resolved ή rejected μία φορά, δεν μπορεί ποτέ να γίνει resolved ή rejected ξανά, ούτε μπορεί να αλλάξει η κατάστασή του.
Με άλλα λόγια:
Ένα Promise μπορεί να επιλυθεί με διάφορους τρόπους:
// Creates a promise that is immediately resolved
Promise.resolve(value);
// Creates a promise that is immediately resolved
new Promise((resolve) => {
resolve(value);
});
// Chaining a promise leads to a resolved promise
somePromise.then(() => {
// ...
return value;
});
Στα παραπάνω παραδείγματα το value μπορεί να είναι οτιδήποτε, ακόμη και ένα σφάλμα, undefined, null ή ένα άλλο Promise.
Συνήθως θέλεις να επιλύσεις το Promise με μια τιμή που δεν είναι σφάλμα.
Ένα Promise μπορεί να απορριφθεί με διάφορους τρόπους:
// Creates a promise that is immediately rejected
Promise.reject(reason)
// Creates a promise that is immediately rejected
new Promise((_, reject) {
reject(reason)
})
// Chaining a promise with an error leads to a rejected promise
somePromise.then(() => {
// ...
throw reason
})
Στα παραπάνω παραδείγματα ο reason μπορεί να είναι οτιδήποτε, ακόμη και ένα σφάλμα, undefined ή null.
Συνήθως θέλεις να απορρίψεις το Promise με ένα σφάλμα.
Ένα Promise μπορεί να συνεχιστεί με μια μελλοντική ενέργεια μόλις επιλυθεί ή απορριφθεί.
promise.then() καλείται μόλις το promise επιλυθείpromise.catch() καλείται μόλις το promise απορριφθείpromise.finally() καλείται μόλις το promise είτε επιλυθεί είτε απορριφθείΚάθε Promise είναι "thenable".
Αυτό σημαίνει ότι υπάρχει μια συνάρτηση then που θα εκτελεστεί μόλις επιλυθεί το αρχικό Promise.
Δεδομένου του promise.then(onResolved), το callback onResolved λαμβάνει την τιμή με την οποία επιλύθηκε το αρχικό Promise.
Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.
Η επιστροφή ενός value από το then επιλύει το αλυσιδωτό Promise.
Το πέταγμα ενός reason στο then απορρίπτει το αλυσιδωτό Promise.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
});
const promise2 = promise1.then(function (value) {
console.log(value);
// expected output: "Success!"
return true;
});
Αυτό θα τυπώσει στο log το "Success!" μετά από περίπου 1000 ms.
Η κατάσταση και η τιμή του promise1 θα είναι resolved και "Success!".
Η κατάσταση και η τιμή του promise2 θα είναι resolved και true.
Υπάρχει και ένα δεύτερο όρισμα που εκτελείται όταν το αρχικό Promise απορριφθεί.
Δεδομένου του promise.then(onResolved, onRejected), το callback onResolved λαμβάνει την τιμή με την οποία επιλύθηκε το αρχικό Promise, ή το callback onRejected λαμβάνει τον λόγο για τον οποίο απορρίφθηκε το Promise.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return true;
}
function shout(reason) {
console.error(reason.toUpperCase());
return false;
}
const promise2 = promise1.then(log, shout);
"Success!" μετά από περίπου 1000 ms.
promise1 θα είναι resolved και "Success!".promise2 θα είναι resolved και true."NOPE!".
promise1 θα είναι rejected και Nope!.promise2 θα είναι resolved και false.Είναι σημαντικό να καταλάβεις ότι, λόγω των κανόνων του κύκλου ζωής, όταν κάνει reject, το resolve που έρχεται ~1000 ms αργότερα αγνοείται σιωπηλά, αφού η εσωτερική κατάσταση δεν μπορεί να αλλάξει από τη στιγμή που θα απορριφθεί ή θα επιλυθεί.
Είναι σημαντικό να καταλάβεις ότι η επιστροφή μιας τιμής από ένα Promise το επιλύει, και το πέταγμα μιας τιμής το απορρίπτει.
Όταν το promise1 επιλυθεί και υπάρχει συνδεδεμένο onResolved: then(onResolved), τότε αυτή η συνέχεια είναι ένα νέο Promise που μπορεί να επιλυθεί ή να απορριφθεί.
Όταν το promise1 απορριφθεί αλλά υπάρχει συνδεδεμένο onRejected: then(, onRejected), τότε αυτή η συνέχεια είναι ένα νέο Promise που μπορεί να επιλυθεί ή να απορριφθεί.
Μερικές φορές θέλεις να συλλάβεις σφάλματα και να συνεχίσεις μόνο όταν το αρχικό Promise απορριφθεί.
Δεδομένου του promise.catch(onCatch), το callback onCatch λαμβάνει τον λόγο για τον οποίο απορρίφθηκε το αρχικό Promise.
Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.
Η επιστροφή ενός value από το catch επιλύει το αλυσιδωτό Promise.
Το πέταγμα ενός reason στο catch απορρίπτει το αλυσιδωτό Promise.
const promise1 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve('Success!');
}, 1000);
if (Math.random() < 0.5) {
reject('Nope!');
}
});
function log(value) {
console.log(value);
return 'done';
}
function recover(reason) {
console.error(reason.toUpperCase());
return 42;
}
const promise2 = promise1.catch(recover).then(log);
Σε περίπου 1/2 των περιπτώσεων, αυτό θα τυπώσει στο log το "Success!" μετά από περίπου 1000 ms.
Στις άλλες 1/2 των περιπτώσεων, αυτό θα τυπώσει αμέσως στο log το 42.
promise1 επιλυθεί, το catch παραλείπεται και φτάνει στο then, το οποίο τυπώνει την τιμή στο log.
promise1 θα είναι resolved και "Success!".promise2 θα είναι resolved και "done";promise1 απορριφθεί, το catch εκτελείται, το οποίο επιστρέφει μια τιμή, και έτσι η αλυσίδα πλέον είναι resolved, και φτάνει στο then, το οποίο τυπώνει την τιμή στο log.
promise1 θα είναι rejected και "Nope!".promise2 θα είναι resolved και "done";Μερικές φορές θέλεις να εκτελέσεις κώδικα αφού ένα Promise ολοκληρωθεί, ανεξάρτητα από το αν επιλυθεί ή απορριφθεί.
Δεδομένου του promise.finally(onSettled), το callback onSettled δεν λαμβάνει τίποτα.
Αυτό θα επιστρέφει πάντα ένα νέο αλυσιδωτό Promise.
Η επιστροφή ενός value από το finally αντιγράφει την κατάσταση και την τιμή από το αρχικό Promise, αγνοώντας το value.
Το πέταγμα ενός reason στο finally απορρίπτει το αλυσιδωτό Promise, αντικαθιστώντας οποιαδήποτε κατάσταση και τιμή ή λόγο από το αρχικό Promise.
Διάφορες από τις μεθόδους μαζί:
const myPromise = new Promise(function (resolve, reject) {
const sampleData = [2, 4, 6, 8];
const randomNumber = Math.round(Math.random() * 5);
if (sampleData[randomNumber]) {
resolve(sampleData[randomNumber]);
} else {
reject('Sampling did not result in a sample');
}
});
const finalPromise = myPromise
.then(function (sampled) {
// If the random number was 0, 1, 2, or 3, this will be
// reached and the number 2, 4, 6, or 8 will be logged.
console.log(`Sampled data: ${sampled}`);
return 'yay';
})
.catch(function (reason) {
// If the random number was 4 or 5, this will be reached and
// reason will be "An error occurred". The entire chain will
// then reject with an Error with the reason as message.
throw new Error(reason);
})
.finally(function () {
// This will always log after either the sampled data is
// logged or the error is raised.
console.log('Promise completed');
});
randomNumber είναι 0-3:
myPromise θα επιλυθεί με την τιμή 2, 4, 6, or 8
finalPromise θα επιλυθεί με την τιμή 'yay'
Sampled data: ...Promise completedrandomNumber είναι 4-5:
myPromise θα απορριφθεί με τον λόγο 'Sampling did not result in a sample'
finalPromise θα απορριφθεί με τον λόγο Error('Sampling did not result in a sample')
Promise completed"uncaught rejected promise: Error('Sampling did not result in a sample')" στο logΌπως φαίνεται παραπάνω, το reject δουλεύει με συμβολοσειρά, και ένα Promise μπορεί επίσης να απορριφθεί με ένα Error.
Αν η αλυσιδωτή σύνδεση των Promises ή η γενικότερη χρήση τους δεν είναι ξεκάθαρη, ο οδηγός στο MDN είναι μια καλή πηγή για να διαβάσεις.
Σε αυτή την άσκηση, θα φτιάξεις μια TranslationService που παρέχει βασικές υπηρεσίες μετάφρασης σε δωρεάν μέλη και προηγμένη μετάφραση σε premium μέλη με εγγυήσεις ποιότητας.
Το API
Βρήκες ένα API μετάφρασης από το διάστημα που εκτελεί οποιοδήποτε request μετάφρασης σε λογικό χρόνο.
Θέλεις να το εκμεταλλευτείς.
Οι μεταφραστές του διαστήματος είναι εξαιρετικά ιδιότροποι και μισούν την επανάληψη, οπότε παρέχουν και δορυφόρους API storage όπου μπορείς να κάνεις fetch παλιές μεταφράσεις χωρίς να τους ενοχλήσεις.
Λήψη μιας μετάφρασης
Το api.fetch(text) ανακτά μια μετάφραση του text από το API storage και επιστρέφει ένα promise που παρέχει δύο τιμές:
translation: η ίδια η μετάφρασηquality: η ποιότητα εκφρασμένη ως αριθμόςΑν μια μετάφραση δε βρεθεί στο API storage, το API πετάει ένα σφάλμα NotAvailable.
Οι μεταφράσεις μπορούν να προστεθούν με τη μέθοδο api.request.
Αν το 'text' δε μπορεί να μεταφραστεί, το API πετάει ένα σφάλμα Untranslatable.
api.fetch('jIyaj');
// => Promise({ resolved: 'I understand' })
Ζήτηση μιας μετάφρασης
Κάποιες μεταφράσεις σίγουρα υπάρχουν, αλλά δεν έχουν προστεθεί ακόμα στο API storage. Αυτή είναι η διαφορά ανάμεσα στο NotAvailable (δεν είναι στο storage, αλλά μπορεί να ζητηθεί) και το Untranslatable (δε μπορεί να μεταφραστεί).
Το api.request(text, callback) ζητά να εκτελεστεί μια μετάφραση του text και να προστεθεί στο API storage.
Όταν ολοκληρωθεί, καλείται η συνάρτηση callback.
callback περνιέται το undefined: αυτό δείχνει ότι η μετάφραση ήταν επιτυχής και είναι προσβάσιμη με τη μέθοδο api.fetch.callback περνιέται ένα error: αυτό δείχνει ότι κάτι πήγε στραβά.
Το API του διαστήματος είναι ασταθές, που σημαίνει ότι αποτυγχάνει συχνά.
Αν συμβεί αυτό, δεν πειράζει να καλέσεις ξανά το api.request.api.request('majQa’', callback);
// => undefined
//
// later: the passed callback is called with undefined
// because it was successful.
⚠ Προσοχή! ⚠
Το API κάνει τη μαγεία του τηλεμεταφέροντας τους διάφορους μεταφραστές όταν φτάνει ένα request.
Αυτή είναι μια πολύ δαπανηρή ενέργεια, οπότε δεν πρέπει να καλείται όταν μια μετάφραση είναι διαθέσιμη.
Δυστυχώς, δε διαβάζουν όλοι το εγχειρίδιο, γι' αυτό υπάρχει ένα σύστημα που αποβάλλει τους κακόβουλους χρήστες.
Αν κληθεί ένα api.request για text που είναι διαθέσιμο, το API πετάει ένα AbusiveClientError για αυτή την κλήση, και για κάθε επόμενη κλήση.
Βεβαιώσου ότι ποτέ δε ζητάς μετάφραση αν κάτι έχει ήδη μεταφραστεί.
Η δωρεάν υπηρεσία παρέχει μόνο μεταφράσεις που βρίσκονται αυτή τη στιγμή στο API storage.
Υλοποίησε μια μέθοδο free(text) που παρέχει στα δωρεάν μέλη μεταφράσεις που υπάρχουν ήδη στο API storage.
Αγνόησε την ποιότητα και προώθησε τυχόν σφάλματα που πετάει το API.
api.fetch (το api.fetch επιστρέφει ένα promise)service.free('jIyaj');
// => Promise<...> resolves "I understand."
service.free("jIyajbe'");
// => Promise<...> rejects Error("Not yet translated")
Υλοποίησε μια μέθοδο batch([text, text, ...]) για δωρεάν μέλη που μεταφράζει έναν πίνακα κειμένων με τη δωρεάν υπηρεσία, επιστρέφοντας όλες τις μεταφράσεις ή ένα μόνο σφάλμα.
BatchIsEmpty αν δε δοθούν κείμεναservice.batch(['jIyaj', "majQa'"]);
// => Promise<...> resolves ["I understand.", "Well done!"]
service.batch(['jIyaj', "jIyajbe'"]);
// => Promise<...> rejects new Error("Not yet translated")
service.batch([]);
// => Promise<...> rejects BatchIsEmpty()
Υλοποίησε μια μέθοδο premium χρήστη request(text), που ζητά να προστεθεί μια μετάφραση στο API storage.
Το αίτημα πρέπει να επαναλαμβάνεται αυτόματα αν προκύψει αποτυχία.
Δεν πρέπει να κάνει περισσότερες από 3 κλήσεις για το ίδιο αίτημα (μην αναστατώσεις τους μεταφραστές του διαστήματος!!!).
api.request δεν επιστρέψει σφάλμα, επιλύεται με undefined
api.request επιστρέψει σφάλμα, επανάλαβε έως δύο φορέςservice.request("jIyajbe'");
// => Promise<...> resolves (with nothing), can now be retrieved using the fetch API
Υλοποίησε μια μέθοδο premium χρήστη premium(text, quality) για να ανακτήσεις μια μετάφραση.
Αν μια μετάφραση είναι NotAvailable, ζήτησε τη μετάφραση και ανακτήσέ την αφού προστεθεί στο API storage.
Η μέθοδος πρέπει να επιστρέφει τη μετάφραση μόνο αν πληροί ένα συγκεκριμένο όριο quality.
api.fetch επιλυθεί, έλεγξε την ποιότητα πριν επιλυθείapi.fetch απορριφθεί, ζήτησε τη μετάφραση αντ' αυτούapi.request απορριφθεί, προώθησε το σφάλμαservice.premium("jIyajbe'", 100);
// => Promise<...> resolves "I don't understand."
service.premium("'arlogh Qoylu'pu'?", 100);
// => Promise<...> rejects QualityThresholdNotMet()
service.premium("'arlogh Qoylu'pu'?", 40);
// => Promise<...> resolves "What time is it?"
Σημείωση
Η σωστή μετάφραση του 'arlogh Qoylu'pu'? είναι How many times has it been heard?.
Γράψου στο Exercism για να μάθεις και να κατακτήσεις JavaScript με 37 έννοιες159 ασκήσεις και πραγματική καθοδήγηση από ανθρώπους, όλα δωρεάν.