Tìm kiếm trang web

javascript - Tại sao biến của tôi không thay đổi sau khi tôi sửa nó bên trong một hàm? - Tham chiếu về mã bất đồng bộ


Dựa trên các ví dụ sau, tại sao outerScopeVar lại không xác định trong tất cả các 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);
// with observables
var outerScopeVar;
myObservable.subscribe(function (value) {
    outerScopeVar = value;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

Tại sao nó lại xuất ra undefined trong tất cả các ví dụ này? Tôi không muốn các cách khắc phục, tôi muốn biết tại sao điều này xảy ra.

Lưu ý: Đây là một câu hỏi chuẩn về tính không đồng bộ trong JavaScript. Hãy thoải mái cải thiện câu hỏi này và thêm nhiều ví dụ đơn giản hơn để cộng đồng có thể nhận biết.

Câu trả lời #1

Một từ trả lời: tính không đồng bộ.

Lời nói đầu

Chủ đề này đã được nhắc đi nhắc lại ít nhất vài nghìn lần ở đây trên Stack Overflow. Vì vậy, 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 câu hỏi "Làm thế nào để trả về phản hồi từ một cuộc gọi bất đồng bộ?". Xem câu trả lời xuất sắc của anh ấy giải thích về các luồng đồng bộ và bất đồng bộ, cũng như phần "Tái cấu trúc mã".
    @Benjamin Gruenbaum cũng đã dành nhiều công sức để giải thích về tính bất đồng bộ trong cùng 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ự bất đồng bộ rất tốt một cách đơn giản.

Câu trả lời cho câu hỏi đang được bàn

Hãy theo dõi hành vi phổ biến trước. Trong tất cả các ví dụ, 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à điều mà chúng ta gọi là callback.

Bây giờ câu hỏi là, khi nào thì callback đó được gọi?

Điều đó phụ thuộc vào từng trường hợp. Hãy thử theo dõi một số hành vi phổ biến một lần nữa:

  • img.onload có thể được gọi vào một thời điểm nào đó trong tương lai khi (và nếu) hình ảnh đã được tải thành công.
  • setTimeout có thể được gọi vào một lúc nào đó trong tương lai sau khi khoảng thời gian trì hoãn đã hết và timeout chưa bị hủy bởi clearTimeout. Lưu ý: ngay cả khi sử dụng 0 làm thời gian trì hoãn, tất cả các trình duyệt đều có giới hạn thời gian trì hoãn tối thiểu (theo quy định là 4ms trong đặc tả HTML5).
  • Hàm callback của jQuery $.post 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.
  • fs.readFile của Node.js 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 đã xảy ra lỗi.

Trong mọi trường hợp, chúng tôi có một hàm gọi lại có thể chạy vào một lúc nào đó trong tương lai. "Vào một lúc nào đó trong tương lai" này chính là những gì chúng tôi gọi là dòng chảy không đồng bộ.

Thực thi không đồng bộ được tách 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 việc JavaScript là đơn luồng.

Cụ thể hơn, khi engine JS ở trạng thái nhàn rỗi -- không thực thi một ngăn xếp mã (a)synchronous -- nó sẽ kiểm tra các sự kiện có thể đã kích hoạt các callback bất đồng bộ (ví dụ: hết thời gian chờ, nhận phản hồi mạng) và thực thi chúng lần lượt. Điều này được coi là Vòng lặp Sự kiện.

Điều đó có nghĩa là, mã bất đồng bộ được đánh dấu bằng các hình đỏ vẽ tay có thể chỉ thực thi sau khi tất cả mã đồng bộ còn lại trong các khối mã tương ứng của chúng đã được thực thi:

Tóm lại, các hàm callback được tạo ra một cách đồng bộ nhưng được thực thi một cách bất đồng bộ. Bạn không thể dựa vào việc thực thi của một hàm bất đồng bộ cho đến khi bạn biết nó đã được thực thi, và làm thế nào để làm điều đó?

Nó thực sự đơn giản. Logic mà phụ thuộc vào việc thực thi hàm bất đồng bộ nên được khởi động/gọi từ bên trong chính hàm bất đồng bộ này. Ví dụ, di chuyển các alert và console.log vào bên trong hàm callback sẽ xuất ra kết quả như mong đợi vì kết quả đã có sẵn tại thời điểm đó.

Triển khai logic callback 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 bất đồng bộ hoặc làm những việc khác nhau với kết quả tùy thuộc vào nơi mà hàm bất đồng bộ được gọi. Hãy cùng tìm hiểu 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 độ trễ ngẫu nhiên như một hàm bất đồng bộ chung; ví dụ tương tự cũng áp dụng cho Ajax, readFile, onload, và bất kỳ luồng bất đồng bộ nào khác.

Ví dụ này rõ ràng gặp phải cùng một vấn đề như các ví dụ khác; nó không chờ cho đến khi hàm bất đồng bộ thực thi.

Hãy giải quyết nó bằng cách triển khai hệ thống callback của riêng chúng ta. Trước hết, chúng ta loại bỏ outerScopeVar xấu xí đó, thứ hoàn toàn vô dụng trong trường hợp này. Sau đó, chúng ta thêm một tham số nhận một đối số là hàm, callback của chúng ta. Khi thao tác bất đồng bộ kết thúc, chúng ta gọi callback này, truyền kết quả vào. Cách triển khai (vui lòng đọc các chú thích 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 an 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 an 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 an 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);
}

Trong hầu hết các trường hợp sử dụng thực tế, API DOM và hầu hết các thư viện đã cung cấp chức năng callback (cách triển khai helloCatAsync trong ví dụ minh họa này). Bạn chỉ cần truyền hàm callback 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 bất đồng bộ, không thể return một giá trị từ một luồng bất đồng bộ trở lại luồng đồng bộ nơi callback được định nghĩa, vì các callback bất đồng bộ được thực thi sau khi mã đồng bộ đã hoàn thành việc thực thi từ lâu.

Thay vì return một giá trị từ một callback bất đồng bộ, bạn sẽ phải sử dụng mẫu callback, hoặc... Promises.

Những lời hứa

Mặc dù có những cách để tránh tình trạng callback hell với vanilla JS, nhưng promises đang ngày càng trở nên phổ biến và hiện đang được chuẩn hóa trong ES6 (xem Promise - MDN).

Promises (còn gọi là Futures) cung cấp một cách đọc tuần tự hơn, và do đó dễ chịu hơn, của mã bất đồng bộ, 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 nguồn tài liệu tuyệt vời này cho những ai quan tâm:

  • JavaScript Promises - HTML5 Rocks
  • Bạn Đang Hiểu Sai Về Ý Nghĩa Của Lời Hứa - domenic.me

Nhiều tài liệu đọc thêm về bất đồng bộ trong JavaScript

  • Cuốn sách The Art of Node - Callbacks giải thích rất rõ ràng về mã bất đồng bộ và callbacks với các ví dụ vanilla JS cũng như mã Node.js.

Lưu ý: Tôi đã đánh dấu câu trả lời này là Cộng đồng Wiki. Do đó, bất kỳ ai có ít nhất 100 điểm uy tín đều có thể chỉnh sửa và cải thiện nó! Hãy thoải mái 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 biến câu hỏi này thành một chủ đề chuẩn để trả lời các vấn đề bất đồng bộ không liên quan đến Ajax (đã có câu hỏi How to return the response from an AJAX call? cho việc đó), do đó chủ đề này cần sự giúp đỡ của bạn để trở nên tốt và hữu ích nhất có thể!

Câu trả lời #2

Câu trả lời của Fabrício rất chính xác; nhưng tôi muốn bổ sung vào câu trả lời của anh ấy bằng một điều gì đó ít kỹ thuật hơn, tập trung vào một phép ẩn dụ để giúp giải thích khái niệm bất đồng bộ.

Một phép ẩn dụ...

Hôm qua, công việc tôi đang làm cần một số thông tin từ một đồng nghiệp. Tôi đã gọi cho anh ấy; đây là cách cuộc trò chuyện diễn ra:

Mình: Chào Bob, mình cần biết cách chúng ta foo bar tuần trước. Jim muốn một báo cáo về nó, và chỉ có bạn là người biết chi tiết về việc này.

Bob: Chắc chắn rồi, nhưng sẽ mất khoảng 30 phút của tôi?

Tôi: Thật tuyệt, Bob. Gọi lại cho tôi khi bạn có thông tin nhé!

Vào lúc này, tôi đã cúp điện thoại. Vì tôi cần thông tin từ Bob để hoàn thành báo cáo của mình, tôi bỏ báo cáo sang một bên và đi uống cà phê, sau đó tôi kiểm tra email một số. 40 phút sau (Bob chậm), Bob gọi lại và cung cấp cho tôi thông tin mà tôi cần. Vào lúc này, tôi tiếp tục làm việc với báo cáo của mình, vì tôi đã có tất cả cá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 thay vào đó;

Mình: Chào Bob, mình cần biết tuần trước chúng ta foo như thế nào với bar. Jim muốn có một báo cáo về việc này, 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 sẽ mất khoảng 30 phút của tôi?

Tôi: Tuyệt quá Bob. Tôi sẽ chờ.

Và tôi ngồi đó và chờ. Và chờ. Và chờ. Suốt 40 phút. Không làm gì ngoài việc chờ. Cuối cùng, Bob cung cấp thông tin cho tôi, chúng tôi ngắt 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 đồ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 ta. Tải một hình ảnh, tải một tệp từ đĩa và yêu cầu một trang qua AJAX đều là những thao tác chậm (trong bối cảnh tính toán hiện đại).

Thay vì chờ đợi các thao tác chậm này hoàn tất, JavaScript cho phép bạn đăng ký một hàm callback sẽ được thực thi khi thao tác chậm đã hoàn tất. Trong thời gian chờ đợi, JavaScript vẫn tiếp tục thực thi các đoạn mã khác. Thực tế rằng JavaScript thực thi các đoạn mã khác trong khi chờ thao tác chậm hoàn tất làm cho hành vi này trở thành một hành vi bất đồng bộ. Nếu JavaScript chờ đợi hoàn tất thao tác trước khi thực thi bất kỳ đoạn 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 ta đang yêu cầu JavaScript tải lolcat.png, đây là một thao tác rất chậm. Hàm callback sẽ được thực thi khi thao tác chậm này hoàn tất, nhưng trong lúc đó, 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 ta thấy cảnh báo hiển thị undefined; vì alert() được xử lý ngay lập tức, thay vì sau khi hình ảnh đã được tải xong.

Để sửa mã của chúng ta, tất cả những gì chúng ta phải làm là di chuyển đoạn mã alert(outerScopeVar) vào trong hàm callback. Kết quả là, chúng ta không còn cần biến outerScopeVar được khai báo dưới dạng 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 luôn thấy một callback được chỉ định là một hàm, bởi vì đó là cách duy nhất* trong JavaScript để định nghĩa một đoạn mã, nhưng không thực thi nó cho tới sau này.

Do đó, trong tất cả các ví dụ của chúng tôi, function() { /* Làm gì đó */ } là callback; để sửa tất cả các ví dụ, tất cả những gì chúng ta cần làm là chuyển mã cần phản hồi của thao tác vào đó!

* Về mặt kỹ thuật, bạn cũng có thể sử dụng eval(), nhưng eval() là xấu cho mục đích này

Làm thế nào để tôi làm cho người gọi phải chờ?

Bạn có thể hiện tại có một số mã giống 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, giờ chúng ta biết rằng return outerScopeVar xảy ra ngay lập tức; trước khi hàm callback onload cập nhật biến. Điều này dẫn đến getWidthOfImage() trả về undefined, và undefined được cảnh báo.

Để khắc phục điều này, chúng ta cần cho phép chức năng gọi getWidthOfImage() đăng ký một hàm callback, sau đó di chuyển việc cảnh báo chiều rộng vào bên trong hàm callback đó;

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, lưu ý rằng chúng tôi đã có thể loại bỏ các biến toàn cục (trong trường hợp này là width).

Câu trả lời #3

Đây là câu trả lời ngắn gọn hơn cho những người đang tìm kiếm một tài liệu tham khảo nhanh cũng như một số ví dụ sử dụng promises 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 bất đồng bộ (trong trường hợp này là setTimeout) và trả về một thông điệp:

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 bởi vì getMessage trả về trước khi callback của setTimeout được gọi và cập nhật outerScopeVar.

Hai cách chính để giải quyết điều này là sử dụng callbacks và promises:

Hàm gọi lại

Sự thay đổi ở đây là getMessage chấp nhận một tham số callback sẽ được gọi để trả kết quả về cho 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

Promises cung cấp một lựa chọn thay thế linh hoạt hơn so với callbacks vì chúng có thể được kết hợp một cách tự nhiên để phối hợp nhiều hoạt động bất đồng bộ. Một triển khai theo chuẩn Promises/A+ được cung cấp nguyên bản trong node.js (0.12+) và nhiều trình duyệt hiện nay, nhưng cũng được triển khai 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 Deferreds

jQuery cung cấp chức năng tương tự như promises với Deferred 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 hỗ trợ async và await (như Node.js 7.6+), thì bạn có thể sử dụng promises một cách đồng bộ bên trong các hàm async:

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();

Câu trả lời #4

Nói rõ ràng thì, chiếc cốc đại diện cho outerScopeVar.

Các hàm bất đồng bộ kiểu như...

Câu trả lời số 5

Những câu trả lời khác đều rất xuất sắc và tôi chỉ muốn cung cấp một câu trả lời thẳng thắn cho điều này. Chỉ giới hạn ở các cuộc gọi bất đồng bộ jQuery

Tất cả các cuộc gọi ajax (bao gồm $.get hoặc $.post hoặc $.ajax) đều 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

Việc thực thi mã bắt đầu từ dòng 1, khai báo biến và kích hoạt một cuộc gọi bất đồng bộ ở dòng 2 (tức là yêu cầu POST) và nó tiếp tục thực thi từ dòng 3 mà không chờ yêu cầu POST hoàn thành việc thực thi của nó.

Giả sử rằng yêu cầu POST mất 10 giây để hoàn thành, giá trị của outerScopeVar sẽ chỉ được thiết lập 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 thi điều này, bạn sẽ nhận được một thông báo ở dòng 3. Bây giờ hãy đợi một thời gian cho đến khi bạn chắc chắn rằng yêu cầu post đã trả về một giá trị nào đó. Sau đó, khi bạn nhấn OK trên hộp thông báo, thông báo tiếp theo sẽ in ra giá trị mong muốn, vì bạn đã đợi nó.

Trong tình huống thực tế, 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 cuộc gọi bất đồng bộ được di chuyển vào bên trong khối bất đồng bộ, hoặc bằng cách chờ các cuộc gọi bất đồng bộ.

Câu trả lời số 6

Trong tất cả các kịch bản này, outerScopeVar được sửa đổi hoặc gán giá trị một cách không đồng bộ hoặc xảy ra sau một thời gian (chờ hoặc lắng nghe một sự kiện nào đó xảy ra), mà luồng thực thi hiện tại sẽ không chờ. Do đó, trong tất cả các trường hợp này, luồng thực thi hiện tại sẽ dẫn đến outerScopeVar=undefined

Hãy cùng thảo luận từng ví dụ (Tôi đã đánh dấu phần được gọi là bất đồng bộ hoặc trì hoãn để một số sự kiện xảy ra):

1.

Ở đây chúng ta đăng ký một eventlistener sẽ được thực thi khi sự kiện cụ thể đó xảy ra. Ở đây là tải hình ảnh. Sau đó, quá trình thực thi hiện tại tiếp tục với các dòng tiếp theo img.src='lolcat.png'; và alert(outerScopeVar); trong khi sự kiện có thể chưa xảy ra. Tức là, hàm img.onload chờ hình ảnh được tham chiếu tải xong, một cách không đồng bộ. Điều này sẽ xảy ra với tất cả các ví dụ tiếp theo - sự kiện có thể khác nhau.

2.

Ở đây sự kiện timeout đóng vai trò, sẽ gọi bộ xử lý sau thời gian đã định. Ở đây là 0, nhưng vẫn đăng ký một sự kiện bất đồng bộ, nó sẽ được thêm vào vị trí cuối cùng của Event Queue để thực thi, điều này đảm bảo độ trễ.

3.

4.

Node có thể được xem như một dạng lập trình bất đồng bộ. Ở đây, hàm được đánh dấu được đăng ký làm bộ xử lý callback, sẽ được thực thi sau khi đọc xong tệp được chỉ định.

5.

Lời hứa rõ ràng (một việc gì đó sẽ được thực hiện trong tương lai) là bất đồng bộ. xem Những khác biệt giữa Deferred, Promise và Future trong JavaScript là gì?

https://www.quora.com/Điểm-khác-nhau-giữa-promise-và-callback-trong-Javascript

Bài viết liên quan