鑑於以下範例,為什麼 outerScopeVar
在所有情況下都未定義?
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);
為什麼在所有這些範例中都輸出 undefined
?我不需要解決方法,我想知道為什麼會發生這種情況。
注意:這是一個關於 JavaScript 非同步性的規範問題。請隨意改進這個問題並添加社區可以認同的更多簡化範例。
Fabrício 的回答非常正確;但我想用一些不太技術性的內容來補充他的答案,重點是透過類比來幫助解釋非同步性的概念。
類比...
昨天,我正在做的工作需要從同事那裡得到一些資訊。我打了電話給他;談話是這樣進行的:
說到這裡,我掛斷了電話。由於我需要鮑勃提供的資訊來完成我的報告,所以我留下了報告,去喝了杯咖啡,然後我看了一些電子郵件。 40 分鐘後(鮑伯很慢),鮑伯回電並給了我我需要的資訊。此時,我繼續處理我的報告,因為我已經獲得了所需的所有資訊。
想像一下如果對話是這樣進行的;
我坐在那裡等待。並等待著。並等待著。 40分鐘。除了等待什麼都不做。最終,鮑伯給了我訊息,我們掛斷了電話,我完成了我的報告。但我損失了 40 分鐘的工作效率。
這是非同步與同步行為
這正是我們問題中所有範例中發生的情況。載入圖片、從磁碟載入檔案以及透過 AJAX 請求頁面都是緩慢的操作(在現代計算的背景下)。
JavaScript 允許您註冊一個回呼函數,該函數將在慢速操作完成時執行,而不是等待這些慢速操作完成。但同時,JavaScript 將繼續執行其他程式碼。事實上,JavaScript 在等待緩慢操作完成的同時執行其他程式碼,這使得該行為非同步。如果 JavaScript 在執行任何其他程式碼之前等待操作完成,這將是同步行為。
在上面的程式碼中,我們要求 JavaScript 載入
lolcat.png
,這是一個sloooow 操作。一旦這個緩慢的操作完成,回呼函數就會被執行,但同時,JavaScript 將繼續處理下一行程式碼;即alert(outerScopeVar)
。這就是為什麼我們看到警報顯示
未定義
;因為alert()
是立即處理的,而不是在映像載入之後。為了修復我們的程式碼,我們要做的就是將
alert(outerScopeVar)
程式碼移到回呼函數中。因此,我們不再需要將outerScopeVar
變數宣告為全域變數。您將總是看到回呼被指定為函數,因為這是 JavaScript 中定義某些程式碼但稍後才執行它的唯一*方法。
因此,在我們所有的範例中,
function() { /* Do Something */ }
是回調;要修復所有範例,我們要做的就是將需要操作響應的程式碼移到那裡!* 從技術上講,您也可以使用
eval()
,但是eval()
為此目的是邪惡的如何讓來電者等待?
您目前可能有一些與此類似的程式碼;
但是,我們現在知道
返回outerScopeVar
會立即發生;在onload
回呼函數更新變數之前。這會導致getWidthOfImage()
傳回undefined
,並發出警報undefined
。要解決此問題,我們需要允許呼叫
getWidthOfImage()
的函數註冊回調,然後將寬度警報移至該回調內;...和先前一樣,請注意,我們已經能夠刪除全域變數(在本例中為
width
)。一個字回答:非同步性。
前言
這個主題在 Stack Overflow 中已經重複了至少幾千次。因此,首先我想指出一些非常有用的資源:
@Felix Kling 對「如何從非同步呼叫回傳回應?」的回答。請參閱他解釋同步和非同步流程的出色答案,以及「重組程式碼」部分。
@Benjamin Gruenbaum 也投入了大量精力來解釋同一線程中的非同步性。
@Matt Esch 對「從 fs.readFile 取得資料」的答案也很好地解釋了非同步性簡單的方式。
當前問題的答案
讓我們先追蹤常見的行為。在所有範例中,
outerScopeVar
均在函數內部進行修改。該函數顯然不會立即執行;它被分配或作為參數傳遞。這就是我們所說的回呼。現在的問題是,什麼時候呼叫該回呼?
這要看具體情況。讓我們試著再次追蹤一些常見行為:
img.onload
可能會在將來的某個時候呼叫(如果)圖像已成功載入。setTimeout
可能會在延遲到期且超時尚未被clearTimeout
取消後在將來的某個時間被呼叫。注意:即使使用0
作為延遲,所有瀏覽器都有最小逾時延遲上限(HTML5 規格中指定為 4 毫秒)。$.post
的回呼。fs.readFile
可能會在將來的某個時候被呼叫。在所有情況下,我們都有一個可能在將來某個時候運行的回調。這個「將來的某個時候」就是我們所說的非同步流。
非同步執行被推出同步流程。也就是說,當同步程式碼堆疊正在執行時,非同步程式碼將永遠執行。這就是JavaScript單執行緒的意義。
更具體地說,當JS 引擎空閒時——不執行一堆(a)同步程式碼——它將輪詢可能觸發非同步回調的事件(例如超時、收到網路回應)並執行它們又一個。這被視為事件循環。
也就是說,手繪紅色形狀中突出顯示的非同步程式碼只能在其各自程式碼區塊中的所有剩餘同步程式碼執行完畢後才執行:
簡而言之,回呼函數是同步建立但非同步執行的。在知道非同步函數已執行之前,您不能依賴它的執行,如何做到這一點?
這真的很簡單。依賴非同步函數執行的邏輯應該從該非同步函數內部啟動/呼叫。例如,將
alert
和console.log
移至回呼函數內將輸出預期結果,因為結果在此時可用。實作您自己的回呼邏輯
通常,您需要對非同步函數的結果執行更多操作,或根據呼叫非同步函數的位置對結果執行不同的操作。讓我們來處理一個更複雜的例子:
注意:我使用具有隨機延遲的
setTimeout
作為通用非同步函數;相同的範例適用於 Ajax、readFile、onload 和任何其他非同步流。這個範例顯然與其他範例有相同的問題;它不會等到非同步函數執行。
讓我們透過實現我們自己的回調系統來解決這個問題。首先,我們擺脫了那個醜陋的
outerScopeVar
,它在這種情況下完全沒用。然後我們加入一個接受函數參數的參數,也就是我們的回呼。當非同步操作完成時,我們呼叫此回調並傳遞結果。實作(請依序閱讀註解):上述範例的程式碼片段: