Tại sao biến của tôi không thay đổi sau khi tôi sửa đổi nó bên trong một hàm? - Tham chiếu mã không đồng bộ
Cho các ví dụ sau đây, tại sao outerScopeVar
không xác định trong mọi trường hợp?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
Tại sao nó xuất hiện undefined
trong tất cả các ví dụ này? Tôi không muốn giải pháp thay thế, tôi muốn biết tại sao điều này lại xảy ra.
Lưu ý: Đây là một câu hỏi chuẩn cho tính không đồng bộ của JavaScript . Vui lòng cải thiện câu hỏi này và thêm nhiều ví dụ đơn giản hơn mà cộng đồng có thể xác định.
Trả lời
Câu trả lời một từ: không đồng bộ .
Lời nói đầu
Chủ đề này đã được lặp lại ít nhất vài nghìn lần, tại đây, trong Stack Overflow. Do đó, trước hết tôi muốn chỉ ra một số tài nguyên cực kỳ hữu ích:
Câu trả lời của @Felix Kling cho "Làm cách nào để trả lại phản hồi từ cuộc gọi không đồng bộ?" . Xem câu trả lời xuất sắc của anh ấy giải thích các luồng đồng bộ và không đồng bộ, cũng như phần "Tái cấu trúc mã".
@Benjamin Gruenbaum cũng đã nỗ lực rất nhiều để giải thích sự không đồng bộ trong cùng một chủ đề.Câu trả lời của @Matt Esch cho "Lấy dữ liệu từ fs.readFile" cũng giải thích sự không đồng bộ rất tốt theo cách đơn giản.
Câu trả lời cho câu hỏi trong tầm tay
Hãy theo dõi các hành vi phổ biến trước. Trong tất cả các ví dụ, hàm outerScopeVar
được sửa đổi bên trong một hàm . Hàm đó rõ ràng không được thực thi ngay lập tức, nó đang được gán hoặc truyền như một đối số. Đó là những gì chúng tôi gọi là một cuộc gọi lại .
Bây giờ câu hỏi là, cuộc gọi lại đó được gọi khi nào?
Nó phụ thuộc vào từng trường hợp. Hãy thử theo dõi lại một số hành vi phổ biến:
img.onload
có thể được gọi vào một lúc nào đó trong tương lai , khi (và nếu) hình ảnh đã tải thành công.setTimeout
có thể được gọi đôi khi trong tương lai , sau khi trì hoãn đã hết hạn và thời gian chờ đã không bị hủy bởiclearTimeout
. Lưu ý: ngay cả khi sử dụng0
làm độ trễ, tất cả các trình duyệt đều có giới hạn độ trễ thời gian chờ tối thiểu (được chỉ định là 4ms trong thông số kỹ thuật HTML5).- Lệnh
$.post
gọi lại của jQuery có thể được gọi vào một lúc nào đó trong tương lai , khi (và nếu) yêu cầu Ajax đã được hoàn thành thành công. - Node.js's
fs.readFile
có thể được gọi vào một thời điểm nào đó trong tương lai , khi tệp đã được đọc thành công hoặc gặp lỗi.
Trong mọi trường hợp, chúng tôi có một cuộc gọi lại có thể chạy đôi khi trong tương lai . "Đôi khi trong tương lai" này được chúng tôi gọi là luồng không đồng bộ .
Thực thi không đồng bộ bị đẩy ra khỏi luồng đồng bộ. Nghĩa là, mã không đồng bộ sẽ không bao giờ thực thi trong khi ngăn xếp mã đồng bộ đang thực thi. Đây là ý nghĩa của JavaScript là một luồng.
Cụ thể hơn, khi công cụ JS không hoạt động - không thực thi một chồng (a) mã đồng bộ - nó sẽ thăm dò các sự kiện có thể đã kích hoạt lệnh gọi lại không đồng bộ (ví dụ: hết thời gian chờ, đã nhận được phản hồi mạng) và thực thi chúng lần lượt. Đây được coi là Vòng lặp sự kiện .
Nghĩa là, mã không đồng bộ được đánh dấu trong các hình màu đỏ vẽ tay chỉ có thể thực thi sau khi tất cả các mã đồng bộ còn lại trong các khối mã tương ứng của chúng đã thực thi:
Nói tóm lại, các hàm gọi lại được tạo đồng bộ nhưng thực thi không đồng bộ. Bạn chỉ không thể dựa vào việc thực thi một hàm không đồng bộ cho đến khi bạn biết nó đã được thực thi, và làm thế nào để thực hiện điều đó?
Nó là đơn giản, thực sự. Logic phụ thuộc vào việc thực thi hàm không đồng bộ nên được khởi động / gọi từ bên trong hàm không đồng bộ này. Ví dụ, di chuyển cả alert
s và console.log
s bên trong hàm gọi lại sẽ xuất ra kết quả mong đợi, vì kết quả có sẵn tại thời điểm đó.
Triển khai logic gọi lại của riêng bạn
Thường thì bạn cần làm nhiều việc hơn với kết quả từ một hàm không đồng bộ hoặc làm những việc khác với kết quả tùy thuộc vào vị trí mà hàm không đồng bộ đã được gọi. Hãy giải quyết một ví dụ phức tạp hơn một chút:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Lưu ý: Tôi đang sử dụng setTimeout
với một sự chậm trễ ngẫu nhiên như một chức năng không đồng bộ chung chung, ví dụ tương tự áp dụng cho Ajax, readFile
, onload
và bất kỳ dòng chảy không đồng bộ khác.
Ví dụ này rõ ràng gặp phải vấn đề tương tự như các ví dụ khác, đó là không đợi cho đến khi thực thi hàm không đồng bộ.
Hãy giải quyết nó bằng cách triển khai một hệ thống gọi lại của riêng chúng ta. Trước hết, chúng tôi loại bỏ cái xấu xí outerScopeVar
đó hoàn toàn vô dụng trong trường hợp này. Sau đó, chúng tôi thêm một tham số chấp nhận một đối số hàm, gọi lại của chúng tôi. Khi hoạt động không đồng bộ kết thúc, chúng tôi gọi cuộc gọi lại này truyền kết quả. Việc thực hiện (vui lòng đọc các ý kiến theo thứ tự):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Đoạn mã của ví dụ trên:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Thông thường, trong các trường hợp sử dụng thực, API DOM và hầu hết các thư viện đã cung cấp chức năng gọi lại (cách helloCatAsync
triển khai trong ví dụ minh họa này). Bạn chỉ cần chuyển hàm gọi lại và hiểu rằng nó sẽ thực thi ngoài luồng đồng bộ và cấu trúc lại mã của bạn để phù hợp với điều đó.
Bạn cũng sẽ nhận thấy rằng do tính chất không đồng bộ, không thể để return
một giá trị từ luồng không đồng bộ quay trở lại luồng đồng bộ nơi lệnh gọi lại được xác định, vì lệnh gọi lại không đồng bộ được thực thi lâu sau khi mã đồng bộ đã thực thi xong.
Thay vì nhập return
một giá trị từ một lệnh gọi lại không đồng bộ, bạn sẽ phải sử dụng mẫu gọi lại, hoặc ... Lời hứa.
Lời hứa
Mặc dù có nhiều cách để ngăn chặn callback hell với vani JS, nhưng các hứa hẹn đang ngày càng phổ biến và hiện đang được tiêu chuẩn hóa trong ES6 (xem Promise - MDN ).
Promises (hay còn gọi là Futures) cung cấp cách đọc mã không đồng bộ tuyến tính hơn và do đó dễ chịu hơn, nhưng việc giải thích toàn bộ chức năng của chúng nằm ngoài phạm vi của câu hỏi này. Thay vào đó, tôi sẽ để lại những tài nguyên tuyệt vời này cho những người quan tâm:
Tài liệu đọc thêm về tính không đồng bộ của JavaScript
- Nghệ thuật của nút - Gọi lại giải thích mã không đồng bộ và gọi lại rất tốt với các ví dụ JS vani và mã Node.js.
Lưu ý: Tôi đã đánh dấu câu trả lời này là Wiki Cộng đồng, do đó bất kỳ ai có ít nhất 100 danh tiếng đều có thể chỉnh sửa và cải thiện nó! Vui lòng cải thiện câu trả lời này hoặc gửi một câu trả lời hoàn toàn mới nếu bạn muốn.
Tôi muốn chuyển câu hỏi này thành một chủ đề chính tắc để trả lời các vấn đề không đồng bộ không liên quan đến Ajax (có Cách trả lại phản hồi từ lệnh gọi AJAX? Cho điều đó), do đó chủ đề này cần sự trợ giúp của bạn để trở nên tốt và hữu ích nhất có thể !
Câu trả lời của Fabrício là đúng; nhưng tôi muốn bổ sung câu trả lời của anh ấy bằng một cái gì đó ít kỹ thuật hơn, tập trung vào phép loại suy để giúp giải thích khái niệm không đồng bộ .
Phép tương tự ...
Hôm qua, công việc tôi đang làm yêu cầu một số thông tin từ một đồng nghiệp. Tôi gọi cho anh ta; đây là cách cuộc trò chuyện diễn ra:
Me : Hi Bob, tôi cần phải biết làm thế nào chúng ta foo 'd các thanh ' d tuần trước. Jim muốn báo cáo về nó và bạn là người duy nhất biết chi tiết về nó.
Bob : Chắc chắn rồi, nhưng tôi sẽ mất khoảng 30 phút?
Tôi : Thật tuyệt, Bob. Hãy trả lại cho tôi khi bạn có thông tin!
Đến lúc này, tôi cúp máy. Vì tôi cần thông tin từ Bob để hoàn thành báo cáo của mình, nên tôi rời báo cáo và đi uống cà phê, sau đó tôi nhận được một số email. 40 phút sau (Bob chậm), Bob gọi lại và cho tôi thông tin tôi cần. Tại thời điểm này, tôi tiếp tục công việc với báo cáo của mình, vì tôi đã có tất cả thông tin cần thiết.
Hãy tưởng tượng nếu cuộc trò chuyện diễn ra như thế này;
Me : Hi Bob, tôi cần phải biết làm thế nào chúng ta foo 'd các thanh ' d tuần trước. Jim muốn báo cáo về nó, và bạn là người duy nhất biết chi tiết về nó.
Bob : Chắc chắn rồi, nhưng tôi sẽ mất khoảng 30 phút?
Tôi : Thật tuyệt, Bob. Tôi sẽ đợi.
Và tôi ngồi đó và chờ đợi. Và đã đợi. Và đã đợi. Trong 40 phút. Không làm gì khác ngoài chờ đợi. Cuối cùng, Bob đã cung cấp cho tôi thông tin, chúng tôi gác máy và tôi đã hoàn thành báo cáo của mình. Nhưng tôi đã mất 40 phút năng suất.
Đây là hành vi không đồng bộ so với hành vi đồng bộ
Đây chính xác là những gì đang xảy ra trong tất cả các ví dụ trong câu hỏi của chúng tôi. Tải hình ảnh, tải tệp ra đĩa và yêu cầu trang qua AJAX đều là những hoạt động chậm chạp (trong bối cảnh máy tính hiện đại).
Thay vì chờ đợi các hoạt động chậm này hoàn thành, JavaScript cho phép bạn đăng ký một hàm gọi lại sẽ được thực thi khi hoạt động chậm đã hoàn thành. Tuy nhiên, trong khi chờ đợi, JavaScript sẽ tiếp tục thực thi mã khác. Thực tế là JavaScript thực thi mã khác trong khi đợi hoạt động chậm hoàn thành làm cho hành vi không đồng bộ . Nếu JavaScript đợi hoạt động hoàn tất trước khi thực thi bất kỳ mã nào khác, thì đây sẽ là hành vi đồng bộ .
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
Trong đoạn mã trên, chúng tôi yêu cầu JavaScript tải lolcat.png
, đây là một hoạt động sloooow . Hàm gọi lại sẽ được thực thi sau khi hoạt động chậm này hoàn tất, nhưng trong thời gian chờ đợi, JavaScript sẽ tiếp tục xử lý các dòng mã tiếp theo; tức là alert(outerScopeVar)
.
Đây là lý do tại sao chúng tôi thấy cảnh báo hiển thị undefined
; vì alert()
nó được xử lý ngay lập tức, thay vì sau khi hình ảnh đã được tải.
Để sửa mã của chúng tôi, tất cả những gì chúng tôi phải làm là di chuyển alert(outerScopeVar)
mã vào hàm gọi lại. Do đó, chúng ta không cần outerScopeVar
biến được khai báo như một biến toàn cục nữa.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Bạn sẽ luôn thấy một lệnh gọi lại được chỉ định dưới dạng một hàm, vì đó là cách * duy nhất trong JavaScript để xác định một số mã, nhưng không thực thi nó cho đến sau này.
Do đó, trong tất cả các ví dụ của chúng tôi, function() { /* Do something */ }
gọi lại là; để sửa chữa tất cả các ví dụ, tất cả những gì chúng ta phải làm là di chuyển mã cần phản hồi của hoạt động vào đó!
* Về mặt kỹ thuật, bạn có thể sử dụng eval()
là tốt, nhưng eval()
là ác cho mục đích này
Làm cách nào để giữ cho người gọi của tôi đợi?
Bạn hiện có thể có một số mã tương tự như thế này;
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Tuy nhiên, bây giờ chúng ta biết rằng điều đó return outerScopeVar
xảy ra ngay lập tức; trước khi onload
hàm gọi lại đã cập nhật biến. Điều này dẫn đến việc getWidthOfImage()
quay trở lại undefined
và undefined
được cảnh báo.
Để khắc phục điều này, chúng ta cần cho phép hàm đang gọi getWidthOfImage()
đăng ký một cuộc gọi lại, sau đó di chuyển cảnh báo về độ rộng nằm trong lệnh gọi lại đó;
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... như trước đây, hãy lưu ý rằng chúng tôi đã có thể xóa các biến toàn cục (trong trường hợp này width
).
Dưới đây là câu trả lời ngắn gọn hơn cho những người đang tìm kiếm tài liệu tham khảo nhanh cũng như một số ví dụ sử dụng hứa hẹn và async / await.
Bắt đầu với cách tiếp cận ngây thơ (không hoạt động) cho một hàm gọi một phương thức không đồng bộ (trong trường hợp này setTimeout
) và trả về một thông báo:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
được ghi lại trong trường hợp này vì getMessage
trả về trước khi setTimeout
gọi lại và cập nhật outerScopeVar
.
Hai cách chính để giải quyết nó là sử dụng các lệnh gọi lại và lời hứa :
Gọi lại
Thay đổi ở đây là getMessage
chấp nhận một callback
tham số sẽ được gọi để cung cấp kết quả trở lại mã gọi khi có sẵn.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Lời hứa cung cấp một giải pháp thay thế linh hoạt hơn lệnh gọi lại vì chúng có thể được kết hợp một cách tự nhiên để điều phối nhiều hoạt động không đồng bộ. Một Promises / A + thực hiện tiêu chuẩn được quy định tại natively Node.js (0.12+) và nhiều trình duyệt hiện nay, mà còn được thực hiện trong các thư viện như Bluebird và Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Trì hoãn
jQuery cung cấp chức năng tương tự như những lời hứa với Deferreds của nó.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
async / await
Nếu môi trường JavaScript của bạn bao gồm hỗ trợ cho async
và await
(như Node.js 7.6+), thì bạn có thể sử dụng đồng bộ các hứa hẹn trong các async
hàm:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Để nói rõ ràng, cái cốc đại diện cho outerScopeVar
.
Các hàm không đồng bộ giống như ...
Các câu trả lời khác là tuyệt vời và tôi chỉ muốn cung cấp một câu trả lời thẳng cho vấn đề này. Chỉ giới hạn các cuộc gọi không đồng bộ jQuery
Tất cả các lệnh gọi ajax (bao gồm cả $.get
hoặc $.post
hoặc $.ajax
) là không đồng bộ.
Xem xét ví dụ của bạn
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
Quá trình thực thi mã bắt đầu từ dòng 1, khai báo biến và kích hoạt và cuộc gọi không đồng bộ trên dòng 2, (tức là yêu cầu đăng) và nó tiếp tục thực hiện từ dòng 3, mà không cần đợi yêu cầu đăng hoàn thành việc thực thi.
Giả sử yêu cầu đăng bài mất 10 giây để hoàn thành, giá trị của outerScopeVar
sẽ chỉ được đặt sau 10 giây đó.
Để thử,
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Bây giờ khi bạn thực hiện điều này, bạn sẽ nhận được một cảnh báo trên dòng 3. Bây giờ, hãy đợi một lúc cho đến khi bạn chắc chắn rằng yêu cầu bài đăng đã trả lại một số giá trị. Sau đó, khi bạn bấm OK, trên hộp cảnh báo, cảnh báo tiếp theo sẽ in ra giá trị mong đợi, vì bạn đã đợi nó.
Trong kịch bản cuộc sống thực, mã trở thành,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Tất cả mã phụ thuộc vào các lệnh gọi không đồng bộ, được di chuyển bên trong khối không đồng bộ hoặc bằng cách chờ các lệnh gọi không đồng bộ.
Trong tất cả các trường hợp outerScopeVar
này được sửa đổi hoặc gán một giá trị không đồng bộ hoặc xảy ra trong thời gian sau (chờ hoặc lắng nghe một số sự kiện xảy ra), mà quá trình thực thi hiện tại sẽ không chờ đợi . Vì vậy, tất cả các trường hợp này, luồng thực thi hiện tại dẫn đếnouterScopeVar = undefined
Hãy thảo luận về từng ví dụ (tôi đã đánh dấu phần được gọi là không đồng bộ hoặc bị trì hoãn đối với một số sự kiện xảy ra):
1.
Ở đây chúng ta đăng ký một eventlistner đó sẽ được thực hiện khi mà đặc biệt event.Here tải image.Then việc thực hiện liên tục với dòng tiếp theo img.src = 'lolcat.png';
và alert(outerScopeVar);
khi đó sự kiện này có thể không xảy ra. tức là, img.onload
chờ đợi cho hình ảnh được giới thiệu tải không đồng bộ. Điều này sẽ xảy ra tất cả các ví dụ sau đây - sự kiện có thể khác.
2.
Ở đây, sự kiện thời gian chờ đóng vai trò, sẽ gọi trình xử lý sau thời gian được chỉ định. Đây rồi 0
, nhưng nó vẫn đăng ký một sự kiện không đồng bộ, nó sẽ được thêm vào vị trí cuối cùng của quá trình Event Queue
thực thi, điều này làm cho độ trễ được đảm bảo.
3.
4.
Node có thể được coi là vua của mã hóa không đồng bộ, ở đây hàm được đánh dấu được đăng ký như một trình xử lý gọi lại sẽ được thực thi sau khi đọc tệp được chỉ định.
5.
Lời hứa hiển nhiên (điều gì đó sẽ được thực hiện trong tương lai) là không đồng bộ. xem Sự khác biệt giữa Trì hoãn, Hứa hẹn và Tương lai trong JavaScript là gì?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript