Web nâng cao (7) - JavaScript cho React (6) - Đồng bộ - Bất đồng bộ

Bài trước: Web nâng cao (6) - JavaScript cho React (5)

-----

1.1       JavaScript cho React (6)

Phần này bạn sẽ tìm hiểu về:

– Lập trình đồng bộ

– Lập trình bất đồng bộ

– Các kĩ thuật lập trình bất đồng bộ trong JavaScript (callback, promise, async/await)

– Fetch API

1.1.1       Lập trình kiểu đồng bộ và bất đồng bộ

Lập trình kiểu đồng bộ

Đồng bộ (tiếng Anh là synchronous) là một tính từ, có nghĩa là (những chuyển động) có cùng chu kì hoặc cùng tốc độ, được tiến hành trong cùng một thời gian, tạo ra một sự phối hợp nhịp nhàng, ăn khớp với nhau; có sự ăn khớp giữa tất cả các bộ phận hoặc các khâu, tạo nên một sự hoạt động nhịp nhàng của chỉnh thể. [theo wiktionary].

Lập trình theo kiểu đồng bộ được hiểu là quá trình xử lý đoạn mã sẽ được thực hiện tuần tự, từ trên xuống dưới, xử lý hết lệnh trước rồi đến lệnh sau, nếu lệnh trước chưa xử lý xong thì lệnh sau sẽ phải chờ.

Bạn cùng xem xét ví dụ sau:

     const name = "cu Teo";

     const greeting = `Xin chao, ten toi la ${name}!`;

     console.log(greeting);

     // Xin chao, ten toi la cu Teo!

Đoạn mã trên gồm 3 việc là:

1.      Khai báo một chuỗi có tên là name để chứa một chuỗi kí tự

2.      Khai báo một chuỗi khác có tên là greeting, chuỗi này sẽ sử dụng chuỗi name

3.      Xuất chuỗi greeting ra cửa sổ console

Khi thực thi đoạn mã trên, trình duyệt sẽ thực thi tuần tự từng lệnh một, từ trên xuống dưới theo đúng thứ tự của mã nguồn. Do lệnh sau cần sử dụng tới kết quả của lệnh trước, nên các lệnh phía sau phải chờ cho các lệnh phía trước thực hiện xong thì nó mới được thực hiện. Đoạn mã trên chính là một chương trình chạy theo kiểu đồng bộ.

Nếu bạn viết lại đoạn mã trên có dùng tới lời gọi hàm thì chương trình vẫn chạy theo kiểu đồng bộ. Ví dụ:

     function makeGreeting(name) {

       return `Xin chao, ten toi la ${name}`;

     }

 

     const name = "cu Teo";

     const greeting = makeGreeting(name);

     console.log(greeting);

     // Xin chao, ten toi la cu Teo!

Hàm makeGreeting() là một hàm xử lý theo kiểu đồng bộ, vì vậy khi gọi nó để thực thi bạn cần phải đợi cho tới khi nào nó thực thi xong và trả về kết quả thì chương trình mới có thể thực hiện các lệnh kế tiếp.

Ưu điểm của lập trình theo kiểu đồng bộ là đơn giản, dễ hiểu, dễ kiểm soát logic xử lý; chạy mã nguồn là cho ra kết quả ngay.

Nếu một hàm đồng bộ thực thi trong thời gian hữu hạn cho phép thì ổn, tuy nhiên nếu nó cần nhiều thời gian để thực thi thì sẽ làm cho chương trình bị ngưng lại, ảnh hưởng tới trải nghiệm người dùng, tới quá trình xử lý các tác vụ. Chúng ta cùng xem xét ví dụ sau:

Ví dụ sau sẽ xuất ra ngẫu nhiên một số lượng lớn các số nguyên tố (prime number).

<label for="quota">So luong so nguyen to:</label>

    <input type="text" id="quota" name="quota" value="1000000">

   

    <button id="generate">Sinh so nguyen to</button>

    <button id="reload">Lam lai</button>

 

    <div id="output"></div>

   <script>

     // tao cac so nguyen to

     function generatePrimes(quota){

      // kiem tra mot so co la nguyen to?

      function isPrime(n){

         for(let c = 2; c <= Math.sqrt(n); ++c){

           if(n % c === 0) {

             return false;

           }

           return true;

         }

       }

       // tao ra quota so nguyen to luu vao mang primes

       const primes = [];

       const maximum = 1000000;

       while(primes.length < quota){

         const candidate = Math.floor(Math.random() * (maximum + 1));

         if(isPrime(candidate)){

           primes.push(candidate);

         }

       }

       return primes;

     }

     // doan ma xu ly giao dien

     document.querySelector('#generate').addEventListener('click', () => {

        const quota = document.querySelector('#quota').value;

        const primes = generatePrimes(quota);

        console.log(primes);

        document.querySelector('#output').textContent = `Da tao duoc ${quota} so nguyen to!`;

     });

 

     document.querySelector('#reload').addEventListener('click', () => {

        document.location.reload();

     });

   </script>


Khi chạy đoạn mã trên, sau khi bấm vào nút "Sinh so nguyen to", tùy theo tốc độ xử lý của máy tính, có thể bạn phải chờ một vài giây trước khi trình duyệt xuất hết các số nguyên tố ra cửa sổ console và hiển thị thông báo "Da tao duoc … so nguyen to !".

Hạn chế của các hàm xử lý đồng bộ

Đối với các hàm xử lý theo kiểu đồng bộ mà cần nhiều thời gian xử lý sẽ tạo ra trải nghiệm người dùng không tốt. Để chứng minh, bạn sẽ thêm một ô nhập liệu (textarea) vào đoạn chương trình Sinh số nguyên tố.

<button id="reload">Lam lai</button>

    <br><br>

    <textarea name="" id="" cols="70" rows="10" placeholder="Nhap van ban ..."></textarea>

    <div id="output"></div>


Bạn hãy nhập số nguyên tố cần sinh ra là 10 000 000, ngay khi bấm nút "Sinh so nguyen to", bạn sẽ không thể nhập liệu vào ô "Nhap van ban"; mà phải chờ cho việc sinh số nguyên tố thực hiện xong thì mới có thể nhập liệu được. Có hiện tượng này là do JavaScript là ngôn ngữ xử lý đơn luồng (single threaded) nên khi một lệnh đang được xử lý thì không thể chen ngang một lệnh khác.

Tuy nhiên, trong lập trình thực tế có những công việc cần nhiều thời gian để xử lý (như tải dữ liệu/hình ảnh từ server về, gọi dịch vụ từ xa, đọc/ghi tập tin), nếu cứ phải chờ cho lệnh đó xử lý xong thì mới thực hiện các lệnh kế tiếp sẽ làm cho ứng dụng bị “treo”, hoặc bị lỗi, không đáp ứng được nhu cầu của người dùng. Xem hình minh họa.


 Vậy, để chương trình chạy mượt mà, cần có giải pháp để:  

 – Các công việc cần nhiều thời gian xử lý sẽ được viết trong một hàm (ví dụ hàm xyz), để khi muốn thực hiện chỉ việc gọi hàm

– Có cơ chế để ngầm gọi hàm xyz thực thi, trong khi vẫn có thể xử lý và đáp ứng các xử lý khác của chương trình chính

– Khi nào hàm xyz thực hiện xong sẽ tự động hiển thị kết quả

Lập trình bất đồng bộ chính là giải pháp cho yêu cầu này.

Lập trình kiểu bất đồng bộ

Bất đồng bộ (tiếng Anh là asynchronous) là tính từ, có nghĩa là hai cái gì đó xảy ra không đồng thời. Trong trường hợp này chính là việc thực thi đoạn mã và cho ra kết quả, có thể đoạn mã được thực thi nhưng không cho ra kết quả ngay.

Lập trình theo kiểu bất đồng bộ cũng được hiểu theo một khía cạnh khác là quá trình xử lý mã nguồn không nhất thiết phải thực hiện tuần tự, một lệnh/đoạn mã phía sau vẫn có thể được thực thi khi một lệnh/đoạn mã phía trước chưa thực thi xong, chưa cho ra kết quả.

Để dễ hiểu về lập trình bất đồng bộ, bạn cùng viết và chạy thử đoạn mã sau:

console.log("Câu lệnh 1");

      console.log("Câu lệnh 2");

      setTimeout(() => console.log("Câu lệnh 3"), 2000);

      console.log("Câu lệnh 4");

// Câu lệnh 1

      // Câu lệnh 2

      // Câu lệnh 4

      // Câu lệnh 3

Hàm setTimeout() là một hàm có sẵn của JavaScript, có thể sử dụng hàm này để minh họa cho kiểu chạy bất đồng bộ. Hàm setTimeout(function, milliseconds) sẽ gọi một hàm khác (tham số function) để thực thi, sau một khoảng thời gian nhất định (tham số milliseconds).

Theo đoạn mã nguồn ở trên, nếu thực thi theo tuần tự, đoạn mã sẽ xuất chuỗi “Câu lệnh 1”, “Câu lệnh 2”, hàm setTimeout() sẽ xuất chuỗi “Câu lệnh 3”, cuối cùng là xuất chuỗi “Câu lệnh 4”. Tuy nhiên, khi thực thi đoạn mã thì thứ tự các chuỗi được xuất ra là: “Câu lệnh 1”, “Câu lệnh 2”, “Câu lệnh 4”, “Câu lệnh 3”. Nghĩa là, chương trình không được thực thi theo thứ tự từ trên xuống dưới mà có sự xáo trộn trong thứ tự thực thi các câu lệnh, đó là tính bất đồng bộ trong việc thực thi mã nguồn.

Lập trình theo kiểu bất đồng bộ giúp chương trình chạy hợp lý hơn trong một số tình huống, chạy mượt mà hơn, đem lại trải nghiệm người dùng tốt hơn; tuy nhiên nó làm cho lập trình viên khó kiểm soát logic xử lý.

Một số tình huống cần sử dụng kĩ thuật lập trình bất đồng bộ như: thực hiện lời gọi hàm fetch(), truy cập camera hoặc microphone getUserMedia(), yêu cần người dùng lựa chọn một tập tin showOpenFilePicker().

JavaScript là ngôn ngữ lập trình đơn luồng (single threaded) do vậy tại một thời điểm chỉ có một luồng (một lệnh/đoạn mã) được xử lý. Để có thể lập trình theo kiểu bất đồng bộ, JavaScript cung cấp một số kỹ thuật như: callback, promise và async/await.

1.1.2       Callback

Trong JavaScript, callback là một kỹ thuật lập trình. Nó được sử dụng trong nhiều tình huống khác nhau chứ không chỉ trong lập trình bất đồng bộ. Vì vậy, trước hết chúng ta nên tìm hiểu callback là gì?

Callback là gì?

Trong tình huống giao tiếp thông thường, callback nghĩa là gọi lại, ví dụ khi bạn đang bận làm việc, bạn để điện thoại ở chế độ im lặng nên không biết có cuộc gọi tới, khi xong việc bạn mở danh sách các cuộc gọi nhỡ để gọi lại. Có thể hiểu, lập trình theo kiểu callback là hành động tạo ra danh sách các cuộc gọi nhỡ giúp người sử dụng điện thoại có đầu mối để biết cần phải gọi lại cho ai.

Chúng ta cùng làm ví dụ sau:

     function lamBaiTap(tenBaiTap){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

     }

     lamBaiTap("Bai tap 1");// Toi dang lam bai tap: Bai tap 1

Ở ví dụ trên, hàm lamBaiTap() sẽ nhận một tham số là tenBaiTap khi thực thi. Vì hàm trong JavaScript cũng là object, nên bạn hoàn toàn có thể truyền hàm cho một hàm khác như là một tham số, như đoạn mã sau:

     function uongNuoc(){

      console.log("Toi uong nuoc");

     }

     function lamBaiTap(tenBaiTap, tenHam){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       tenHam();

     }

     lamBaiTap("Bai tap 1", uongNuoc);

     // Toi dang lam bai tap: Bai tap 1

     // Toi uong nuoc

Bạn hoàn toàn có thể gọi trực tiếp hàm uongNuoc() trong hàm lamBaiTap() mà không nhất thiết phải truyền hàm uongNuoc() vào hàm lamBaiTap() theo kiểu truyền tham số:

    function uongNuoc(){

      console.log("Toi uong nuoc");

    }

    function lamBaiTap(tenBaiTap){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       uongNuoc();

     }

     lamBaiTap("Bai tap 1");

     // Toi dang lam bai tap: Bai tap 1

     // Toi uong nuoc

Tuy nhiên, bạn hãy cứ trải nghiệm với kiểu truyền hàm cho hàm và luôn suy nghĩ là "tại sao người ta lại tạo ra kiểu lập trình này"? kiểu lập trình này có ưu điểm gì? dùng được trong những tình huống nào?

Giờ bạn sẽ đổi tên hàm được gọi là callback cho trực quan hơn:

       function uongNuoc(){

      console.log("Toi uong nuoc");

    }

    function lamBaiTap(tenBaiTap, callback){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       callback();

     }

     lamBaiTap("Bai tap 1", uongNuoc);

     // Toi dang lam bai tap: Bai tap 1

     // Toi uong nuoc

Tóm lại, kĩ thuật lập trình cho phép truyền hàm cho một hàm khác dưới dạng một tham số được gọi là lập trình kiểu callback. Hàm truyền cho hàm khác được gọi là hàm callback (ví dụ uongNuoc()). Nhờ hàm callback đã được khai báo là tham số, nên nó sẽ được "hàm cha" gọi lại để thực thi ở một thời điểm trong tương lai.

Quay trở lại tình huống "cuộc gọi nhỡ ", thì đây là quá trình tạo danh sách cuộc gọi nhỡ:

function uongNuoc(){

      console.log("Toi uong nuoc");

    }

     function lamBaiTap(tenBaiTap, callback){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       callback();

     }

Còn đây là thực hiện gọi lại:

     lamBaiTap("Bai tap 1", uongNuoc);

     // Toi dang lam bai tap: Bai tap 1

     // Toi uong nuoc

Tham số truyền cho hàm có thể là một dữ liệu thông thường, có thể là một hàm? Làm sao biết được tham số nào là dữ liệu thông thường, tham số nào là một hàm? Bạn cứ yên tâm, JavaScript sẽ có cách để kiểm tra điều này cho bạn. Nếu là dữ liệu, JavaScript sẽ thực hiện các phép toán, nếu là hàm, JavaScript sẽ thực thi nội dung của hàm. Bạn có thể tự kiểm tra bằng đoạn mã sau:

    const number = 123;

    function uongNuoc(){

      console.log("Toi uong nuoc");

    }

    function lamBaiTap(tenBaiTap, callback){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       if(typeof(callback) === 'function') {

         callback();

       }

     }

     lamBaiTap("Bai tap 1", number);

     // Toi dang lam bai tap: Bai tap 1

Đoạn mã trên sử dụng typeof() để kiểm tra tham số truyền vào, nếu nó là function thì mới gọi thực thi. Khi gọi hàm, bạn đã truyền vào một giá trị số (biến number), nên hàm callback không được thực thi.

Bạn có thể định nghĩa trước hàm callback, sau đó truyền nó vào một hàm khác như ví dụ ở trên. Hoặc bạn có thể định nghĩa trực tiếp hàm callback trong quá trình "hàm cha" thực thi. Ví dụ dùng từ khóa function để định nghĩa hàm callback:

function lamBaiTap(tenBaiTap, callback){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       callback();

     }

     lamBaiTap("Bai tap 1", function() {

      console.log("Toi uong nuoc");

     });

Bạn cũng có thể dùng hàm nặc danh (anonymous function, arrow function) để định nghĩa hàm callback:

function lamBaiTap(tenBaiTap, callback){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       callback();

     }

     lamBaiTap("Bai tap 1", () => {

      console.log("Toi uong nuoc");

     });

Bạn có thể gọi một lúc nhiều hàm callback:

 function lamBaiTap(tenBaiTap, callback1, callback2){

       console.log("Toi dang lam bai tap: " + tenBaiTap);

       callback2();

       callback1();

     }

     lamBaiTap("Bai tap 1",

     () => {

      console.log("Toi uong nuoc");

     },

     () => {

      console.log("Toi an banh");

     });

Tiếp theo, chúng ta sẽ trải nghiệm một số ví dụ sử dụng callback trong JavaScript.

Sắp xếp mảng với callback

Bạn chạy ví dụ sau sẽ thấy, kết quả sắp xếp mảng theo chiều tăng không đúng, vì JavaScript đã đổi dữ liệu của mảng thành kiểu chuỗi để sắp xếp theo thứ tự bảng chữ cái:

const arr = [9, 4, 7, 2, 13, 45];

     console.log(arr); // [9, 4, 7, 2, 13, 45]

     // sắp xếp mảng arr bằng hàm sort()

     arr.sort();

     console.log(arr); //[13, 2, 4, 45, 7, 9]

Vì vậy bạn cần phải định nghĩa một hàm callback để thực hiện so sánh hai số a và b. Hàm callback sẽ trả về giá trị như sau:

– Giá trị âm, nếu a nhỏ hơn b (đồng nghĩa với không đổi chỗ a và b)

– Giá trị 0, nếu a bằng b (đồng nghĩa với không đổi chỗ a và b)

– Giá trị dương, nếu a lớn hơn b (đồng nghĩa với có đổi chỗ a và b)

function soSanhSo(a, b){

       return a - b;

     }

     const arr = [9, 4, 7, 2, 13, 45];

     arr.sort(soSanhSo);

     console.log(arr); //[2, 4, 7, 9, 13, 45]

Nếu muốn sắp xếp mảng theo chiều giảm thì sửa lại hàm callback như sau:

function soSanhSo(a, b){

       return b - a;

     }

forEach() với callback

Thông thường bạn hay duyệt mảng như sau:

const mau = ['xanh', 'do', 'tim', 'vang'];

     for(let i = 0; i < mau.length; i++) {

       console.log(`Mau tai vi tri ${ i } la ${ mau[i] }`);

       /*

        Mau tai vi tri 0 la xanh

        Mau tai vi tri 1 la do

        Mau tai vi tri 2 la tim

        Mau tai vi tri 3 la vang

       */

     }

Với callback bạn có thể duyệt mảng theo kiểu khác, với kết quả xuất ra màn hình là như nhau:

 const mau = ['xanh', 'do', 'tim', 'vang'];

     mau.forEach((color, index) => {

       console.log(`Mau tai vi tri ${ index } la ${ color }`);

     });

map() với callback

const arr = [1, 2, 3];

     let result = arr.map( (x) => x * 2 );

     console.log(result); // [2, 4, 6]

Sự kiện DOM với callback

<body>

    <button id = "nut-bam">Nut bam</button>

    <script>

     const btn = document.getElementById('nut-bam');

     btn.addEventListener('click', function(){

       alert('Ban vua bam nut');

     });

     </script>

  </body>

-----

Cập nhật: 30/3/2022

-----

Bài tiếp: Web nâng cao (8) - JavaScript cho React (7)