Skip to main content

Callbacks

Οι Callbacks είναι ο παραδοσιακός τρόπος διαχείρισης ασύγχρονων λειτουργιών στη JavaScript.

Μια callback είναι μια συνάρτηση που περνιέται ως παράμετρος σε μια άλλη συνάρτηση και καλείται όταν ολοκληρωθεί η ασύγχρονη λειτουργία. Ας δούμε κάποια παραδείγματα:

function greet(name, callback) {
console.log('Hello, ' + name + '!');
callback(); // Εκτέλεση της callback συνάρτησης
}

function sayGoodbye() {
console.log('Goodbye!');
}

// Κλήση της greet συνάρτησης με την sayGoodBye ως callback
greet('Maria', sayGoodbye);

// Hello, Maria!
// Goodbye!

Στο παράδειγμα αυτό, η sayGoodBye δίνεται ως callback στη greet, η οποία την καλεί μετά τον χαιρετισμό.

Ας δούμε ένα πιο ρεαλιστικό παράδειγμα με ασύγχρονη λειτουργία:

function fetchData(callback) {
console.log('Λήψη δεδομένων...');

setTimeout(() => {
console.log('Τα δεδομένα ελήφθησαν!');
callback();
}, 2000);
}

function processData() {
console.log('Επεξεργασία δεδομένων...');
}

// Κλήση της συνάρτησης με callback
fetchData(processData);

// Λήψη δεδομένων...
// Τα δεδομένα ελήφθησαν!
// Επεξεργασία δεδομένων...

Στο παραπάνω παράδειγμα, η fetchData προσομοιώνει μια ασύγχρονη λειτουργία (π.χ. λήψη δεδομένων από έναν διακομιστή) χρησιμοποιώντας την setTimeout.

Μόλις ολοκληρωθεί η λήψη, καλεί την processData ως callback για να επεξεργαστεί τα δεδομένα.

Σημείωση

Η setTimeout είναι μια ενσωματωμένη συνάρτηση της JavaScript που καθυστερεί την εκτέλεση κώδικα για συγκεκριμένο χρονικό διάστημα. Στο παραπάνω παράδειγμα, η setTimeout προσομοιώνει μια ασύγχρονη λειτουργία (π.χ. αίτημα δικτύου) που διαρκεί 2000 milliseconds (2 δευτερόλεπτα).

Χειρισμός Σφαλμάτων

Στις callbacks χρησιμοποιούμε το μοτίβο error-first όπου το πρώτο όρισμα της callback είναι το σφάλμα (error) και το δεύτερο τα δεδομένα (data).

Για παράδειγμα:

const getUser = (id, callback) => {
if (id <= 0) {
callback(new Error('Μη έγκυρο ID'));
return;
}
callback(null, { id, name: 'Χρήστης ' + id });
};

// Παράδειγμα επιτυχίας
getUser(123, (error, user) => {
if (error) {
console.error(error.message);
return;
}
console.log(user.name); // ✅ "Χρήστης 123"
});

// Παράδειγμα σφάλματος
getUser(-123, (error, user) => {
if (error) {
console.error(error.message); // ❌ "Μη έγκυρο ID"
return;
}
console.log(user.name);
});

Στο παράδειγμα αυτό, η getUser καλεί την callback με ένα σφάλμα αν το id είναι μη έγκυρο (μικρότερο ή ίσο του μηδενός).

Όταν καλούμε την getUser, ορίζουμε και την λειτουργία της callback ώστε να χειριστεί τόσο το σφάλμα όσο και τα δεδομένα.

Συνηθισμένες Χρήσεις

Οι callbacks χρησιμοποιούνται συχνά σε:

// Node.js - Ανάγνωση αρχείου
fs.readFile('config.json', 'utf8', (error, data) => {
if (error) return console.error('Σφάλμα:', error);
console.log('Περιεχόμενο:', data);
});

// Browser - Χειρισμός συμβάντων
document.getElementById('button').addEventListener('click', () => {
console.log('Το κουμπί πατήθηκε!');
});

Στο πρώτο παράδειγμα, η readFile χρησιμοποιεί μια callback για να χειριστεί το αποτέλεσμα της ανάγνωσης αρχείου.

Στο δεύτερο παράδειγμα, μια callback χρησιμοποιείται για να χειριστεί το συμβάν κλικ σε ένα κουμπί.

Callback hell

Οι callbacks μπορούν συχνά να οδηγήσουν σε callback hellpyramid of doom) όταν έχουμε πολλές εμφωλευμένες ασύγχρονες λειτουργίες:

fetchUserData((error, userData) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση δεδομένων χρήστη:', error);
return;
}
getUserPosts(userData.id, (error, posts) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση αναρτήσεων:', error);
return;
}
getPostComments(posts[0].id, (error, comments) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση σχολίων:', error);
return;
}
// Αυτή η εμφωλευμένη δομή μπορεί να γίνει δύσκολα διαχειρίσιμη
});
});
});

Αυτό το φαινόμενο κάνει τον κώδικα:

  • Δύσκολο στην ανάγνωση
  • Δύσκολο στη συντήρηση
  • Επιρρεπή σε λάθη

Βελτίωση της Αναγνωσιμότητας των Callbacks

Υπάρχουν καλές πρακτικές που μπορούμε να ακολουθήσουμε για να βελτιώσουμε την αναγνωσιμότητα του κώδικα με callbacks.

Μία από αυτές είναι να χρησιμοποιούμε ονομασμένες συναρτήσεις (named functions). Στην ουσία, αντί να ορίζουμε ανώνυμες συναρτήσεις απευθείας μέσα στις κλήσεις, μπορούμε να δημιουργήσουμε ξεχωριστές ονομασμένες συναρτήσεις για κάθε βήμα της διαδικασίας.

const handleUserData = (error, userData) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση δεδομένων χρήστη:', error);
return;
}
getUserPosts(userData.id, handlePosts);
};

const handlePosts = (error, posts) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση αναρτήσεων:', error);
return;
}
getPostComments(posts[0].id, handleComments);
};

const handleComments = (error, comments) => {
if (error) {
console.error('Σφάλμα στην ανάκτηση σχολίων:', error);
return;
}
console.log('Σχόλια:', comments);
};

fetchUserData(handleUserData);

Η διαφορά μεταξύ των δύο προσεγγίσεων μπορεί να απεικονιστεί ως εξής:

Callback Hell:

fetchUserData(
└── getUserPosts(
└── getPostComments(
└── handleResult()
)
)
)

Ονομασμένες Συναρτήσεις:

fetchUserData → handleUserData


getUserPosts → handlePosts


getPostComments → handleComments

Η προσέγγιση με ονομασμένες συναρτήσεις δημιουργεί μια οριζόντια ροή αντί για μια εμφωλευμένη δομή, κάνοντας τον κώδικα πιο ευανάγνωστο και ευκολότερο στη συντήρηση.

Σύγκριση με Σύγχρονες Προσεγγίσεις

Για να αντιμετωπίσουμε τα προβλήματα των callbacks, η JavaScript έχει εισαγάγει πιο σύγχρονους τρόπους διαχείρισης ασύγχρονων λειτουργιών:

ΠροσέγγισηΠλεονεκτήματαΜειονεκτήματα
CallbacksΑπλή υλοποίηση, ευρεία υποστήριξηCallback hell, δύσκολος χειρισμός σφαλμάτων
PromisesΑλυσιδωτές λειτουργίες, καλύτερος χειρισμός σφαλμάτωνΠιο περίπλοκη σύνταξη από τα callbacks
async/awaitΣυγχρονική σύνταξη, εύκολη ανάγνωσηΑπαιτεί κατανόηση των Promises
Συμβουλή

Παρόλο που οι callbacks θεωρούνται παλαιότερη τεχνολογία, είναι σημαντικό να τις κατανοήσουμε καθώς πολλές βιβλιοθήκες και APIs εξακολουθούν να τις χρησιμοποιούν.

Στην επόμενη ενότητα, θα εξετάσουμε τα Promises, που προσφέρουν έναν πιο κομψό τρόπο διαχείρισης ασύγχρονων λειτουργιών.