JavaScript là đồng bộ. Điều này có nghĩa là nó sẽ thực thi khối mã của bạn theo thứ tự sau khi cẩu. Trước khi mã thực thi, var
và function
khai báo được “nâng” lên đầu phạm vi của chúng.
Đây là một ví dụ về mã đồng bộ:
console.log('1')
console.log('2')
console.log('3')
Mã này sẽ ghi nhật ký “1 2 3″ một cách đáng tin cậy.
Các yêu cầu không đồng bộ sẽ đợi bộ đếm thời gian kết thúc hoặc yêu cầu phản hồi trong khi phần còn lại của mã tiếp tục thực thi. Sau đó, khi đến thời điểm thích hợp, một cuộc gọi lại sẽ đưa các yêu cầu không đồng bộ này vào hoạt động.
Đây là một ví dụ về mã không đồng bộ:
console.log('1')
setTimeout(function afterTwoSeconds() {
console.log('2')
}, 2000)
console.log('3')
Điều này thực sự sẽ ghi “1 3 2”, vì “2” nằm trên setTimeout
mà sẽ chỉ thực hiện, theo ví dụ này, sau hai giây. Ứng dụng của bạn không bị treo chờ hai giây là xong. Thay vào đó, nó tiếp tục thực thi phần còn lại của mã và khi hết thời gian chờ, nó sẽ quay lại afterTwoSeconds.
Bạn có thể hỏi “Tại sao điều này lại hữu ích?” hoặc “Làm cách nào để mã không đồng bộ của tôi trở thành đồng bộ?”. Hy vọng rằng tôi có thể cho bạn thấy câu trả lời.
“Vấn đề”
Giả sử mục tiêu của chúng tôi là tìm kiếm người dùng GitHub và lấy tất cả các kho lưu trữ của người dùng đó. Vấn đề là chúng tôi không biết tên chính xác của người dùng. Vì vậy, chúng tôi phải liệt kê tất cả người dùng có tên tương tự và kho lưu trữ tương ứng của họ.
Không cần quá cầu kỳ, đại loại như thế này

Trong các ví dụ này, mã yêu cầu sẽ sử dụng XHR (XMLHttpRequest). Bạn có thể thay thế nó bằng jQuery $.ajax
hoặc cách tiếp cận bản địa gần đây hơn được gọi là fetch
. Cả hai sẽ cung cấp cho bạn những lời hứa tiếp cận ra khỏi cổng.
Nó sẽ được thay đổi một chút tùy thuộc vào cách tiếp cận của bạn nhưng với tư cách là người mới bắt đầu:
// 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 mã. Thay vào đó, mục tiêu của bạn là hiểu được sự khác biệt của các phương pháp tiếp cận và cách bạn có thể tận dụng chúng cho sự phát triển của mình.
Gọi lại
Bạn có thể lưu tham chiếu của một hàm trong một biến khi sử dụng JavaScript. Sau đó, bạn có thể sử dụng chúng làm đối số của hàm khác để thực thi sau này. Đây là “cuộc gọi lại” của chúng tôi.
Một ví dụ sẽ là:
// Execute the function "doThis" with another function as parameter, in this case "andThenThis". doThis will execute whatever code it has and when it finishes it should have "andThenThis" being executed.
doThis(andThenThis)
// Inside of "doThis" it's referenced as "callback" which is just a variable that is holding the reference to this function
function andThenThis() {
console.log('and then this')
}
// You can name it whatever you want, "callback" is common approach
function doThis(callback) {
console.log('this first')
// the '()' is when you are telling your code to execute the function reference else it will just log the reference
callback()
}
Sử dụng callback
để giải quyết vấn đề của chúng tôi cho phép chúng tôi làm điều gì đó như thế này với request
chức năng chúng tôi đã xác định 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();
}
Chức năng của chúng tôi cho yêu cầu bây giờ sẽ chấp nhận một callback
để khi một request
được thực hiện, nó sẽ được gọi trong trường hợp có lỗi và trong trường hợp 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á vỡ điều này xuống:
- Chúng tôi yêu cầu lấy kho lưu trữ của người dùng
- Sau khi yêu cầu hoàn tất, chúng tôi sử dụng gọi lại
handleUsersList
- Nếu không có lỗi thì chúng tôi phân tích phản hồi máy chủ của mình thành một đối tượng bằng cách sử dụng
JSON.parse
- Sau đó, chúng tôi lặp lại danh sách người dùng của mình vì nó có thể có nhiều hơn một
Đối với mỗi người dùng, chúng tôi yêu cầu danh sách kho lưu trữ của họ.
Chúng tôi sẽ sử dụng url được trả về cho mỗi người dùng trong phản hồi đầu tiên của chúng tôi
Chúng tôi gọirepos_url
làm url cho các yêu cầu tiếp theo của chúng tôi hoặc từ phản hồi đầu tiên - Khi yêu cầu đã hoàn thành cuộc gọi lại, chúng tôi sẽ gọi
Điều này sẽ xử lý lỗi của nó hoặc phản hồi với danh sách các kho lưu trữ cho người dùng đó
Ghi chú: Gửi lỗi trước dưới dạng tham số là một phương pháp phổ biến, đặc biệt là khi sử dụng Node.js.
Một cách tiếp cận “đầy đủ” và dễ đọc hơn là xử lý lỗi. Chúng tôi sẽ giữ cuộc gọi lại tách biệt với việc thực hiện yêu cầu.
Một cái gì đó 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
// Handle the repositories list here
console.log('My very few repos', repos)
}
Điều này dẫn đến các vấn đề như các vấn đề về đua xe và xử lý lỗi. Cuộc đua xảy ra khi bạn không kiểm soát được người dùng nào bạn sẽ có trước. Chúng tôi đang yêu cầu thông tin cho tất cả chúng trong trường hợp có nhiều hơn một. Chúng tôi không tính đến một đơn đặt hàng. Ví dụ: người dùng 10 có thể đến trước và người dùng 2 đến sau cùng. Chúng tôi có một giải pháp khả thi sau này trong bài viết.
Vấn đề chính với các cuộc gọi lại là việc bảo trì và khả năng đọc có thể trở thành một vấn đề khó khăn. Nó sắp như vậy rồi và mã hầu như không làm gì cả. Điều này được gọi là địa ngục gọi lại có thể tránh được với cách tiếp cận tiếp theo của chúng tôi.

lời hứa
Hứa rằng bạn có thể làm cho mã của mình dễ đọc hơn. Một nhà phát triển mới có thể đến cơ sở mã và xem thứ tự thực thi rõ ràng đối với mã của bạn.
Để tạo một lời hứa, 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 để chúng tôi phân tách nó:
- Một lời hứa được khởi tạo với một
function
cái đó córesolve
vàreject
các câu lệnh - Tạo mã không đồng bộ của bạn bên trong
Promise
chức năngresolve
khi mọi thứ diễn ra như mong muốn
Nếu không thìreject
- Khi một
resolve
được tìm thấy.then
phương thức sẽ thực thi cho điều đóPromise
Khi mộtreject
được tìm thấy.catch
sẽ đượ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’ đến.then
chức năng gọi lại- Nếu bạn xâu chuỗi nhiều
.then
thêm mộtreturn
nếu bạn muốn tiếp theo.then
giá trị không đượcundefined
- Khi một
reject
bị bắt với.catch
nếu bạn có một.then
xích vào nó
Nó vẫn sẽ thực hiện điều đó.then
Bạn có thể nhìn thấy.then
dưới dạng “luôn thực thi” và bạn có thể kiểm tra một ví dụ trong nhận xét này - Với một chuỗi trên
.then
nếu một lỗi xảy ra trên cái đầu tiên
Nó sẽ bỏ qua tiếp theo.then
cho đến khi nó tìm thấy một.catch
- Một lời hứa có ba trạng thái
chưa giải quyết - Khi chờ đợi một
resolve
hoặcreject
xảy ra
giải quyết
vật bị loại bỏ - Một khi nó ở trong một
resolved
hoặcrejected
tiểu bang
Nó không thể thay đổi
Ghi chú: Bạn có thể tạo lời hứa mà không cần chức năng tại thời điểm khai báo. Cách mà tôi đang trình bày chỉ là một cách phổ biến để làm việc đó.
“Lý thuyết, lý thuyết, lý thuyết…Tôi đang bối rối” bạn có thể nói.
Hãy sử dụng ví dụ về yêu cầu của chúng tôi với một lời hứa sẽ cố gắng làm sáng tỏ mọi thứ:
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 hiện request
nó sẽ trả về một cái gì đó 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 tôi giải quyết vấn đề đua xe và một số vấn đề xử lý lỗi. Mã này vẫn còn một chút 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 đề về khả năng đọc.
Cách khắc phục nhanh là tách các cuộc gọi lại như sau:
const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users`
const userRequest = request(userGet)
// Just by reading this part out loud you have a good idea of what the code does
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 những gì userRequest
đang chờ đợi theo thứ tự với .then
bạn có thể hiểu được những gì chúng tôi mong đợi ở khối mã này. Mọi thứ ít nhiều được ngăn cách bởi trách nhiệm.
Đây là “làm xước bề mặt” của những gì Hứa hẹn. Để có cái nhìn sâu sắc về cách chúng hoạt động, tôi không thể giới thiệu đủ bài viết này.
máy phát điện
Một cách tiếp cận khác là sử dụng máy phát điện. Đây là phần nâng cao hơn một chút, vì vậy nếu bạn mới bắt đầu, vui lòng chuyển sang chủ đề tiếp theo.
Một cách sử dụng cho trình tạo là chúng cho phép bạn có mã không đồng bộ trông giống như đồng bộ hóa.
Chúng được đại diện bởi một *
trong một chức năng và trông giống như:
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 một return
máy phát điện có một yield
bản tường trình. Nó dừng thực thi chức năng cho đến khi một .next
được thực hiện cho chức năng lặp lại đó. Nó tương tự như .then
lời hứa chỉ thực hiện khi giải quyết trở lại.
Chức năng yêu cầu của chúng tôi 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 tôi muốn có url
như một lý lẽ. Nhưng thay vì thực hiện yêu cầu ngoài cổng, chúng tôi chỉ muốn nó khi chúng tôi có một cuộc gọi lại để xử lý phản hồi.
Của chúng ta generator
sẽ là một cái gì đó như:
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 đầu tiên
request
được chuẩn bị - Trả lại một
function
tài liệu tham khảo mong đợi mộtcallback
lần đầu tiênrequest
Của chúng tarequest
chức năng chấp nhận mộturl
và trả về mộtfunction
mà mong đợi mộtcallback
- mong đợi một
users
sẽ được gửi trong lần tiếp theo.next
- Lặp đi lặp lại
users
- đợi một
.next
cho mỗiusers
- Trả lại chức năng gọi lại tương ứng của họ
Vì vậy, việc thực hiện điều này sẽ là:
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)
}
Chúng ta có thể tách các chức năng gọi lại như chúng ta đã làm trước đây. Bây giờ bạn đã có thỏa thuận, một điểm đáng chú ý là giờ đây chúng tôi có thể xử lý từng danh sách kho lưu trữ người dùng riêng lẻ.
Tôi có nhiều suy nghĩ khác nhau về máy phát điện. Một mặt, tôi có thể nắm bắt được những gì được mong đợi ở mã bằng cách xem trình tạo.
Nhưng quá trình thực thi của nó kết thúc bằng những vấn đề tương tự như địa ngục gọi lại.
Giống như async/await, nên sử dụng trình biên dịch. Điều này là do nó không được hỗ trợ trong các phiên bản trình duyệt cũ hơn.
Ngoài ra nó không phải là phổ biến trong kinh nghiệm của tôi. Vì vậy, nó có thể gây nhầm lẫn trong các cơ sở mã được duy trì bởi các nhà phát triển khác nhau.
Bạn có thể tìm thấy thông tin chi tiết tuyệt vời về cách thức hoạt động của máy phát điện trong bài viết này. Và đây là một nguồn tài nguyên tuyệt vời khác.
Không đồng bộ/Đang chờ
Phương pháp này có vẻ giống như sự kết hợp của các trình tạo với lời hứa. Bạn chỉ cần nói cho mã của mình biết các chức năng sẽ là gì async
. Và phần nào của mã sẽ phải await
vì điều đó promise
kêt thuc.
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
như là một chức năng không đồng bộ - Chúng tôi bảo mã của chúng tôi đợi
resolve
hoặcreject
cho chức năng lời hứa của chúng tôiafterTwoSeconds
- Nó sẽ chỉ kết thúc trong
.then
khi màawait
hoạt động kết thúc
Trong trường hợp này chỉ có một
Áp dụng điều này cho chúng tôi request
chúng tôi để nó như một promise
như đã 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 tôi tạo ra của chúng tôi async
chức năng với sự chờ đợi cần thiết như vậ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)
}
Vì vậy, bây giờ chúng tôi có một async list
chức năng sẽ xử lý các yêu cầu. Một async khác là cần thiết trong forEach
để chúng tôi có danh sách repos
để mỗi người dùng thao tác.
Chúng tôi gọi nó là:
list()
.catch(e => console.error(e))
Cách tiếp cận này và cách tiếp cận hứa hẹn là mục yêu thích của tôi vì mã dễ đọc và dễ thay đổi. Bạn có thể đọc sâu hơn về async/await tại đây.
Một nhược điểm của việc sử dụng async/await là nó không được hỗ trợ ở mặt trước bởi các trình duyệt cũ hơn hoặc ở mặt sau. Bạn phải sử dụng Nút 8.
Bạn có thể sử dụng trình biên dịch như babel để giải quyết vấn đề đó.
“Dung dịch”
Bạn có thể thấy mã kết thúc hoàn thành mục tiêu ban đầu của chúng tôi bằng cách sử dụng async/await trong đoạn mã này.
Một điều tốt nên làm là tự mình thử theo các hình thức khác nhau được đề cập trong bài viết này.
Phần kết luận
Tùy thuộc vào tình huống mà bạn có thể thấy mình đang sử dụng:
Tùy thuộc vào bạn những gì phù hợp với mục đích của bạn. Và điều gì cho phép bạn duy trì mã sao cho người khác và bản thân bạn trong tương lai có thể hiểu được.
Ghi chú: Bất kỳ cách tiếp cận nào trở nên ít dài dòng hơn một chút khi sử dụng các lựa chọn thay thế cho các yêu cầu như $.ajax
và fetch
.
Hãy cho tôi biết bạn sẽ làm những gì khác nhau và những cách khác nhau mà bạn tìm thấy để làm cho mỗi cách tiếp cận dễ đọc hơn.
Đây là Điều 11 trên 30. Đây là một phần của dự án xuất bản một bài báo ít nhất một lần một tuần, từ những suy nghĩ vu vơ đến hướng dẫn. Để lại nhận xét, theo dõi tôi trên Diogo Spínola và sau đó quay lại dự án tuyệt vời của bạn!