Callback, Promise và Async/Await trong Javascript

Javascript là một ngôn ngữ hướng sự kiện và có khả năng hoạt động bất đồng bộ. Mình đã có một bài viết nói về cách thức hoạt động bất đồng bộ của JS, bạn có thể xem thêm Event Loop trong JS để hiểu thêm nhé.

Bài viết hôm nay, chúng ta hãy cùng tìm hiểu Callback, Promise và Async/Await trong Javascript. Đây là những kiến thức này nên và phải biết đối với một JS Developer 😎

Let’s get started !

callback, promise, async/await trong javascript

Callback và Callback hell

Tuy không khó, nhưng khi mới học về JS ta sẽ thấy khái niệm này khá mông lung 😂. Để đơn giản, 1 thứ gì đó trong JS được gọi là callback khi nó thoả 2 điều kiện sau:

  • Nó là một hàm (function).
  • Nó là đối số cho một hàm khác.

Ví dụ:

// định nghĩa trước 1 hàm làm callback
function callbackFn(params){
  console.log(`Tui là con bét nè ${params}`);
}

// định nghĩa một hàm nào đó dùng callback
function motHamNaoDo(str, cbFn){
  // str là tham số bth, không phải callback
  // kiểm tra trước khi thực thi hàm
  if(typeof cbFn === "function"){
    cbFn(str);
  }
}

// thực thi hàm vừa định nghĩa và truyền vào callback
motHamNaoDo("hihi", callbackFn);
// "Tui là con bét nè hihi"

Callback có lợi ích gì?

  • Callback sẽ được gọi để thực thi bên trong một hàm khác. Điều này giúp chúng ta thực hiện một chuỗi các hành động (hàm) liên tiếp nhau.
  • Callback rất hữu dụng khi xử lý sự kiện (event). Tức là chức năng đó sẽ được gọi ở một thời điểm nào đó khi event được kích hoạt. VD: click cái button thì hiển thị modal.
  • Vì sao không định nghĩa chức năng đó ngay bên trong hàm luôn mà lại đi dùng callback?
    • Vì callback được truyền vào hàm như một tham số. Điều này sẽ tăng tính linh động hơn, tuỳ vào trường hợp cụ thể mà bạn sẽ định nghĩa các hàm callback khác nhau để truyền vào.
  • Callback còn thường được dùng trong các method của các object, array như các phương thức map, reduce, find, …

Một số lưu ý khi dùng callback

Bạn có thể định nghĩa trước một hàm và truyền nó vào như một callback. Hoặc truyền hẳn một anonymous function nếu không cần tái sử dụng.

const arr = [1, 5, 3, 2, 1];

// Cách 1: định nghĩa sẵn một hàm
function ascendingSort(a, b){
  return a - b;
}
arr.sort(ascendingSort);

// Cách 2: truyền hẳn 1 anonymous function
arr.sort((a, b) => a - b);

Khi định nghĩa một hàm có dùng callback, nhớ kiểm tra tham số truyền vào có phải là một hàm hay không bằng typeof params === 'function'. Nếu không sẽ gây ra lỗi param is not a function.

Callback hell

Chắc mọi người đã nghe qua đâu đó thuật ngữ này. Đây là hiện tượng một chuỗi các callback được lồng vào nhau liên tiếp, dẫn đến việc khó theo dõi và debug code. Chúng ta có thể khắc phục điều này bằng Promise trong phần dưới.

Callback hell
hình ảnh của một con bét heo

Promise là gì?

Khái niệm Promise ra đời để giải quyết vấn đề Callback hell phía trên, nó giúp việc xử lý bất đồng độ trở nên dễ dàng, dễ code và dễ debug hơn khi chỉ sử dụng callback.

Lưu ý: điều này không có nghĩa là chúng ta không sử dụng callback nữa. Callback vẫn được sử dụng bên trong Promise, chỉ là chúng ta không cần dùng chúng lồng nhau khiến gây ra callback hell nữa.

Cơ chế hoạt động:

  • khởi tạo một promise thông qua từ khoá new => new Promise().
  • Một promise nhận vào một hàm (executor) với 2 tham số cũng là 2 hàm là resolve và reject.
    • Resolve dùng để trả dữ liệu về khi xử lý logic thành công.
    • Reject dùng để trả lỗi khi xử lý thất bại.
  • Dùng .then(callback) để nhận kết quả trả về từ resolve (thành công).
  • Dùng .catch(callback) để nhận kết quả trả về từ reject (bắt lỗi).
  • Dùng .finally() để xử lý logic trong mọi trường hợp sau khi này hoàn thành (luôn chạy).

Ví dụ:

// Định nghĩa một hàm fake lấy dữ liệu bất đồng bộ dùng Promise
function fetchData(url){
  return new Promise(function(resolve, reject){
    setTimeout(()=>{
      const data = Math.random();
      if(data > 0.5){
        resolve(data);
      }else{
        reject("Thất bại");
      }
    }, 3000)
  })
}

// Thực thi hàm
fetchData('https://dynonguyen.com')
.then(function(data){ console.log(data) })
.catch(function(error){ console.log(error) })
.finally(function(){ console.log('Done') })

Điểm nổi bật của Promise

  • Trong .then() bạn có thể tiếp tục trả về một Promise. Và kết quả của nó sẽ được đưa vào .then() tiếp theo. VD: bạn gọi hàm fetchData() sau đó lấy dữ liệu vừa lấy được để gọi tiếp hàm fetchData khác.
  • Bạn có thể có rất nhiều .then() nhưng chỉ cần 1 .catch() duy nhất để bắt lỗi.

Nhờ đặc điểm trên mà chúng ta có thể giải quyết được vấn đề callback hell 😮

Async/Await trong Javascript

Từ khoá Async ra đời giúp chúng ta viết Promise gọn hơn. Ngoài ra, nếu dùng async thì chúng ta có thể dùng một từ khoá khác là await.

Cơ chế hoạt động:

Khi khai báo một hàm với từ khoá là async thì mặc định hàm đó sẽ luôn trả về một Promise. Nếu bạn return thì những gì được return sẽ được đưa vào Promise.resolve. Còn khi bạn throw exception thì nó sẽ được đưa vào Promise.reject

Từ đó, bạn hoàn toàn có thể sử dụng .then.catch như với Promise. Lưu ý: Async vẫn hoạt động trên cơ chế của Promise, vì thế đừng hiểu nhầm có async thì không cần dùng Promise nữa 😅

// Định nghĩa một hàm fake lấy dữ liệu bất đồng bộ dùng Async
async function fetchData(url){
    const data = Math.random();
    if(data > 0.5){
      return data;
    }else{
      throw "Thất bại";
    }
}

// Thực thi hàm
fetchData('https://dynonguyen.com')
.then(function(data){ console.log(data) })
.catch(function(error){ console.log(error) })
.finally(function(){ console.log('Done') })

Từ khoá await:

Bên trong một async function, bạn có thể sử dụng từ khoá await. Với await nó sẽ đợi một hàm có trả về Promise hoàn thành (thành công hoặc thất bại) và sau đó mới thực thi tiếp các câu lệnh dưới nó. và nó sẽ trả giá trị trực tiếp của hàm đó luôn mà không cần phải .then()

// Sử dụng Promise
function getData(url, param){
  let result = null;

  callAPIs(url, param)
  .then(data1 => callAPIs(url, data2))
  .then(data2 => callAPIs(url, data2))
  .then(data3 => result = data3)
  .catch(err => console.error(err));

  return result;
}

// Sử dụng async/await
async function getData(url, param){
  try{
    const data1 = await callAPIs(url, param);
    // đợi data1
    const data2 = await callAPIs(url, data1);
    // đợi data2
    const data3 = await callAPIs(url, data2);
    return data3;
  }catch(error){
    console.error(error);
  }
}

Chú ý: await không dừng để đợi bên trong forEach, map. Tốt nhất với trường hợp bạn nên dùng vòng loop bình thường hoặc for await

Promise.all()

Trong trường hợp ở một hàm nào đó, chúng ta phải call nhiều API giống ví dụ trên, nhưng những API này không ảnh hưởng đến nhau. Tức là việc call API thứ 2 không cần phải lấy dữ liệu của việc call API thứ nhất, …

Nếu chúng ta làm như ví dụ trên thì hàm này chỉ hoàn thành khi lần call API thứ 3 hoàn thành (hoặc xảy ra lỗi). Điều này sẽ không hợp lý trong case hiện tại vì các lần call API không cần phải đợi lẫn nhau.

Promise.all sẽ giải quyết vấn đề này. Xem ví dụ sau:

// Hàm này sẽ mất 5s để hoàn thành nếu không có error xảy ra
async function getData(url){
  try{
    const data1 = await callAPIs(url, 1); // mất 1s
    const data2 = await callAPIs(url, 2); // mất 3s
    const data3 = await callAPIs(url, 3); // mất 1s
    return data3;
  }catch(error){
    console.error(error);
  }
}

// Hàm này sẽ mất 3s để hoàn thành nếu không có error xảy ra
async function getData2(url){
  try{
    const data1 = callAPIs(url); // mất 1s
    const data2 = callAPIs(url); // mất 3s
    const data3 = callAPIs(url); // mất 1s
    return await Promise.all([data1, data2, data3]);
  }catch(error){
    console.error(error);
  }
}

Promise.all hoàn thành khi Promise chạy lâu nhất hoàn thành.

Tạm kết

Đó là những gì mình biết về Callback, Promise cũng như Async/Await trong Javascript. Chỉ có một cách để hiểu và vận dụng tốt những thứ này là luyện tập thật nhiều và gặp các trường hợp cụ thể. Lúc đấy, chúng ta mới có cái nhìn sâu hơn về các khái niệm loằng nhoằng này trong Javascript.

Cảm ơn mọi người đã đọc bài viết 😍

Tham khảo:

Bạn có thể tham khảo thêm:

1 Comment

  1. Nguyễn Văn Khoa 17/09/2021

Để lại một bình luận nhé