Εάν ξεκινάτε μόνο με τον προγραμματισμό, το πιθανότερο είναι ότι σκέφτεστε τα προγράμματα ως ένα σύνολο διαδοχικών μπλοκ λογικής, όπου κάθε μπλοκ κάνει ένα συγκεκριμένο πράγμα και περνά το αποτέλεσμά του, ώστε το επόμενο μπλοκ να μπορεί να τρέξει και ούτω καθεξής, και για το ως επί το πλείστον έχετε δίκιο, τα περισσότερα προγράμματα εκτελούνται με διαδοχικό τρόπο, αυτό το μοντέλο μας επιτρέπει να δημιουργήσουμε προγράμματα που είναι απλά στη σύνταξη και τη συντήρηση. Ωστόσο, υπάρχουν συγκεκριμένες περιπτώσεις χρήσης όπου αυτό το διαδοχικό μοντέλο δεν θα λειτουργούσε ή δεν θα ήταν το βέλτιστο. Για παράδειγμα, εξετάστε μια εφαρμογή ανάγνωσης βιβλίων. Αυτή η εφαρμογή διαθέτει μερικές προηγμένες λειτουργίες, όπως εύρεση όλων των εμφανίσεων μιας λέξης, πλοήγηση μεταξύ σελιδοδεικτών και παρόμοια. Τώρα φανταστείτε ότι ο χρήστης αυτή τη στιγμή διαβάζει ένα μεγάλο βιβλίο και αποφασίζει να αναζητήσει όλες τις εμφανίσεις μιας κοινής λέξης όπως το "The". Η εφαρμογή κανονικά θα χρειαστεί μερικά δευτερόλεπτα για να βρει και να ευρετηριάσει όλες τις εμφανίσεις αυτής της λέξης. Σε ένα διαδοχικό πρόγραμμα, ο χρήστης δεν μπορεί να αλληλεπιδράσει με την εφαρμογή (να αλλάξει τη σελίδα ή να επισημάνει ένα κείμενο) έως ότου ολοκληρωθεί η λειτουργία αναζήτησης. Ας ελπίσουμε ότι μπορείτε να δείτε ότι αυτή δεν είναι η βέλτιστη εμπειρία χρήστη!
Το διάγραμμα απεικονίζει μια τυπική ροή εκτέλεσης της εφαρμογής ανάγνωσης βιβλίων. Εάν ο χρήστης ξεκινήσει μια μακροχρόνια λειτουργία (σε αυτήν την περίπτωση η αναζήτηση για όλες τις εμφανίσεις του "the" σε ένα μεγάλο βιβλίο), η εφαρμογή "παγώνει" για όλη τη διάρκεια αυτής της λειτουργίας. Σε αυτήν την περίπτωση, ο χρήστης θα συνεχίσει να κάνει κλικ στο επόμενο κουμπί σελιδοδείκτη χωρίς αποτέλεσμα μέχρι να ολοκληρωθεί η λειτουργία αναζήτησης και όλες οι λειτουργίες θα τεθούν σε ισχύ αμέσως δίνοντας στον τελικό χρήστη την αίσθηση μιας καθυστερημένης εφαρμογής.
Ίσως έχετε παρατηρήσει ότι αυτό το παράδειγμα δεν αντιστοιχεί πραγματικά στο διαδοχικό μοντέλο που παρουσιάσαμε νωρίτερα. Αυτό συμβαίνει επειδή οι λειτουργίες εδώ είναι ανεξάρτητες μεταξύ τους. Ο χρήστης δεν χρειάζεται να γνωρίζει τον αριθμό των εμφανίσεων του "the" για να πλοηγηθεί στον επόμενο σελιδοδείκτη, επομένως η σειρά εκτέλεσης των λειτουργιών δεν είναι πραγματικά σημαντική. Δεν χρειάζεται να περιμένουμε το τέλος της λειτουργίας αναζήτησης για να μπορέσουμε να πλοηγηθούμε στον επόμενο σελιδοδείκτη. Μια πιθανή βελτίωση της προηγούμενης ροής εκτέλεσης βασίζεται σε αυτή τη λογική: μπορούμε να εκτελέσουμε τη λειτουργία μακράς αναζήτησης στο παρασκήνιο, να προχωρήσουμε σε τυχόν εισερχόμενες λειτουργίες και μόλις ολοκληρωθεί η μεγάλη λειτουργία, μπορούμε απλώς να ειδοποιήσουμε τον χρήστη. Η ροή εκτέλεσης γίνεται ως εξής:
Με αυτήν τη ροή εκτέλεσης, η εμπειρία χρήστη βελτιώνεται σημαντικά. Τώρα ο χρήστης μπορεί να ξεκινήσει μια μακροχρόνια λειτουργία, να προχωρήσει στη χρήση της εφαρμογής κανονικά και να ειδοποιηθεί μόλις ολοκληρωθεί η λειτουργία. Αυτή είναι η βάση του ασύγχρονου προγραμματισμού.
Η Javascript, μεταξύ άλλων γλωσσών, υποστηρίζει αυτό το στυλ ασύγχρονου προγραμματισμού παρέχοντας εκτεταμένα API για να επιτύχετε σχεδόν οποιαδήποτε ασύγχρονη συμπεριφορά μπορείτε να σκεφτείτε. Στο τέλος της ημέρας, η Javascript θα πρέπει να είναι εγγενώς μια ασύγχρονη γλώσσα. Αν αναφερθούμε στο προηγούμενο παράδειγμα, η ασύγχρονη λογική βρίσκεται στη βάση όλων των εφαρμογών αλληλεπίδρασης χρήστη και η Javascript δημιουργήθηκε κυρίως για να χρησιμοποιείται στο πρόγραμμα περιήγησης όπου τα περισσότερα προγράμματα αφορούν την απόκριση σε ενέργειες χρήστη.
Τα παρακάτω θα σας δώσουν έναν σύντομο οδηγό για το Asynchronous Javascript:
Επανακλήσεις
Σε ένα τυπικό πρόγραμμα, θα βρείτε συνήθως μια σειρά από λειτουργίες. Για να χρησιμοποιήσουμε μια συνάρτηση, την καλούμε με ένα σύνολο παραμέτρων. Ο κωδικός συνάρτησης θα εκτελεστεί και θα επιστρέψει ένα αποτέλεσμα, τίποτα το ασυνήθιστο. Ο ασύγχρονος προγραμματισμός μετατοπίζει αυτή τη λογική ελαφρώς. Επιστρέφοντας στο παράδειγμα της εφαρμογής ανάγνωσης βιβλίων, δεν μπορούμε να χρησιμοποιήσουμε μια κανονική συνάρτηση για να εφαρμόσουμε τη λογική της λειτουργίας αναζήτησης, καθώς η λειτουργία διαρκεί άγνωστο χρονικό διάστημα. Μια κανονική συνάρτηση θα επιστρέψει βασικά πριν γίνει η λειτουργία και αυτή δεν είναι η συμπεριφορά που περιμένουμε. Η λύση είναι να καθορίσετε μια άλλη συνάρτηση που θα εκτελεστεί μόλις ολοκληρωθεί η λειτουργία αναζήτησης. Αυτό μοντελοποιεί την περίπτωση χρήσης μας, καθώς το πρόγραμμά μας μπορεί να συνεχίσει κανονικά τη ροή του και μόλις ολοκληρωθεί η λειτουργία αναζήτησης, η καθορισμένη λειτουργία θα εκτελεστεί για να ειδοποιήσει τον χρήστη για τα αποτελέσματα αναζήτησης. Αυτή η συνάρτηση είναι αυτό που ονομάζουμε συνάρτηση επανάκλησης:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Αρχικά, ορίζουμε τη λειτουργία αναζήτησης, searchOccurrences. Χρειάζεται η λέξη για αναζήτηση και μια δεύτερη παράμετρος "callback" που θα είναι η λειτουργία που θα εκτελεστεί μόλις ολοκληρωθεί η λειτουργία αναζήτησης. Η λειτουργία αναζήτησης κρατήθηκε σκόπιμα αφηρημένη, χρειάζεται μόνο να εστιάσουμε στα δύο πιθανά αποτελέσματά της: η πρώτη περίπτωση είναι όπου όλα πήγαν επιτυχώς και έχουμε το αποτέλεσμα της αναζήτησης στη μεταβλητή αποτελέσματος. Σε αυτήν την περίπτωση, πρέπει απλώς να καλέσουμε τη συνάρτηση επανάκλησης με τις ακόλουθες παραμέτρους: η πρώτη παράμετρος είναι null που σημαίνει ότι δεν έχει συμβεί κανένα σφάλμα, η δεύτερη παράμετρος είναι η λέξη που αναζητήθηκε και η τρίτη και ίσως πιο σημαντική παράμετρος από τις τρεις, είναι το αποτέλεσμα της επιχείρησης αναζήτησης.
Η δεύτερη περίπτωση είναι όπου παρουσιάζεται ένα σφάλμα, αυτή είναι επίσης μια περίπτωση όπου γίνεται η εκτέλεση της λειτουργίας αναζήτησης και πρέπει να καλέσουμε τη συνάρτηση επανάκλησης. Χρησιμοποιούμε ένα μπλοκ try and catch για να υποκλέψουμε οποιοδήποτε σφάλμα και απλώς καλούμε τη συνάρτηση επανάκλησης με το αντικείμενο σφάλματος από το μπλοκ catch.
Στη συνέχεια ορίσαμε τη συνάρτηση επανάκλησης, handleSearchOccurrences, κρατήσαμε τη λογική της αρκετά απλή. Είναι απλώς θέμα εκτύπωσης ενός μηνύματος στην κονσόλα. Ελέγχουμε πρώτα την παράμετρο "err" για να δούμε αν παρουσιάστηκε κάποιο σφάλμα στην κύρια συνάρτηση. Σε αυτήν την περίπτωση, απλώς ενημερώνουμε τον χρήστη ότι η λειτουργία αναζήτησης έληξε με σφάλμα. Εάν δεν παρουσιάστηκαν σφάλματα, εκτυπώνουμε ένα μήνυμα με το αποτέλεσμα της λειτουργίας αναζήτησης.
Τέλος, ονομάζουμε τη συνάρτηση searchOccurrences με τη λέξη «το». Η λειτουργία θα εκτελείται πλέον κανονικά χωρίς να μπλοκάρει το κύριο πρόγραμμα και μόλις ολοκληρωθεί η αναζήτηση, θα εκτελεστεί η επανάκληση και θα λάβουμε το μήνυμα αποτελέσματος είτε με το αποτέλεσμα αναζήτησης είτε με το μήνυμα σφάλματος.
Είναι σημαντικό να αναφέρουμε εδώ ότι έχουμε πρόσβαση μόνο στη μεταβλητή αποτελέσματος εντός του main και των συναρτήσεων επανάκλησης. Αν δοκιμάσουμε κάτι σαν αυτό:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
το αποτέλεσμα της εκτύπωσης θα ήταν απροσδιόριστο επειδή τα προγράμματα δεν περιμένουν να εκτελεστεί η λειτουργία searchOccurrences. Μεταβαίνει στην επόμενη εντολή που είναι η δήλωση εκτύπωσης πριν από την εκχώρηση της μεταβλητής αποτελέσματος μέσα στην κύρια συνάρτηση. Ως αποτέλεσμα, θα εκτυπώσουμε τη μεταβλητή αποτελέσματος που δεν έχει εκχωρηθεί.
Με βάση λοιπόν αυτή τη λογική, θα πρέπει να κρατήσουμε όλο τον κώδικα που χρησιμοποιεί τη μεταβλητή αποτελέσματος μέσα στη συνάρτηση επανάκλησης. Αυτό μπορεί να μην φαίνεται πρόβλημα τώρα, αλλά θα μπορούσε γρήγορα να κλιμακωθεί σε πραγματικό πρόβλημα. Φανταστείτε την περίπτωση όπου έχουμε μια αλυσίδα ασύγχρονων συναρτήσεων που πρέπει να εκτελούνται με τη σειρά, στην τυπική λογική επανάκλησης, θα εφαρμόσατε κάτι σαν αυτό:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Λάβετε υπόψη ότι κάθε επανάκληση έχει μια παράμετρο σφάλματος και κάθε σφάλμα πρέπει να αντιμετωπίζεται ξεχωριστά. Αυτό καθιστά τον ήδη πολύπλοκο κώδικα παραπάνω ακόμη πιο περίπλοκο και δύσκολο να διατηρηθεί. Ας ελπίσουμε ότι οι Promises είναι εδώ για να λύσουν το πρόβλημα της κόλασης της επανάκλησης, θα το καλύψουμε στη συνέχεια.
Υποσχέσεις
Οι υποσχέσεις χτίζονται πάνω από τις επανακλήσεις και λειτουργούν με παρόμοιο τρόπο. Εισήχθησαν ως μέρος των χαρακτηριστικών του ES6 για να λύσουν μερικά από τα κραυγαλέα ζητήματα με τις επανακλήσεις, όπως η κόλαση της επανάκλησης. Οι υποσχέσεις παρέχουν τις δικές τους λειτουργίες που εκτελούνται με την επιτυχή ολοκλήρωση (επίλυση) και όταν εμφανίζονται σφάλματα (απόρριψη). Το παρακάτω παρουσιάζει το παράδειγμα SearchOccurrences που υλοποιήθηκε με υποσχέσεις:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Ας δούμε τις αλλαγές που εφαρμόσαμε:
Η συνάρτηση searchOccurrences επιστρέφει μια υπόσχεση. Μέσα στην υπόσχεση, διατηρούμε την ίδια λογική: έχουμε δύο συναρτήσεις επίλυσης και απόρριψης που αντιπροσωπεύουν τις επανακλήσεις μας αντί να έχουμε μια ενιαία συνάρτηση επανάκλησης που χειρίζεται τόσο μια επιτυχημένη εκτέλεση όσο και μια εκτέλεση με σφάλματα. Οι υποσχέσεις διαχωρίζουν τα δύο αποτελέσματα και παρέχουν μια καθαρή σύνταξη κατά την κλήση της κύριας συνάρτησης. Η συνάρτηση επίλυσης «αγκιστρώνεται» στην κύρια συνάρτηση χρησιμοποιώντας τη λέξη-κλειδί «τότε». Εδώ απλώς καθορίζουμε τις δύο παραμέτρους της συνάρτησης επίλυσης και εκτυπώνουμε το αποτέλεσμα αναζήτησης. Κάτι παρόμοιο ισχύει για τη συνάρτηση απόρριψης, μπορεί να συνδεθεί χρησιμοποιώντας τη λέξη-κλειδί "catch". Ας ελπίσουμε ότι μπορείτε να εκτιμήσετε τα πλεονεκτήματα που προσφέρουν οι υποσχέσεις όσον αφορά την αναγνωσιμότητα και την καθαριότητα του κώδικα. Εάν εξακολουθείτε να το συζητάτε, δείτε πώς μπορούμε να λύσουμε το πρόβλημα της κόλασης της επανάκλησης ενώνοντας τις ασύγχρονες συναρτήσεις για να εκτελούνται η μία μετά την άλλη:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Το Async/Await είναι η πιο πρόσφατη προσθήκη στην ασύγχρονη ζώνη εργαλείων μας στο Javascript. Παρουσιάστηκαν με το ES8, παρέχουν ένα νέο επίπεδο αφαίρεσης πάνω από ασύγχρονες συναρτήσεις απλώς «αναμένοντας» την εκτέλεση μιας ασύγχρονης λειτουργίας. Η ροή του προγράμματος μπλοκάρεται σε αυτήν την εντολή μέχρι να επιστραφεί ένα αποτέλεσμα από την ασύγχρονη λειτουργία και στη συνέχεια το πρόγραμμα θα προχωρήσει με την επόμενη εντολή. Εάν σκέφτεστε τη σύγχρονη ροή εκτέλεσης, έχετε δίκιο. Κλείσαμε τον κύκλο μας! Το Async/wait προσπαθεί να φέρει την απλότητα του σύγχρονου προγραμματισμού στον ασύγχρονο κόσμο. Λάβετε υπόψη ότι αυτό γίνεται αντιληπτό μόνο στην εκτέλεση και στον κώδικα του προγράμματος. Όλα παραμένουν ίδια κάτω από την κουκούλα, το Async/wait εξακολουθεί να χρησιμοποιεί υποσχέσεις και οι επανακλήσεις είναι τα δομικά τους στοιχεία.
Ας δούμε το παράδειγμά μας και ας το εφαρμόσουμε χρησιμοποιώντας Async/wait:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Ο κώδικάς μας δεν άλλαξε πολύ, το σημαντικό πράγμα που πρέπει να προσέξετε εδώ είναι η λέξη-κλειδί "ασύγχρονη" πριν από τη δήλωση συνάρτησης searchOccurrences. Αυτό δείχνει ότι η συνάρτηση είναι ασύγχρονη. Επίσης, προσέξτε τη λέξη-κλειδί "αναμονή" όταν καλείτε τη συνάρτηση αναζήτησηςΠροσεύματα. Αυτό θα δώσει εντολή στο πρόγραμμα να περιμένει την εκτέλεση της συνάρτησης έως ότου επιστραφεί το αποτέλεσμα πριν το πρόγραμμα μπορέσει να μετακινηθεί στην επόμενη εντολή, με άλλα λόγια, η μεταβλητή αποτελέσματος θα κρατά πάντα την επιστρεφόμενη τιμή της συνάρτησης searchOccurrences και όχι την υπόσχεση η συνάρτηση, υπό αυτή την έννοια, Async/Await δεν έχει κατάσταση σε εκκρεμότητα ως Promises. Μόλις ολοκληρωθεί η εκτέλεση, μεταβαίνουμε στην πρόταση εκτύπωσης και αυτή τη φορά το αποτέλεσμα περιέχει πραγματικά το αποτέλεσμα της λειτουργίας αναζήτησης. Όπως ήταν αναμενόμενο, ο νέος κώδικας έχει την ίδια συμπεριφορά σαν να ήταν σύγχρονος.
Ένα άλλο δευτερεύον πράγμα που πρέπει να θυμάστε είναι ότι εφόσον δεν έχουμε πλέον συναρτήσεις επανάκλησης, πρέπει να χειριστούμε το σφάλμα searchOccurrences μέσα στην ίδια συνάρτηση, καθώς δεν μπορούμε απλώς να μεταδώσουμε το σφάλμα στη συνάρτηση επανάκλησης και να το χειριστούμε εκεί. Εδώ απλώς εκτυπώνουμε ένα μήνυμα σφάλματος σε περίπτωση σφάλματος για χάρη του παραδείγματος.
Τύλιξε
Σε αυτό το άρθρο εξετάσαμε τις διαφορετικές προσεγγίσεις που χρησιμοποιούνται για την εφαρμογή της ασύγχρονης λογικής στο Javascript. Ξεκινήσαμε με τη διερεύνηση ενός συγκεκριμένου παραδείγματος για το γιατί θα έπρεπε να μεταβούμε από το κανονικό σύγχρονο στυλ προγραμματισμού στο ασύγχρονο μοντέλο. Στη συνέχεια, προχωρήσαμε στις επανακλήσεις, οι οποίες είναι τα κύρια δομικά στοιχεία της ασύγχρονης Javascript. Οι περιορισμοί των επανακλήσεων μας οδήγησαν στις διαφορετικές εναλλακτικές που προστέθηκαν με τα χρόνια για να ξεπεράσουμε αυτούς τους περιορισμούς, κυρίως υποσχέσεις και Async/wait. Η ασύγχρονη λογική μπορεί να βρεθεί οπουδήποτε στον ιστό, είτε καλείτε ένα εξωτερικό API, εκκινείτε ένα ερώτημα βάσης δεδομένων, γράφετε στο τοπικό σύστημα αρχείων ή ακόμα και περιμένετε για εισαγωγή από τον χρήστη σε μια φόρμα σύνδεσης. Ας ελπίσουμε ότι τώρα νιώθετε πιο σίγουροι για να αντιμετωπίσετε αυτά τα ζητήματα γράφοντας καθαρή και διατηρήσιμη ασύγχρονη Javascript!
Αν σας αρέσει αυτό το άρθρο, ρίξτε μια ματιά στο CLA Blog όπου συζητάμε διάφορα θέματα σχετικά με το πώς να μπείτε στην τεχνολογία. Επίσης, ρίξτε μια ματιά στο κανάλι μας στο YouTube για τα προηγούμενα δωρεάν εργαστήρια μας και ακολουθήστε μας στα social media για να μην χάσετε τα επερχόμενα!