Chúng tôi sử dụng các phương thức của trình duyệt trong các ví dụ ở đây

Để thể hiện việc sử dụng các cuộc gọi lại(callback), lời hứa(promises) và các khái niệm trừu tượng khác, chúng tôi sẽ sử dụng một số phương pháp của trình duyệt: tải tập lệnh và thực hiện các thao tác tài liệu đơn giản.

Nếu bạn không quen thuộc với các phương thức này và việc sử dụng chúng trong các ví dụ gây nhầm lẫn, bạn có thể muốn đọc một vài phần về làm việc với trình duyệt.

Mặc dù, chúng ta sẽ cố gắng làm cho mọi thứ rõ ràng hơn. Sẽ không có bất cứ thứ gì thực sự phức tạp đối với trình duyệt.

Nhiều hàm được cung cấp bởi các môi trường máy chủ JavaScript cho phép bạn lên lịch các hành động không đồng bộ. Nói cách khác, những hành động mà chúng ta bắt đầu bây giờ, nhưng chúng kết thúc sau đó.

Ví dụ, một hàm như vậy là hàm setTimeout.

Có các ví dụ thực tế khác về các hành động không đồng bộ, ví dụ như tải các tập lệnh và mô-đun (chúng ta sẽ đề cập đến chúng trong các chương sau).

Hãy xem hàm loadScript(src), tải một tập lệnh với cái đã cho src:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

function loadScript(src) {
  // creates a <script> tag and append it to the page
  // this causes the script with given src to start loading and run when complete
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

Nó gắn vào document và được tạo động, <script src="…">được cung cấp src. Trình duyệt tự động bắt đầu tải nó và thực thi khi hoàn thành.

Chúng ta có thể sử dụng hàm này như thế này:

-->
// load and execute the script at the given path
loadScript('/my/script.js');

Kịch bản được thực thi không đồng bộ, vì nó bắt đầu tải ngay bây giờ, nhưng chạy sau đó, khi hàm đã hoàn thành.

Nếu có bất kỳ code nào bên dưới loadScript(…), nó sẽ không đợi cho đến khi quá trình tải tập lệnh kết thúc.

loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

Giả sử chúng ta cần sử dụng tập lệnh mới ngay khi tải. Các câu lệnh hàm mới và chúng ta muốn chạy chúng.

Nhưng nếu chúng ta làm điều đó ngay sau lệnh gọiloadScript(…), điều đó sẽ không hiệu quả:



loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

Đương nhiên, trình duyệt có thể không có thời gian để tải tập lệnh. Cho đến nay, hàm loadScript này không cung cấp cách theo dõi quá trình hoàn thành tải. Kịch bản bạn muốn tải và chạy nó, đó là tất cả. Nhưng chúng ta muốn biết khi nào nó xảy ra, để sử dụng các hàm và biến mới từ tập lệnh đó.

Chúng ta hãy thêm một hàmcallback làm đối số thứ hai để loadScriptthực thi khi tập lệnh tải hoàn thành:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

Bây giờ nếu chúng ta muốn gọi các hàm mới từ tập lệnh, chúng ta nên viết nó trong hàm gọi lại:

loadScript('/my/script.js', function() {
  // the callback runs after the script is loaded
  newFunction(); // so now it works
  ...
});

Đó là ý tưởng: đối số thứ hai là một hàm (thường là ẩn danh) chạy khi hành động được hoàn thành.

Đây là một ví dụ có thể chạy được với một tập lệnh thực sự:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`Cool, the script ${script.src} is loaded`);
  alert( _ ); // function declared in the loaded script
});

Cái đó được gọi là kiểu lập trình không đồng bộ dựa trên cuộc gọi lại(hàm callback). Một hàm thực hiện một cái gì đó không đồng bộ sẽ cung cấp một đối sốcallback trong đó chúng ta đặt hàm để chạy sau khi hoàn thành.

Ở đây chúng ta đã làm điều đó loadScript, nhưng tất nhiên đó là một cách tiếp cận chung.

1. Gọi lại(Callback) trong một gọi lại khác(Callback)

Làm thế nào chúng ta có thể tải hai tập lệnh một cách tuần tự: tập đầu tiên và sau đó tập lệnh thứ hai sau nó?

Giải pháp bình thường là đặt lệnh gọiloadScript thứ hai vào trong cuộc gọi lại(Callback), như thế này:

loadScript('/my/script.js', function(script) {

  alert(`Cool, the ${script.src} is loaded, let's load one more`);

  loadScript('/my/script2.js', function(script) {
    alert(`Cool, the second script is loaded`);
  });

});

Sau khi bên ngoài loadScripthoàn thành, cuộc gọi lại bắt đầu bên trong.

Điều gì sẽ xảy ra nếu chúng ta muốn có thêm một đoạn script

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continue after all scripts are loaded
    });

  })

});

Vì vậy, mỗi hành động mới là trong một cuộc gọi lại. Điều đó tốt cho một vài hành động, nhưng không tốt cho nhiều hành động, vì vậy chúng ta sẽ sớm thấy các biến thể khác.

2. Xử lý lỗi

Trong các ví dụ trên, chúng ta đã không xem xét lỗi. Điều gì xảy ra nếu tải tập lệnh thất bại? Cuộc gọi lại của chúng ta sẽ có thể phản ứng về điều đó.

Đây là phiên bản cải tiến của loadScriptkhi lỗi tải file:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

Nó gọi callback(null, script)cho tải thành công và callback(error)nếu không.

Việc sử dụng:

loadScript('/my/script.js', function(error, script) {
  if (error) {
    // handle error
  } else {
    // script loaded successfully
  }
});

Một lần nữa, công thức mà chúng ta sử dụng loadScriptthực sự khá phổ biến. Nó được gọi là kiểu gọi lại với lỗi.

Quy ước là:

  1. Đối số đầu tiên của callbackđược dành riêng cho một lỗi nếu nó xảy ra. Ta có callback(err)được gọi.
  2. Đối số thứ hai (và các đối số tiếp theo nếu cần) là cho kết quả thành công. Ta cócallback(null, result1, result2…)được gọi.

Vì vậy, Hàm callback duy nhất được sử dụng cả cho báo lỗi và gửi lại kết quả.

3. Kim tự tháp gọi lại(callback)

Từ cái nhìn đầu tiên, đó là một cách khả thi của code không đồng bộ đối với một hoặc có thể hai cuộc gọi lồng nhau, nó có vẻ tốt.

Nhưng đối với nhiều hành động không đồng bộ nối tiếp nhau, chúng ta sẽ có code như thế này:

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continue after all scripts are loaded (*)
          }
        });

      }
    })
  }
});

Trong đoạn code trên:

  1. Chúng tôi tải 1.js, sau đó nếu không có lỗi.
  2. Chúng tôi tải 2.js, sau đó nếu không có lỗi.
  3. Chúng tôi tải 3.js, sau đó nếu không có lỗi – làm một cái gì đó khác (*).

Khi các cuộc gọi trở nên lồng nhau nhiều hơn, code trở nên sâu hơn và ngày càng khó quản lý hơn, đặc biệt nếu chúng ta có code thực thay vì ...điều đó có thể bao gồm nhiều vòng lặp, câu lệnh có điều kiện, v.v.

Điều đó đôi khi được gọi là địa ngục, gọi lại hay Kim tự tháp của sự diệt vong.

Các kim tự tháp của Nhật Bản Các cuộc gọi lồng nhau phát triển sang phải với mọi hành động không đồng bộ. Chẳng mấy chốc nó vượt khỏi tầm kiểm soát.

Vì vậy, cách code này không tốt lắm.

Chúng ta có thể cố gắng giảm bớt vấn đề bằng cách biến mọi hành động thành một hàm độc lập, như thế này:

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

loadScript('1.js', step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('3.js', step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...continue after all scripts are loaded (*)
  }
};

Hiện tại không có lồng nhau sâu bởi vì chúng ta đã thực hiện mọi hành động thành một hàm riêng biệt.

Nó hoạt động, nhưng code trông giống như một phép tính lở dỡ. Thật khó để đọc và có lẽ bạn nhận thấy rằng người ta cần phải đảo mắt giữa các mảnh trong khi đọc nó. Điều đó thật bất tiện, đặc biệt là nếu người đọc không quen thuộc với code và không biết phải nhảy mắt ở đâu.

Ngoài ra, các hàm được đặt tên step*là tất cả chỉ sử dụng một lần, chúng chỉ được tạo ra để tránh kim tự tháp của sự diệt vong. Không ai sẽ tái sử dụng chúng ngoài chuỗi hành động trên. Vì vậy, có một chút không gian tên lộn xộn ở đây.

Chúng ta muốn có một cái gì đó tốt hơn.

May mắn thay, có những cách khác để tránh những kim tự tháp như vậy. Một trong những cách tốt nhất là sử dụng những “promises”, được mô tả trong chương tiếp theo.

Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!