Bài viết được dịch từ: medium.freecodecamp.org
JavaScript là đồng bộ. Điều này có nghĩa là nó sẽ thực thi code của bạn theo thứ tự sau khi hoiting. Trước khi code thực thi, các khai báo var và function được "đẩy" lên phía trên cùng scope (phạm vi) của chúng.
Đây là ví dụ về code đồng bộ:
console.log('1')
console.log('2')
console.log('3')
Đoạn code này chắc này chắc chắn sẽ in ra "1 2 3".
Các request bất đồng bộ sẽ chờ timer kết thúc (setTimeout) hoặc request được đáp trả, trong khi phần còn lại của code tiếp tục được thực thi. Sau đó, khi đến thời điểm callback sẽ đưa các request bất đồng bộ vào hoạt động.
Đây là một ví dụ về code bất đồng bộ:
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
Đoạn code trên sẽ in ra "1 3 2", trong đó "2" ở trong hàm setTimeout, cái sẽ chỉ thực thi sau 2 giây. Ứng dụng của bạn sẽ không chờ cho đến khi 2 giây kết thúc. Thay vào đó nó sẽ tiếp tục thực thi phần còn lại của code và khi timeout kết thúc nó sẽ quay trở lại để thực thi hàm afterTwoSeconds.
Bạn có thể hỏi "Tại sao điều này lại hữu ích?" hay "Làm thế nào tôi biến code bất đồng bộ (async) thành đồng bộ (sync)?". Hi vọng rằng tôi sẽ cho bạn thấy câu trả lời.
Vấn đề
Mục tiêu của chúng ta là tìm kiếm một người dùng GitHub và lấy tất cả các repo (kho) của người đó. Và bởi vì chúng ta không biết chính xác tên người dùng. Vì thế chúng ta liệt kê tất cả người dùng có tên tương tự nhau và các repo tương ứng. Như thế này:

Trong những ví dụ này, chúng ta sẽ sử dụng XHR (XMLHttpRequest). Bạn có thể thay thế nó với jQuery $.ajax hoặc một hướng tiếp cập tự nhiên hơn gọi là fetch. Tất cả sẽ cho bạn các promise ở đầu ra.
Đoạn code dưới đây sẽ thay đổi một chút phụ thuộc và hướng tiếp cận của bạn, nhưng bắt đầu sẽ như thế này:
// url argument can be something like 'https://api.github.com/users/daspinola/repos'
function request(url) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Code here for the server answer when successful
} else {
// Code here for the server answer when not successful
}
}
}
xhr.ontimeout = function () {
// Well, it took to long do some code here to handle that
}
xhr.open('get', url, true)
xhr.send();
}
Hãy nhớ rằng, trong những ví dụ này phần quan trọng không phải là kết quả cuối cùng của code. Thay vào đó, mục tiêu của bạn là phải hiểu được sự khác biệt của các cách tiếp cận và cách bạn có thể tận dụng chúng cho công việc của mình.
Callback
Bạn có thể lưu một hàm trong một biến khi sử dụng JavaScript. Sau đó, sử dụng chúng như các tham số của một hàm khác để thực thi sau đó. Đây chính là "callback".
Một ví dụ sẽ là:
// Thực thi hàm "doThis" với tham số là một hàm khác, trong trường hợp này là "andThenThis". doThis sẽ thực thi ở bất kỳ đau trong code và khi nó kết thúc hàm "andThenThis" sẽ được thực thi.
doThis(andThenThis)
// Bên trong "doThis" nó được tham chiếu như "callback", cái chỉ là một biến lưu giữ tham chiếu tới hàm này.
function andThenThis() {
console.log('and then this')
}
// Bạn có thể sử dụng bất cứ tên nào bạn muốn, nhưng "callback" được sử dụng phổ biến nhất.
function doThis(callback) {
console.log('this first');
callback()
}
Sử dụng callback cho phép chúng ta làm một số thứ với hàm request đã định nghĩa trước đó:
function request(url, callback) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('Timeout')
}
xhr.open('get', url, true)
xhr.send();
}
Hàm request sẽ có thêm một tham số callback, và khi một request được tạo nó sẽ được gọi trong cả 2 trường hợp lỗi hoặc thành công.
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
request(userGet, function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, function handleReposList(err, repos) {
if (err) throw err
// Handle the repositories list here
})
})
})
Phân tích:
- Chúng tạo một request để lấy danh sách người dùng
- Sau khi request hoàn thành chúng ta sử dụng hàm callback
handleUserList - Nếu không có lỗi chúng ta sẽ phân tích kết quả trả về từ server sử dụng
JSON.parse - Sau đó lặp qua danh sách người dùng khi nó lớn hơn 1. Với mỗi người dùng chúng ta yêu cầu danh sách các repo của họ. Chúng ta sẽ sử dụng url là giá trị của
repos_urltrong mỗi đối tượnguser. - Khi request hoàn thành callback sẽ được gọi. Nó sẽ xử lý lỗi hoặc kết quả trả về là danh sách các repo.
Lưu ý: Sử dụng err như là tham số đầu tiên của callback là một quy ước khá phổ biến, đặc biệt khi sử dụng Node.js.
Một hướng tiếp cần "hoàn chỉnh" và dễ đọc hơn sẽ có một vài hàm xử lý lỗi. Chúng ta cũng tách callback ra khỏi hàm request. Giống như thế này:
try {
request(userGet, handleUsersList)
} catch (e) {
console.error('Request boom! ', e)
}
function handleUsersList(error, users) {
if (error) throw error
const list = JSON.parse(users).items
list.forEach(function(user) {
request(user.repos_url, handleReposList)
})
}
function handleReposList(err, repos) {
if (err) throw err
// Xử lý danh sách repo ở đây
console.log('My very few repos', repos)
}
Đoạn code này có vấn đề là bạn không thể điều khiển thứ tự nhận danh sách repo của người dùng. Chúng ta đang yêu cầu thông tin cho tất cả người dùng trong trường hợp có nhiều hơn 1. Và không thể quyết định được thứ tự sẽ nhận. Ví dụ, danh sách repo của người dùng thứ 10 có thể nhận được đầu tiên và người dùng thứ 2 thì nhận được cuối cùng. Có một giải pháp khả thi cho vấn đề này sẽ được đề cập ở phần sau.
Vấn đề chính với callbacks là việc bảo trì và tính dễ đọc. Đây được gọi là callback hell, cái có thể tránh được với cách tiếp cận ở phần tiếp theo.

Promies
Promises có thể làm cho code của bạn dễ đọc hơn. Một lập trình viên mới có thể xem code base và thấy rõ thứ tự thực thi của code.
Để tạo một promise bạn có thể sử dụng:
const myPromise = new Promise(function(resolve, reject) {
// code here
if (codeIsFine) {
resolve('fine')
} else {
reject('error')
}
})
myPromise
.then(function whenOk(response) {
console.log(response)
return response
})
.catch(function notOk(err) {
console.error(err)
})
Hãy phân tích đoạn code trên:
- Một promise được khởi tạo với hàm với 2 tham số
resolvevàreject - Đặt code bất đồng bộ bên trong hàm
Promise. Gọi hàmresolvekhi mọi thứ hoạt động như mong muốn. Ngược lại gọireject - Khi một
resolveđược tìm thấy phương thức.thenđược thực thi. Khi mộtrejectđược tìm thấy.catchđược kích hoạt.
Những điều cần ghi nhớ:
resolvevàrejectchỉ chấp nhận một tham số.resolve('yey', 'works')sẽ chỉ gửi 'yey' tới.then- Nếu nối chuỗi nhiều
.then. Bạn nên thêm mộtreturntại cuối mỗi callback tương ứng với chúng. - Khi một
rejectxảy racatchsẽ được thực thi, nếu bạn có một.thenđã được nối đằng saucatch. Nó sẽ vẫn thực thi.thenđó. - Với một chuỗi
.thennếu lỗi xảy ra trên hàm đầu tiên. Nó sẽ bỏ qua chuỗi.thenđằng sau cho đến khi tìm thấy một.catch - Một promise có 3 trạng thái
- pending khi nó đang chờ
resolvehoặcrejectxảy ra - resolved
- rejected
- pending khi nó đang chờ
- Khi trong trạng thái
resolvedhoặcrejected. Nó không thể thay đổi.
Lưu ý: Bạn có thể tạo ra các promise mà không cần thực thi ngay tại thời điểm khai báo.
Hãy sử dụng ví dụ request với một promise để minh họa
function request(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.timeout = 2000;
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send();
})
}
Trong trường hợp này khi bạn thực thi request nó sẽ trả về như thế này:

const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const myPromise = request(userGet)
console.log('will be pending when logged', myPromise)
myPromise
.then(function handleUsersList(users) {
console.log('when resolve is found it comes here with the response, in this case users ', users)
const list = JSON.parse(users).items
return Promise.all(list.map(function(user) {
return request(user.repos_url)
}))
})
.then(function handleReposList(repos) {
console.log('All users repos in an array', repos)
})
.catch(function handleErrors(error) {
console.log('when a reject is executed it will come here ignoring the then statement ', error)
})
Đây là cách chúng ta xử lý thứ tự kết quả trả về và một vài vấn đề xử lý lỗi. Code vẫn còn hơi phức tạp. Nhưng đó là một cách để cho bạn thấy rằng cách tiếp cận này cũng có thể tạo ra các vấn đề gây khó đọc.
Chúng ta có thể sửa nhanh để tách các callback ra như thế này:
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Chỉ cần đọc phần này bạn sẽ hiểu đoạn code này đang làm gì
userRequest
.then(handleUsersList)
.then(repoRequest)
.then(handleReposList)
.catch(handleErrors)
function handleUsersList(users) {
return JSON.parse(users).items
}
function repoRequest(users) {
return Promise.all(users.map(function(user) {
return request(user.repos_url)
}))
}
function handleReposList(repos) {
console.log('All users repos in an array', repos)
}
function handleErrors(error) {
console.error('Something went wrong ', error)
}
Bằng cách nhìn vào chuỗi .then đằng sau userRequest bạn có thể biết được cái chúng ta mong đợi từ đoạn code này. Mọi thứ đều có nhiệm vụ riêng.
Đây chỉ là "bề nổi" về Promises. Để có một cái nhìn sâu sắc về cách chúng làm việc tôi khuyên bạn nên đọc thêm bài viết này.
Generators
Một hướng tiếp cận khác là sử dụng generators. Phần này hơi nâng cao một chút vì thế nếu mới bắt đầu có thể thoải mái bỏ qua phần này.
Generators cho phép bạn viết code bất đồng bộ giống như đồng bộ.
Chúng được biểu diễn bằng dấu * trong một hàm như thế này:
function* foo() {
yield 1
const args = yield 2
console.log(args)
}
var fooIterator = foo()
console.log(fooIterator.next().value) // will log 1
console.log(fooIterator.next().value) // will log 2
fooIterator.next('aParam') // will log the console.log inside the generator 'aParam'
Thay vì trả lại với câu lệnh return, generators có một câu lệnh yield. Nó dừng thực thi hàm cho đến khi một phương thức .next được gọi. Tương tự với .then của promise chỉ thực thi khi resoled được trả lại.
Hàm request của chúng ta sẽ trông như thế này:
function request(url) {
return function(callback) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(null, xhr.response)
} else {
callback(xhr.status, null)
}
}
}
xhr.ontimeout = function () {
console.log('timeout')
}
xhr.open('get', url, true)
xhr.send()
}
}
Chúng ta muốn url như một tham số. Nhưng thay vì thực thi request ngay lập tức, chúng ta chỉ muốn khi có một callback để xử lý kết quả trả về (response).
generator sẽ như thế này:
function* list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = yield request(userGet)
yield
for (let i = 0; i<=users.length; i++) {
yield request(users[i].repos_url)
}
}
Nó sẽ:
- Chờ cho đến khi
requestđược chuẩn bị - Chấp nhận một tham số
urlvà trả lạifunctionvới tham số là mộtcallback - Nhận
usersđể gửi tới.nexttiếp theo - Lặp qua
users - Chờ
.nextcho mỗiusers - Trả lại hàm callback tương ứng với chúng
Đoạn code để thực thi sẽ như thế này:
try {
const iterator = list()
iterator.next().value(function handleUsersList(err, users) {
if (err) throw err
const list = JSON.parse(users).items
// send the list of users for the iterator
iterator.next(list)
list.forEach(function(user) {
iterator.next().value(function userRepos(error, repos) {
if (error) throw repos
// Handle each individual user repo here
console.log(user, JSON.parse(repos))
})
})
})
} catch (e) {
console.error(e)
}
Bạn sẽ thấy đoạn code này có những vấn đề tương tự callback hell.
Giống như async/await, một trình biên dịch được khuyến khích. Bởi vì nó không được hỗ trợ trong các trình duyệt cũ.
Ngoài ra, theo kinh nghiệm của tôi generators không phổ biến. Vì thế có thể phát sinh nhầm lẫm trong code base được bảo dưỡng bởi nhiều lập trình viên khác nhau.
Cách generators làm việc có thể được tìm thấy trong bài viết này và đây là một tài nguyên khác.
Async/Await
Phương thức này giống như pha trộn generators với promises. Bạn chỉ cần khai báo function nào là async. Và phần nào trong code sẽ có await để promise kết thúc.
sumTwentyAfterTwoSeconds(10)
.then(result => console.log('after 2 seconds', result))
async function sumTwentyAfterTwoSeconds(value) {
const remainder = afterTwoSeconds(20)
return value + await remainder
}
function afterTwoSeconds(value) {
return new Promise(resolve => {
setTimeout(() => { resolve(value) }, 2000);
});
}
Trong kịch bản này:
- Chúng ta có
sumTwentyAfterTwoSecondslà một hàm async - Chúng ta chờ
resolvehoặcrejectcho hàm promiseafterTwoSeconds - Nó sẽ chỉ kết thúc trong
.thenkhiawaitkết thúc.
Áp dụng vào hàm request chúng ta sẽ để nó như một promise đã thấy trước đó:
function request(url) {
return new Promise(function(resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(xhr.status)
}
}
}
xhr.ontimeout = function () {
reject('timeout')
}
xhr.open('get', url, true)
xhr.send()
})
}
Chúng ta tạo hàm async với các await như thế này:
async function list() {
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const users = await request(userGet)
const usersList = JSON.parse(users).items
usersList.forEach(async function (user) {
const repos = await request(user.repos_url)
handleRepoList(user, repos)
})
}
function handleRepoList(user, repos) {
const userRepos = JSON.parse(repos)
// Handle each individual user repo here
console.log(user, userRepos)
}
Bây giờ chúng ta có một hàm async list, cái sẽ xử lý các request. Một hàm async là cần thiết trong forEach vì chúng ta có danh sách repos của mỗi người dùng.
Chúng ta gọi hàm list:
list()
.catch(e => console.error(e))
Phương pháp này và promises là những phương pháp yêu thích của tôi vì chúng dễ đọc và dễ thay đổi. Bạn có thể đọc nhiều hơn về async/await ở đây.
Một nhược điểm của việc sử dụng async/await là chúng không được hỗ trợ trong các trình duyệt cũ ở phía front-end. Ở phía back-end bạn phải sử dụng Node 8 trở lên.
Bạn có thể sử dụng một trình biên dịch như babel để giải quyết vấn đề này.
Giải pháp
Bạn có thể xem source code sử dụng async/await ở đây.
Bạn cũng có thể tự thử những cách khác nhau đã được đề cập trong bài viết này.
Kết luận
Tùy thuộc vào từng tình huống mà bạn có thể sử dụng:
- async/await
- callbacks
- hỗn hợp
Nó phụ thuộc vào mục đích của bạn. Và cái nào sẽ dễ hiểu với những người khác và với chính bạn trong tương lai để có thể dễ dàng bảo trì code.
Đọc thêm
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
https://codeburst.io/generators-in-javascript-1a7f9f884439
https://contourline.wordpress.com/2017/07/26/mixing-asyncawait-promises-and-callbacks/#comments
https://ponyfoo.com/articles/understanding-javascript-async-await
http://chrisbuttery.com/articles/synchronous-asynchronous-javascript-with-es6-generators/
https://blog.hellojs.org/asynchronous-javascript-from-callback-hell-to-async-and-await-9b9ceb63c8e8


Bình luận