首頁  >  問答  >  主體

我的變數在函數內修改後保持不變的原因 - 了解非同步程式碼

鑑於以下範例,為什麼 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 非同步性的規範問題。請隨意改進這個問題並添加社區可以認同的更多簡化範例。


P粉212114661P粉212114661345 天前518

全部回覆(2)我來回復

  • P粉930534280

    P粉9305342802023-10-14 16:59:42

    Fabrício 的回答非常正確;但我想用一些不太技術性的內容來補充他的答案,重點是透過類比來幫助解釋非同步性的概念


    類比...

    昨天,我正在做的工作需要從同事那裡得到一些資訊。我打了電話給他;談話是這樣進行的:

    說到這裡,我掛斷了電話。由於我需要鮑勃提供的資訊來完成我的報告,所以我留下了報告,去喝了杯咖啡,然後我看了一些電子郵件。 40 分鐘後(鮑伯很慢),鮑伯回電並給了我我需要的資訊。此時,我繼續處理我的報告,因為我已經獲得了所需的所有資訊。


    想像一下如果對話是這樣進行的;

    我坐在那裡等待。並等待著。並等待著。 40分鐘。除了等待什麼都不做。最終,鮑伯給了我訊息,我們掛斷了電話,我完成了我的報告。但我損失了 40 分鐘的工作效率。


    這是非同步與同步行為

    這正是我們問題中所有範例中發生的情況。載入圖片、從磁碟載入檔案以及透過 AJAX 請求頁面都是緩慢的操作(在現代計算的背景下)。

    JavaScript 允許您註冊一個回呼函數,該函數將在慢速操作完成時執行,而不是等待這些慢速操作完成。但同時,JavaScript 將繼續執行其他程式碼。事實上,JavaScript 在等待緩慢操作完成的同時執行其他程式碼,這使得該行為非同步。如果 JavaScript 在執行任何其他程式碼之前等待操作完成,這將是同步行為。

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

    在上面的程式碼中,我們要求 JavaScript 載入 lolcat.png,這是一個sloooow 操作。一旦這個緩慢的操作完成,回呼函數就會被執行,但同時,JavaScript 將繼續處理下一行程式碼;即alert(outerScopeVar)

    這就是為什麼我們看到警報顯示未定義;因為 alert() 是立即處理的,而不是在映像載入之後。

    為了修復我們的程式碼,我們要做的就是將 alert(outerScopeVar) 程式碼移到回呼函數中。因此,我們不再需要將 outerScopeVar 變數宣告為全域變數。

    var img = document.createElement('img');
    
    img.onload = function() {
        var localScopeVar = this.width;
        alert(localScopeVar);
    };
    
    img.src = 'lolcat.png';

    您將總是看到回呼被指定為函數,因為這是 JavaScript 中定義某些程式碼但稍後才執行它的唯一*方法。

    因此,在我們所有的範例中,function() { /* Do Something */ } 是回調;要修復所有範例,我們要做的就是將需要操作響應的程式碼移到那裡!

    * 從技術上講,您也可以使用eval(),但是eval() 為此目的是邪惡的


    如何讓來電者等待?

    您目前可能有一些與此類似的程式碼;

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

    但是,我們現在知道返回outerScopeVar會立即發生;在 onload 回呼函數更新變數之前。這會導致 getWidthOfImage() 傳回 undefined,並發出警報 undefined

    要解決此問題,我們需要允許呼叫 getWidthOfImage() 的函數註冊回調,然後將寬度警報移至該回調內;

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

    ...和先前一樣,請注意,我們已經能夠刪除全域變數(在本例中為 width)。

    回覆
    0
  • P粉178894235

    P粉1788942352023-10-14 10:02:51

    一個字回答:非同步性

    前言

    這個主題在 Stack Overflow 中已經重複了至少幾千次。因此,首先我想指出一些非常有用的資源:


    當前問題的答案

    讓我們先追蹤常見的行為。在所有範例中,outerScopeVar 均在函數內部進行修改。該函數顯然不會立即執行;它被分配或作為參數傳遞。這就是我們所說的回呼

    現在的問題是,什麼時候呼叫該回呼?

    這要看具體情況。讓我們試著再次追蹤一些常見行為:

    • img.onload 可能會在將來的某個時候呼叫(如果)圖像已成功載入。
    • setTimeout 可能會在延遲到期且超時尚未被 clearTimeout 取消後在將來的某個時間被呼叫。注意:即使使用 0 作為延遲,所有瀏覽器都有最小逾時延遲上限(HTML5 規格中指定為 4 毫秒)。
    • 當(而且如果)Ajax 請求已成功完成時,將來某個時候可能會呼叫 jQuery $.post 的回呼。
    • 當檔案已成功讀取或拋出錯誤時,Node.js 的 fs.readFile 可能會在將來的某個時候被呼叫。

    在所有情況下,我們都有一個可能在將來某個時候運行的回調。這個「將來的某個時候」就是我們所說的非同步流

    非同步執行被推出同步流程。也就是說,當同步程式碼堆疊正在執行時,非同步程式碼將永遠執行。這就是JavaScript單執行緒的意義。

    更具體地說,當JS 引擎空閒時——不執行一堆(a)同步程式碼——它將輪詢可能觸發非同步回調的事件(例如超時、收到網路回應)並執行它們又一個。這被視為事件循環

    也就是說,手繪紅色形狀中突出顯示的非同步程式碼只能在其各自程式碼區塊中的所有剩餘同步程式碼執行完畢後才執行:

    簡而言之,回呼函數是同步建立但非同步執行的。在知道非同步函數已執行之前,您不能依賴它的執行,如何做到這一點?

    這真的很簡單。依賴非同步函數執行的邏輯應該從該非同步函數內部啟動/呼叫。例如,將 alertconsole.log 移至回呼函數內將輸出預期結果,因為結果在此時可用。

    實作您自己的回呼邏輯

    通常,您需要對非同步函數的結果執行更多操作,或根據呼叫非同步函數的位置對結果執行不同的操作。讓我們來處理一個更複雜的例子:

    var outerScopeVar;
    helloCatAsync();
    alert(outerScopeVar);
    
    function helloCatAsync() {
        setTimeout(function() {
            outerScopeVar = 'Nya';
        }, Math.random() * 2000);
    }

    注意:我使用具有隨機延遲的 setTimeout 作為通用非同步函數;相同的範例適用於 Ajax、readFile、onload 和任何其他非同步流。

    這個範例顯然與其他範例有相同的問題;它不會等到非同步函數執行。

    讓我們透過實現我們自己的回調系統來解決這個問題。首先,我們擺脫了那個醜陋的 outerScopeVar ,它在這種情況下完全沒用。然後我們加入一個接受函數參數的參數,也就是我們的回呼。當非同步操作完成時,我們呼叫此回調並傳遞結果。實作(請依序閱讀註解):

    // 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);
    }

    上述範例的程式碼片段:

    // 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);
    }

    回覆
    0
  • 取消回覆