Callback hell là gì? Cách giải quyết callback hell trong Javascript

5 min read

Để hiểu khác niệm của callbacks và callback hell, trước tiên chúng ta cần tìm hiểu về lập trình đồng bộ(Synchronous) và bất đồng bộ(Asynchronous) trong Javascript.

Lập trình đồng bộ(Synchronous programming)

Lập trình đồng bộ là cách lập trình trong đó bạn chỉ có thể thực hiện một tác vụ(task) tại một thời điểm và khi một tác vụ hoàn thành, chúng ta sẽ chuyển sang tác vụ khác. Cách này còn được gọi là Blocking Code vì bạn cần đợi một tác vụ hoàn thành để chuyển sang tác vụ tiếp theo. Cùng xem một ví dụ đơn giản về lập trình đồng bộ dưới đây:

console.log("Program Starts");
let sum = getSum(2,3);
console.log(sum);
console.log("Program Ends");

Trong đoạn code trên, bạn sẽ thấy code được thực thi từng dòng và khi tác vụ trên một dòng kết thúc thì chúng ta chuyển sang dòng tiếp theo.

Lập trình bất đồng bộ(Asynchronous programming)

Lập trình bất đồng bộ cho phép bạn thực hiện các tác vụ mà không làm chặn tiến trình hoặc luồng chính. Trong lập trình bất đồng bộ, bạn có thể chuyển sang thực thi tác vụ khác mà không cần chờ tác vụ trước đó kết thúc và bằng cách này bạn có thể xử lý nhiều tác vụ cùng lúc.

Trong javascript, một ví dụ điển hình về lập trình bất đồng bộ là sử dụng hàm setTimeout. Cùng xem ví dụ đơn giản dưới đây:

console.log("Program Starts");
setTimeout(() => {
  console.log("Executing asynchronous here...");
}, 2000);
console.log("Program Ends");

Và kết quả của đoạn code trên khi chạy sẽ như sau:

Program Starts
Program Ends
Executing asynchronous here...

Chương trình của chúng ta không cần đợi hàm setTimout kết thúc mà thực thi đến dòng tiếp theo luôn, sau đó quay lại hàm bên trong setTimeout và in kết quả đầu ra. Cách này còn được gọi là Non Blocking code.

Có ba mẫu thiết kế trong javascript để xử lý lập trình bất đồng bộ:

Callbacks

Callbacks là một cách tuyệt vời để xử lý các hành vi bất đồng bộ trong javascript. Trong javascript mọi thứ hoạt động giống như một đối tượng nên các hàm có kiểu là đối tượng(object) hoặc các kiểu khác(string, array,…) bạn có thể truyền các hàm như một tham số(argument) cho hàm khác và ý tưởng đó gọi là callbacks. Chúng ta cùng xem 1 ví dụ đơn giản sau để hiểu rõ hơn về callbacks.

function getUser(id, callback) {
  setTimeout(() => {
    console.log("Reading an user from database...");
    callback({id: id, username: 'example-user'});
  }, 2000);
}

getUser(1, (user) => {
  console.log("User: ", user);
})

Trong ví dụ trên, chúng ta truyền một hàm(có tên callback) như một tham số cho hàm getUser và nó được gọi bên trong hàm getUser. Kết quả được in ra sẽ như sau:

Reading an user from database...
User: {id: 1, username: 'example-user'}

Callback hell

Trong đoạn code ở ví dụ trên, chúng ta sẽ lấy được thông tin username, và bây giờ giả sử bạn muốn sử dụng thông tin username đó để thực hiện việc lấy ra danh sách các repositories theo username và sau đó lấy danh sách các commit theo một repository cụ thể nào đó. Khi đó chúng ta cần sử dụng cách tiếp cận sử dụng hàm callbacks như sau:

getUser(1, (user) => {
  console.log("User: ", user);
  getRepositories(user.username, (repos) => {
    console.log("Repositories:", repos);
    getCommits(repos[0], (commits) => {
      console.log("Commits: ",commits);
      // Callback Hell ("-_-)
    }
})

Bây giờ các bạn đã thấy các hàm được lồng nhau theo nhiều cấp và đoạn code của chúng ta cũng trông đáng sợ hơn và đây được gọi là Callback hell. Với các ứng dụng lớn và dữ liệu có mối quan hệ phụ thuộc thì chúng có thể tạo ra nhiều sự lồng ghép hàm hơn. Ví dụ như hình sau:

Để tránh vấn đề này, chúng ta sẽ cùng tìm hiểu về Promises.

Promises

Promises là một giải pháp thay thế cho callbacks để cung cấp kết quả tính toán bất đồng bộ. Chúng đòi hỏi nhiều effort hơn từ những người triển khai các hàm bất đồng bộ, nhưng mang lại một số lợi ích cho người dùng các hàm đó(các bạn có thể xem thêm ở đây).

Trên thực tế, Promise sẽ có 4 trạng thái như sau:

  • fulfilled – Hành động liên quan đến promise đã thực hiện thành công
  • rejected – Hành động liên quan đến promise không thành công
  • pending – Chưa fulfilled hoặc rejected
  • settled – Đã fulfilled or rejected

Bây giờ chúng ta sẽ triển khai sử dụng promises thay thế cho callbacks của ví dụ trên. Đầu tiên chúng ta phải tạo các Promises như sau:

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Reading from a database....");
      resolve({ id: id, username: "example-user" });
    }, 2000);
  });
}

function getRepositories(username) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Extracting Repositories for ${username}....`);
      resolve(["repo1", "repo2", "repo3"]);
      // reject(new Error("Error occured in repositories"));
    }, 2000);
  });
}

function getCommits(repo) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Extracting Commits for " + repo + "....");
      resolve(["commits"]);
    }, 2000);
  });
}

Chúng ta đã tạo ra 3 hàm, thay vì truyền vào tham số callback, bây giờ chúng ta trả về một Promise có 2 tham số là resolve và reject. Nếu mọi thứ hoạt động bình thường thì resolve sẽ được gọi, ngược lại thì reject sẽ được gọi.

Và dưới đây là đoạn code sử dụng các hàm trên

getUser(1)
  .then((user) => getRepositories(user.githubUsername))
  .then((repos) => getCommits(repos[0]))
  .then((commits) => console.log("Commits", commits))
  .catch((err) => console.log("Error: ", err.message));

Đoạn code trên sẽ dễ đọc hơn so với sử dụng callbacks phải không? Trong đoạn code trên chúng ta có sử dụng các arrow function để làm giảm sự phức tạp so với việc sử dụng các function thông thường. Các bạn có thể tìm hiểu sâu hơn về Promises tại đây

Async/await

Một cách viết code ngắn hơn và dễ hiểu hơn cho Promise đó là chúng ta sẽ sử dụng async/await.

Tất cả những gì bạn cần làm là thêm từ khóa async vào trước bất kỳ một hàm thông thường nào và khi đó nó sẽ trở thành một Promise. Nói cách khác, async/await là một cú pháp sử dụng các promise, điều đó có nghĩa là nếu bạn muốn tránh việc xâu chuỗi các phương thức then() trong các Promise.

Để dễ hiểu hơn, chúng ta cùng viết lại đoạn code gọi các hàm Promises bên trên về dạng async/await như sau:

async function displayCommits() {
  try {
    const user = await getUser(1);
    const repos = await getRepositories(user.username);
    const commits = await getCommits(repos[0]);
    console.log(commits);
  } catch (err) {
    console.log("Error: ", err.message);
  }
}

displayCommit();

Bạn thấy thế nào? Sau khi sửa lại, code của chúng ta đã trở nên dễ đọc hơn đúng không? Mỗi lần sử dụng await chúng ta cần phải nhớ định nghĩa thêm từ khóa async phía trước function mà bạn muốn sử dụng await nhé.

Đến đây thì bạn đã hiểu về Promises, async/await và cách xử lý vấn đề callback hell rồi chứ? Hãy để lại bình luận hoặc tương tác bên dưới bài viết nhé. ❤️

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *