Thực thi những tác vụ bất đồng bộ trong Javascript

Callback là gì

Hiểu đơn giản thì callback là một function (A) được truyền vào 1 function khác (B) thông qua các tham số của B. Lúc này function B sẽ gọi đến function A để thực hiện 1 chức năng gì đó hoặc là khi function B hoàn thành chức năng của mình.

Ví dụ đơn giản:

function dihoc(callback){
// Làm các công việc cần thiết khi đi học. Và cuối cùng kết quả nhận được là thu lại kiến thức.
callback('Kiến thức');
}

dihoc((ketqua) => {
  console.log(ketqua); // Kiến thức
});

Nhìn vào bên trên ta có thể thấy rằng callback được truyền vào function dihoc và khi function. Và chúng ta gọi đến callback sau khi thực hiện các nhiệm vụ cần thiết bên trên: đến lớp, nghe giảng,… Và ở bên dưới ta có nhận lại kết quả là “Kiến thức”.

Vấn đề đặt ra

Khi bạn thành lập 1 và phát triển công ty phần mềm thì công việc đầu tiên tất nhiên sẽ là THÀNH LẬP CÔNG TY. Sau khi thành lập công ty thì chúng ta cần có nhân viên thế nên là chúng ta TẠO RA CÁC VỊ TRÍ CÔNG VIỆC để tuyển. Sau khi tuyển được người thì công việc sẽ là XÂY DỰNG ỨNG DỤNG và để 1 ứng dụng được chạy hợp pháp thì tất nhiên bạn sẽ phải ĐĂNG KÝ GIẤY PHÉP với nhà nước.

Đó là use case và nhiệm vụ của chúng ta là thực hiện use case này trong NodeJS. Vấn đề đặt ra ở đây của chúng ta là làm sao để có thể viết code “clean” nhất cũng như thực thi lần lượt các nhiệm vụ này.

Callback hell???

Nếu chúng ta thực hiện lần lượt các nhiệm vụ trên bằng callback thì nó sẽ trông giống như thế này:

function seriesCallbackHell() {
 const company = new Company({
  name: 'FullStackHour'
 });
 company.save((err, savedCmp) => {
  if (err) {
   return next(err);
  }
  const job = new Job({
   title: 'Node.js Developer',
   _company: savedCmp._id
  });
  job.save((err, savedJob) => {
   if (err) {
    return next(err);
   }
   const application = new Application({
    _job: savedJob._id,
    _company: savedCmp._id
   });
   application.save((err, savedApp) => {
    if (err) {
     return next(err);
    }
    const licence = new Licence({
     name: 'FREE',
     _application: savedApp._id
    });
    licence.save((err, savedLic) => {
     if (err) {
      return next(err);
     }
     return res.json({
      company: savedCmp,
      job: savedJob,
      application: savedApp,
      licence: savedLic
     });
    });
   });
  });
 });
}

Khi mà bạn có quá nhiều các callback lồng nhau thì nó gọi là callback-hell. Của chúng ta mới có 4 công việc thôi mà nhìn nó đã hơi kinh dị như vậy rồi thử nghĩ nếu như chúng ta cần làm 10 công việc lần lượt thì sao? Callback-hell sẽ gây ra khó khăn trong việc đọc code, debug cũng như maintain code.

Vậy thì làm sao để giải quyết được các vấn đề mà không gây ra callback-hell? Sau đây mình xin giới thiệu những cách giúp bạn thực hiện điều đó.

Sử dụng thư viện Async.js của Caolan

Async.series

Async.series nhận vào 2 tham số, tham số thứ nhất tập các function bất đồng bộ và tham số thứ 2 là 1 callback(optional). Khi tất cả các nhiệm vụ được thực hiện xong thì callback sẽ được gọi và trả về kết quả. Kết quả bạn nhận được sẽ là 1 mảng các phần tử dạng như sau: [company, job, application, licence];

function seriesDemo(req, res, next) {
    let rsp = {};
    const tasks = [
        function createCompany(cb) {
            const company = new Company({
                name: 'FullStackhour'
            });
            company.save(function(err, savedCompany) {
                if (err) {
                    return cb(err);
                }
                rsp.company = savedCompany;
                return cb(null, savedCompany);
            });
        },
        function createJob(cb) {
            const job = new Job({
                title: 'Node.js Developer',
                _company: rsp.company._id
            });
            job.save((err, savedJob) => {
                if (err) {
                    return cb(err);
                }
                rsp.job = savedJob;
                return cb(null, savedJob);
            })
        },
        function createApplication(cb) {
            const application = new Application({
                _job: rsp.job._id,
                _company: rsp.company._id
            });
            application.save((err, savedApp) => {
                if (err) {
                    return cb(err);
                }
                rsp.application = savedApp;
                return cb(null, savedApp);
            })
        },
        function createLicence(cb) {
            const licence = new Licence({
                name: 'FREE',
                _application: rsp.application._id
            });
            licence.save((err, savedLic) => {
                if (err) {
                    return cb(err);
                }
                return cb(null, savedLic);
            })
        }
    ];
    async.series(tasks, (err, results) => {
        if (err) {
            return next(err);
        }
        return res.json(results);
    })
}

Bạn có thể thấy nhìn nó khác biệt hoàn toàn so với việc sử dụng hoàn toàn callback như lúc đầu. Việc sử dụng Async.series giúp cho code dễ nhìn hơn, dễ bảo trì cũng như debug hơn.

Async.waterfall

Nếu bạn muốn sử dụng kết quả của function trước để sử dụng ở function sau thì Async.waterfall chính là thứ có thể làm điều đó. Đúng như cái tên của nó waterfall (thác nước), nước trên cao sẽ đổ dồn xuống dưới để bên dưới nhận, rồi bên dưới lại đổ dồn tiếp đến khi nào ko còn chỗ mà đổ nữa.

function waterfallDemo(req, res, next) {
    const tasks = [
        function createCompany(cb) {
            const company = new Company({
                name: 'FullStackhour'
            });
            company.save(function(err, savedCompany) {
                if (err) {
                    return cb(err);
                }
                return cb(null, savedCompany);
            });
        },
        function createJob(company, cb) {
            const job = new Job({
                title: 'Node.js Developer',
                _company: company._id
            });
            job.save((err, savedJob) => {
                if (err) {
                    return cb(err);
                }
                return cb(null, {
                    job: savedJob,
                    company
                });
            });
        },
        function createApplication(result, cb) {
            const application = new Application({
                _job: result.job._id,
                _company: result.company._id
            });
            application.save((err, savedApp) => {
                if (err) {
                    return cb(err);
                }
                return cb(null, {
                    job: result.job,
                    company: result.company,
                    application: savedApp
                });
            })
        },
        function createLicence(result, cb) {
            const licence = new Licence({
                name: 'FREE',
                _application: result.application._id
            });
            licence.save((err, savedLic) => {
                if (err) {
                    return cb(err);
                }
                return cb(null, {
                    job: result.job,
                    company: result.company,
                    application: result.application,
                    licence: savedLic
                });
            })
        }
    ];
    async.waterfall(tasks, (err, results) => {
        if (err) {
            return next(err);
        }
        return res.json(results);
    })
}

Promise

Promise là một cơ chế trong JavaScript giúp bạn thực thi các tác vụ bất đồng bộ mà không rơi vào callback hell hay pyramid of doom, là tình trạng các hàm callback lồng vào nhau ở quá nhiều tầng. Các tác vụ bất đồng bộ có thể là gửi AJAX request, gọi hàm bên trong setTimeout, setInterval hoặc requestAnimationFrame, hay thao tác với WebSocket hoặc Worker…

Về Promise và Async/Await thì có rất nhiều bài viết nói về 2 thằng này rồi, nên mình sẽ ko nói nhiều về nó nữa mà chỉ tập chung vào cách giải quyết vấn đề bên trên. Trong .then() của Promise thì bạn có thể return 1 promise khác để sử dụng kết q ở .then() tiếp theo. Chính vì vậy mà chúng ta có thể giải quyết vấn đề đặt ra như sau:

const promiseChaining = (req, res, next) => {
    let rsp = {};
    const company = new Company({
        name: 'FullStackhour'
    });
    company.save()
        .then(savedCompany => {
            rsp.company = savedCompany;
            const job = new Job({
                title: 'Node.js Developer',
                _company: rsp.company._id
            });
            return job.save();
        })
        .then(savedJob => {
            const application = new Application({
                _job: savedJob._id,
                _company: rsp.company._id
            });
            rsp.job = savedJob;
            return application.save();
        })
        .then(savedApp => {
            const licence = new Licence({
                name: 'FREE',
                _application: savedApp._id
            });
            rsp.application = savedApp;
            return licence.save();
        })
        .then(savedLic => {
            rsp.licence = savedLic;
            return res.json(rsp);
        })
        .catch(err => {
            return next(err);
        })
}

Async/Await

Ở phiên bản ES7 (ES 2017), 1 khái niệm với 2 từ khóa mới được đưa vào là hàm async (async / await). Hàm async cho phép ta viết các thao tác bất đồng bộ với phong cách của các mã đồng bộ. Bằng cách viết như vậy, mã nguồn của ta trông sẽ sáng sủa, dễ đọc hơn và “dễ hiểu hơn”.

Về cơ bản thì Async/Await sẽ giúp chúng ta “đồng bộ hóa” các function bất đồng bộ. Nhờ đó mà chúng ta có thể dùng nó để giải quyết vấn đề mà chúng ta đã đặt ra lúc đầu.

function seriesDemo(req, res, next) {
    const saveRequest = async() => {
        const company = new Company({
            name: 'FullStackhour'
        });
        const savedCompany = await company.save();
        const job = new Job({
            title: 'Node.js Developer',
            _company: savedCompany._id
        });
        const savedJob = await job.save();
        const application = new Application({
            _job: savedJob._id,
            _company: savedCompany._id
        });
        const savedApp = await application.save();
        const licence = new Licence({
            name: 'FREE',
            _application: savedApp._id
        });
        const savedLic = await licence.save();
        return {
            company: savedCompany,
            job: savedJob,
            application: savedApp,
            savedLic: licence
        };
    }
    saveRequest()
        .then(result => {
            return res.json(result);
        })
        .catch(err => next(err));
}

Một vài vấn đề

Vấn đề sử dụng promise lồng promise thì ta nên sử dụng Promise.all bất cứ khi nào có thể. Bởi vì sử dụng Promise.all sẽ khiến các Promise chạy song song (parallell) => cải thiện tốc độ. Còn then bình thường nó sẽ chờ đợi từng Promise thực hiện xong. Tương tự như async.parallel và async.series

Ví dụ:

function smoke() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Hút xong');
    }, 5000);
  });
}

function drink() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Uống xong');
    }, 5000);
  });
}

let startNested = Date.now();

smoke()
  .then(data => drink())
  .then(() => {
    console.log(`\nNested Promise Time: ${Date.now() - startNested}`);
  });


let startAll = Date.now();

Promise.all([
  smoke(),
  drink()
]).then(() => {
  console.log(`\nPromise All time: ${Date.now() - startAll}`);
});

Bài viết được dịch từ: Execute tasks In Series: https://blog.cloudboost.io/execute-asynchronous-tasks-in-series-942b74697f9c

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *