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_url
trong 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ố
resolve
vàreject
- Đặt code bất đồng bộ bên trong hàm
Promise
. Gọi hàmresolve
khi 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ớ:
resolve
vàreject
chỉ 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ộtreturn
tại cuối mỗi callback tương ứng với chúng. - Khi một
reject
xảy racatch
sẽ đượ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
.then
nế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ờ
resolve
hoặcreject
xảy ra - resolved
- rejected
- pending khi nó đang chờ
- Khi trong trạng thái
resolved
hoặ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ố
url
và trả lạifunction
với tham số là mộtcallback
- Nhận
users
để gửi tới.next
tiếp theo - Lặp qua
users
- Chờ
.next
cho 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ó
sumTwentyAfterTwoSeconds
là một hàm async - Chúng ta chờ
resolve
hoặcreject
cho hàm promiseafterTwoSeconds
- Nó sẽ chỉ kết thúc trong
.then
khiawait
kế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