→ Để giải thích tổng quát hơn về hành vi không đồng bộ với các ví dụ khác nhau, vui lòng xem Tại sao biến của tôi không bị thay đổi sau khi tôi sửa đổi nó bên trong hàm? - Tham chiếu mã không đồng bộ
→ Nếu bạn đã hiểu vấn đề, hãy bỏ qua các giải pháp có thể dưới đây.
Vấn đề
Chữ A trong Ajax là viết tắt của không đồng bộ . Điều đó có nghĩa là gửi yêu cầu (hay đúng hơn là nhận phản hồi) được đưa ra khỏi luồng thực thi thông thường. Trong ví dụ của bạn, $.ajax
trả về ngay lập tức và câu lệnh tiếp theo return result;
, được thực thi trước khi hàm bạn chuyển qua khi success
gọi lại thậm chí được gọi.
Đây là một sự tương tự mà hy vọng làm cho sự khác biệt giữa dòng chảy đồng bộ và không đồng bộ rõ ràng hơn:
Đồng bộ
Hãy tưởng tượng bạn gọi điện thoại cho một người bạn và yêu cầu anh ta tìm kiếm thứ gì đó cho bạn. Mặc dù có thể mất một lúc, bạn đợi điện thoại và nhìn chằm chằm vào không gian, cho đến khi bạn của bạn đưa ra câu trả lời mà bạn cần.
Điều tương tự cũng xảy ra khi bạn thực hiện một cuộc gọi hàm chứa mã "bình thường":
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Mặc dù findItem
có thể mất nhiều thời gian để thực thi, bất kỳ mã nào đến sau var item = findItem();
phải đợi cho đến khi hàm trả về kết quả.
Không đồng bộ
Bạn gọi lại cho bạn của bạn vì lý do tương tự. Nhưng lần này bạn nói với anh ấy rằng bạn đang vội và anh ấy nên gọi lại cho bạn trên điện thoại di động của bạn. Bạn cúp máy, rời khỏi nhà và làm bất cứ điều gì bạn dự định làm. Khi bạn của bạn gọi lại cho bạn, bạn đang xử lý thông tin anh ấy đã cung cấp cho bạn.
Đó chính xác là những gì xảy ra khi bạn thực hiện một yêu cầu Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Thay vì chờ phản hồi, việc thực thi tiếp tục ngay lập tức và câu lệnh sau khi lệnh gọi Ajax được thực thi. Để nhận được phản hồi cuối cùng, bạn cung cấp một chức năng được gọi sau khi nhận được phản hồi, gọi lại (có thông báo gì không? Gọi lại ?). Bất kỳ câu lệnh nào đến sau cuộc gọi đó được thực thi trước khi gọi lại.
Các giải pháp)
Nắm bắt bản chất không đồng bộ của JavaScript! Mặc dù các hoạt động không đồng bộ nhất định cung cấp các đối tác đồng bộ ("Ajax"), nhưng nói chung, không khuyến khích sử dụng chúng, đặc biệt là trong bối cảnh trình duyệt.
Tại sao nó xấu khi bạn hỏi?
JavaScript chạy trong luồng UI của trình duyệt và bất kỳ quá trình chạy dài nào cũng sẽ khóa UI, khiến nó không phản hồi. Ngoài ra, có giới hạn trên về thời gian thực hiện đối với JavaScript và trình duyệt sẽ hỏi người dùng có tiếp tục thực hiện hay không.
Tất cả điều này là trải nghiệm người dùng thực sự xấu. Người dùng sẽ không thể biết liệu mọi thứ có hoạt động tốt hay không. Hơn nữa, hiệu quả sẽ tồi tệ hơn đối với người dùng có kết nối chậm.
Sau đây chúng ta sẽ xem xét ba giải pháp khác nhau, tất cả đều được xây dựng chồng lên nhau:
- Hứa hẹn với
async/await
(ES2017 +, khả dụng trong các trình duyệt cũ hơn nếu bạn sử dụng bộ chuyển mã hoặc trình tái tạo)
- Gọi lại (phổ biến trong nút)
- Hứa với
then()
(ES2015 +, khả dụng trong các trình duyệt cũ hơn nếu bạn sử dụng một trong nhiều thư viện hứa hẹn)
Cả ba đều có sẵn trong các trình duyệt hiện tại và nút 7+.
Phiên bản ECMAScript được phát hành năm 2017 đã giới thiệu hỗ trợ cấp cú pháp cho các chức năng không đồng bộ. Với sự giúp đỡ của async
và await
, bạn có thể viết không đồng bộ theo "kiểu đồng bộ". Mã vẫn không đồng bộ, nhưng dễ đọc / dễ hiểu hơn.
async/await
xây dựng dựa trên lời hứa: một async
chức năng luôn trả về một lời hứa. await
"hủy bỏ" một lời hứa và dẫn đến giá trị của lời hứa đã được giải quyết hoặc ném lỗi nếu lời hứa bị từ chối.
Quan trọng: Bạn chỉ có thể sử dụng await
bên trong một async
chức năng. Ngay bây giờ, cấp cao nhất await
chưa được hỗ trợ, do đó bạn có thể phải tạo một IIFE async ( Biểu thức hàm được gọi ngay lập tức ) để bắt đầu một async
bối cảnh.
Bạn có thể đọc thêm về async
và await
trên MDN.
Dưới đây là một ví dụ được xây dựng dựa trên độ trễ trên:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
Phiên bản trình duyệt và nút hiện tại hỗ trợ async/await
. Bạn cũng có thể hỗ trợ các môi trường cũ hơn bằng cách chuyển đổi mã của mình sang ES5 với sự trợ giúp của trình tái tạo (hoặc các công cụ sử dụng trình tái tạo, chẳng hạn như Babel ).
Để các hàm chấp nhận cuộc gọi lại
Một cuộc gọi lại chỉ đơn giản là một chức năng được chuyển đến một chức năng khác. Hàm khác có thể gọi hàm được truyền bất cứ khi nào nó sẵn sàng. Trong ngữ cảnh của một quy trình không đồng bộ, cuộc gọi lại sẽ được gọi bất cứ khi nào quá trình không đồng bộ được thực hiện. Thông thường, kết quả được chuyển đến cuộc gọi lại.
Trong ví dụ của câu hỏi, bạn có thể foo
chấp nhận gọi lại và sử dụng nó làm success
cuộc gọi lại. Vậy đây
var result = foo();
// Code that depends on 'result'
trở thành
foo(function(result) {
// Code that depends on 'result'
});
Ở đây chúng tôi đã định nghĩa hàm "nội tuyến" nhưng bạn có thể vượt qua bất kỳ tham chiếu chức năng nào:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
chính nó được định nghĩa như sau:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
sẽ đề cập đến chức năng chúng ta chuyển đến foo
khi chúng ta gọi nó và chúng ta chỉ cần chuyển nó sang success
. Tức là một khi yêu cầu Ajax thành công, $.ajax
sẽ gọi callback
và chuyển phản hồi cho cuộc gọi lại (có thể được gọi bằng result
, vì đây là cách chúng tôi đã xác định cuộc gọi lại).
Bạn cũng có thể xử lý phản hồi trước khi chuyển nó đến cuộc gọi lại:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
Viết mã bằng cách gọi lại dễ dàng hơn có vẻ. Rốt cuộc, JavaScript trong trình duyệt được điều khiển rất nhiều sự kiện (các sự kiện DOM). Nhận được phản hồi Ajax không gì khác ngoài một sự kiện.
Khó khăn có thể phát sinh khi bạn phải làm việc với mã của bên thứ ba, nhưng hầu hết các vấn đề có thể được giải quyết chỉ bằng cách suy nghĩ thông qua dòng ứng dụng.
Các Promise API là một tính năng mới của ECMAScript 6 (ES2015), nhưng nó có tốt hỗ trợ trình duyệt rồi. Ngoài ra còn có nhiều thư viện triển khai API Promise tiêu chuẩn và cung cấp các phương thức bổ sung để dễ dàng sử dụng và cấu thành các chức năng không đồng bộ (ví dụ: bluebird ).
Lời hứa là container cho các giá trị trong tương lai . Khi lời hứa nhận được giá trị (nó đã được giải quyết ) hoặc khi nó bị hủy ( bị từ chối ), nó sẽ thông báo cho tất cả "người nghe" của mình, những người muốn truy cập giá trị này.
Ưu điểm so với các cuộc gọi lại đơn giản là chúng cho phép bạn tách mã của bạn và chúng dễ dàng soạn thảo hơn.
Đây là một ví dụ đơn giản về việc sử dụng một lời hứa:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Áp dụng cho lệnh gọi Ajax của chúng tôi, chúng tôi có thể sử dụng các lời hứa như thế này:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
Mô tả tất cả các lợi thế mà lời hứa cung cấp nằm ngoài phạm vi của câu trả lời này, nhưng nếu bạn viết mã mới, bạn nên nghiêm túc xem xét chúng. Họ cung cấp một sự trừu tượng hóa và phân tách mã của bạn.
Thông tin thêm về lời hứa: Đá HTML5 - Lời hứa JavaScript
Lưu ý bên lề: Các đối tượng bị trì hoãn của jQuery
Các đối tượng bị trì hoãn là việc thực hiện các lời hứa tùy chỉnh của jQuery (trước khi API Promise được chuẩn hóa). Họ hành xử gần giống như những lời hứa nhưng để lộ một API hơi khác.
Mọi phương thức Ajax của jQuery đã trả về một "đối tượng hoãn lại" (thực ra là một lời hứa về một đối tượng bị trì hoãn) mà bạn có thể trả về từ hàm của mình:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Lưu ý phụ: Promise gotchas
Hãy nhớ rằng những lời hứa và các đối tượng bị trì hoãn chỉ là vật chứa cho một giá trị trong tương lai, bản thân chúng không phải là giá trị. Ví dụ: giả sử bạn có những điều sau đây:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
Mã này hiểu sai các vấn đề không đồng bộ ở trên. Cụ thể, $.ajax()
không đóng băng mã trong khi nó kiểm tra trang '/ password' trên máy chủ của bạn - nó sẽ gửi yêu cầu đến máy chủ và trong khi chờ, nó sẽ trả về ngay một đối tượng Trì hoãn Ajax, chứ không phải phản hồi từ máy chủ. Điều đó có nghĩa là if
câu lệnh sẽ luôn lấy đối tượng Trì hoãn này, coi nó như true
và tiến hành như thể người dùng đã đăng nhập. Không tốt.
Nhưng cách khắc phục rất dễ:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
Không được đề xuất: Các cuộc gọi "Ajax" đồng bộ
Như tôi đã đề cập, một số hoạt động không đồng bộ (!) Có các đối tác đồng bộ. Tôi không ủng hộ việc sử dụng chúng, nhưng để hoàn thiện, đây là cách bạn sẽ thực hiện một cuộc gọi đồng bộ:
Không có jQuery
Nếu bạn trực tiếp sử dụng một XMLHTTPRequest
đối tượng, chuyển false
làm đối số thứ ba cho .open
.
jQuery
Nếu bạn sử dụng jQuery , bạn có thể đặt async
tùy chọn thành false
. Lưu ý rằng tùy chọn này không được dùng kể từ jQuery 1.8. Sau đó, bạn vẫn có thể sử dụng một success
cuộc gọi lại hoặc truy cập vào thuộc responseText
tính của đối tượng jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Nếu bạn sử dụng bất kỳ phương thức jQuery Ajax nào khác, chẳng hạn như $.get
, $.getJSON
v.v., bạn phải thay đổi nó thành $.ajax
(vì bạn chỉ có thể truyền tham số cấu hình cho $.ajax
).
Đứng lên! Không thể thực hiện một yêu cầu JSONP đồng bộ . JSONP về bản chất luôn luôn không đồng bộ (một lý do nữa để thậm chí không xem xét tùy chọn này).