你的代碼應(yīng)該是這樣的:
function foo() { var httpRequest = new XMLHttpRequest(); httpRequest.open('GET', "/echo/json"); httpRequest.send(); return httpRequest.responseText; } var result = foo(); // Always ends up being 'undefined'
菲利克斯·克林做得很好工作為使用 jQuery for AJAX 的人編寫(xiě)答案,但我決定為不使用 jQuery 的人提供替代方案。
(注意,對(duì)于那些使用新的 fetch
API、Angular 或 Promise 的人,我添加了另一個(gè)答案如下)
這是另一個(gè)答案中“問(wèn)題的解釋”的簡(jiǎn)短摘要,如果您在閱讀后不確定,請(qǐng)閱讀該答案。
AJAX 中的 A 代表異步。這意味著發(fā)送請(qǐng)求(或者更確切地說(shuō)接收響應(yīng))被從正常執(zhí)行流程中刪除。在您的示例中, .send code>
立即返回,并且在調(diào)用您作為 success
回調(diào)傳遞的函數(shù)之前執(zhí)行下一條語(yǔ)句 return result;
。
這意味著當(dāng)您返回時(shí),您定義的偵聽(tīng)器尚未執(zhí)行,這意味著您返回的值尚未定義。
這是一個(gè)簡(jiǎn)單的類比:
function getFive(){ var a; setTimeout(function(){ a=5; },10); return a; }
由于 a=5
部分尚未執(zhí)行,因此返回的 a
值為 undefined
。 AJAX 的行為是這樣的,您在服務(wù)器有機(jī)會(huì)告訴您的瀏覽器該值是什么之前就返回了該值。
此問(wèn)題的一個(gè)可能的解決方案是重新主動(dòng)編寫(xiě)代碼,告訴您的程序在計(jì)算完成后要做什么。
function onComplete(a){ // When the code completes, do this alert(a); } function getFive(whenDone){ var a; setTimeout(function(){ a=5; whenDone(a); },10); }
這稱為CPS?;旧?,我們向 getFive
傳遞一個(gè)要在完成時(shí)執(zhí)行的操作,我們告訴我們的代碼如何在事件完成時(shí)做出反應(yīng)(例如我們的 AJAX 調(diào)用,或者在本例中是超時(shí))。
用法是:
getFive(onComplete);
屏幕上會(huì)提示“5”。 (小提琴)。
解決這個(gè)問(wèn)題基本上有兩種方法:
至于同步 AJAX,不要這樣做!Felix 的回答提出了一些令人信服的論點(diǎn),說(shuō)明為什么這是一個(gè)壞主意。總而言之,它會(huì)凍結(jié)用戶的瀏覽器,直到服務(wù)器返回響應(yīng)并造成非常糟糕的用戶體驗(yàn)。以下是來(lái)自 MDN 的另一個(gè)簡(jiǎn)短總結(jié),說(shuō)明原因:
如果您不得不這樣做,您可以傳遞一個(gè)標(biāo)志。 具體方法如下:
var request = new XMLHttpRequest(); request.open('GET', 'yourURL', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) {// That's HTTP for 'ok' console.log(request.responseText); }
讓您的函數(shù)接受回調(diào)。在示例代碼中,可以使 foo
接受回調(diào)。我們將告訴代碼當(dāng) foo
完成時(shí)如何反應(yīng)。
所以:
var result = foo(); // Code that depends on `result` goes here
變成:
foo(function(result) { // Code that depends on `result` });
這里我們傳遞了一個(gè)匿名函數(shù),但我們也可以輕松傳遞對(duì)現(xiàn)有函數(shù)的引用,使其看起來(lái)像:
function myHandler(result) { // Code that depends on `result` } foo(myHandler);
有關(guān)如何完成此類回調(diào)設(shè)計(jì)的更多詳細(xì)信息,請(qǐng)查看 Felix 的回答。
現(xiàn)在,讓我們定義 foo 本身以進(jìn)行相應(yīng)的操作
function foo(callback) { var httpRequest = new XMLHttpRequest(); httpRequest.onload = function(){ // When the request is loaded callback(httpRequest.responseText);// We're calling our method }; httpRequest.open('GET', "/echo/json"); httpRequest.send(); }
現(xiàn)在,我們已經(jīng)讓 foo 函數(shù)接受一個(gè)操作,以便在 AJAX 成功完成時(shí)運(yùn)行。我們可以通過(guò)檢查響應(yīng)狀態(tài)是否不是 200 并采取相應(yīng)措施(創(chuàng)建失敗處理程序等)來(lái)進(jìn)一步擴(kuò)展此功能。它有效地解決了我們的問(wèn)題。
如果您仍然很難理解這一點(diǎn),閱讀 AJAX 獲取在 MDN 上開(kāi)始指南。
Ajax 中的 A 代表 異步。這意味著發(fā)送請(qǐng)求(或者更確切地說(shuō)接收響應(yīng))被從正常執(zhí)行流程中刪除。在您的示例中,$.ajax
立即返回,并且下一條語(yǔ)句 return result;
在您作為 success
回調(diào)傳遞的函數(shù)之前執(zhí)行甚至打電話。
這是一個(gè)類比,希望可以使同步流和異步流之間的區(qū)別更加清晰:
想象一下,您給朋友打電話并請(qǐng)他為您查找一些信息。盡管可能需要一段時(shí)間,但您還是在電話旁等待,凝視著太空,直到您的朋友給您所需的答案。
當(dāng)您進(jìn)行包含“正?!贝a的函數(shù)調(diào)用時(shí),也會(huì)發(fā)生同樣的情況:
function findItem() { var item; while(item_not_found) { // search } return item; } var item = findItem(); // Do something with item doSomethingElse();
盡管 findItem
可能需要很長(zhǎng)時(shí)間才能執(zhí)行,但 var item = findItem();
之后的任何代碼都必須等待直到該函數(shù)返回結(jié)果。
您出于同樣的原因再次給您的朋友打電話。但這次你告訴他你很著急,他應(yīng)該用你的手機(jī)給你回電。你掛斷電話,離開(kāi)家,做你計(jì)劃做的事情。一旦您的朋友給您回電,您就正在處理他提供給您的信息。
這正是您發(fā)出 Ajax 請(qǐng)求時(shí)所發(fā)生的情況。
findItem(function(item) { // Do something with the item }); doSomethingElse();
不等待響應(yīng),而是立即繼續(xù)執(zhí)行,并執(zhí)行 Ajax 調(diào)用之后的語(yǔ)句。為了最終獲得響應(yīng),您需要提供一個(gè)在收到響應(yīng)后調(diào)用的函數(shù),即回調(diào)(注意到什么了嗎?回調(diào)?)。該調(diào)用之后的任何語(yǔ)句都會(huì)在調(diào)用回調(diào)之前執(zhí)行。
擁抱 JavaScript 的異步特性!雖然某些異步操作提供同步對(duì)應(yīng)項(xiàng)(“Ajax”也是如此),但通常不鼓勵(lì)使用它們,尤其是在瀏覽器上下文中。
你問(wèn)為什么不好?
JavaScript 在瀏覽器的 UI 線程中運(yùn)行,任何長(zhǎng)時(shí)間運(yùn)行的進(jìn)程都會(huì)鎖定 UI,使其無(wú)響應(yīng)。另外,JavaScript的執(zhí)行時(shí)間是有上限的,瀏覽器會(huì)詢問(wèn)用戶是否繼續(xù)執(zhí)行。
所有這些都會(huì)導(dǎo)致非常糟糕的用戶體驗(yàn)。用戶將無(wú)法判斷一切是否正常。此外,對(duì)于網(wǎng)速較慢的用戶,效果會(huì)更差。
下面我們將介紹三種不同的解決方案,它們都是相互構(gòu)建的:
async/await
的 Promise(ES2017+,如果您使用轉(zhuǎn)譯器或再生器,則可在舊版瀏覽器中使用)then() 的 Promise
(ES2015+,如果您使用眾多 Promise 庫(kù)之一,則可在舊版瀏覽器中使用)這三個(gè)功能均可在當(dāng)前瀏覽器和 Node 7+ 中使用。
async/await 進(jìn)行承諾
2017 年發(fā)布的 ECMAScript 版本引入了對(duì)異步函數(shù)的語(yǔ)法級(jí)支持。借助async
和await
,您可以以“同步風(fēng)格”編寫(xiě)異步。代碼仍然是異步的,但更容易閱讀/理解。
async/await
構(gòu)建在 Promise 之上:async
函數(shù)始終返回 Promise。 await
“解開(kāi)”一個(gè) Promise,并且要么產(chǎn)生 Promise 被解析的值,要么在 Promise 被拒絕時(shí)拋出錯(cuò)誤。
重要提示:您只能在 async
函數(shù)或 JavaScript 模塊。模塊外部不支持頂級(jí) await
,因此您可能必須創(chuàng)建異步 IIFE (立即調(diào)用函數(shù)表達(dá)式)來(lái)啟動(dòng)異步
上下文(如果不使用模塊)。
這是一個(gè)詳細(xì)說(shuō)明上面的延遲函數(shù)findItem()
的示例:
// 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 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); })();
當(dāng)前瀏覽器和node 版本支持 async/await
。您還可以借助 regenerator (或使用 regenerator 的工具)將代碼轉(zhuǎn)換為 ES5,以支持較舊的環(huán)境,例如 Babel)。
回調(diào)是指函數(shù) 1 傳遞給函數(shù) 2 時(shí)。函數(shù) 2 可以在函數(shù) 1 準(zhǔn)備好時(shí)調(diào)用它。在異步進(jìn)程的上下文中,只要異步進(jìn)程完成,就會(huì)調(diào)用回調(diào)。通常,結(jié)果會(huì)傳遞給回調(diào)。
在問(wèn)題的示例中,您可以使 foo
接受回調(diào)并將其用作 success
回調(diào)。所以這個(gè)
var result = foo(); // Code that depends on 'result'
變成了
foo(function(result) { // Code that depends on 'result' });
這里我們定義了“內(nèi)聯(lián)”函數(shù),但您可以傳遞任何函數(shù)引用:
function myCallback(result) { // Code that depends on 'result' } foo(myCallback);
foo
本身定義如下:
function foo(callback) { $.ajax({ // ... success: callback }); }
callback
將引用我們調(diào)用時(shí)傳遞給 foo
的函數(shù),并將其傳遞給 success
。 IE。一旦Ajax請(qǐng)求成功,$.ajax
將調(diào)用callback
并將響應(yīng)傳遞給回調(diào)(可以用result
引用,因?yàn)檫@就是我們定義回調(diào)的方式)。
您還可以在將響應(yīng)傳遞給回調(diào)之前對(duì)其進(jìn)行處理:
function foo(callback) { $.ajax({ // ... success: function(response) { // For example, filter the response callback(filtered_response); } }); }
使用回調(diào)編寫(xiě)代碼比看起來(lái)更容易。畢竟,瀏覽器中的 JavaScript 很大程度上是事件驅(qū)動(dòng)的(DOM 事件)。接收 Ajax 響應(yīng)只不過(guò)是一個(gè)事件。 當(dāng)您必須使用第三方代碼時(shí)可能會(huì)出現(xiàn)困難,但大多數(shù)問(wèn)題只需思考應(yīng)用程序流程就可以解決。
Promise API 是一個(gè)新的ECMAScript 6 (ES2015) 的功能,但它已經(jīng)具有良好的瀏覽器支持。還有許多庫(kù)實(shí)現(xiàn)了標(biāo)準(zhǔn) Promises API 并提供了其他方法來(lái)簡(jiǎn)化異步函數(shù)的使用和組合(例如,藍(lán)鳥(niǎo))。
Promise 是未來(lái)值的容器。當(dāng) Promise 收到值(已解決)或被取消(拒絕)時(shí),它會(huì)通知所有想要訪問(wèn)該值的“偵聽(tīng)器”。
與普通回調(diào)相比的優(yōu)點(diǎn)是它們?cè)试S您解耦代碼并且更容易編寫(xiě)。
這是使用 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);
});
}
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).
});
.as-console-wrapper { max-height: 100% !important; top: 0; }