1. 程式人生 > 其它 >javascript的非同步程式設計

javascript的非同步程式設計

概念

非同步(Asynchronous, async)是與同步(Synchronous, sync)相對的概念。

在我們學習的傳統單執行緒程式設計中,程式的執行是同步的(同步不意味著所有步驟同時執行,而是指步驟在一個控制流序列中按順序執行)。而非同步的概念則是不保證同步的概念,也就是說,一個非同步過程的執行將不再與原有的序列有順序關係。

簡單來理解就是:同步按你的程式碼順序執行,非同步不按照程式碼順序執行,非同步的執行效率更高。

以上是關於非同步的概念的解釋,接下來我們通俗地解釋一下非同步:非同步就是從主執行緒發射一個子執行緒來完成任務。

什麼時候用非同步程式設計

在前端程式設計中(甚至後端有時也是這樣),我們在處理一些簡短、快速的操作時,例如計算 1 + 1 的結果,往往在主執行緒中就可以完成。主執行緒作為一個執行緒,不能夠同時接受多方面的請求。所以,當一個事件沒有結束時,介面將無法處理其他請求。

現在有一個按鈕,如果我們設定它的 onclick 事件為一個死迴圈,那麼當這個按鈕按下,整個網頁將失去響應。

為了避免這種情況的發生,我們常常用子執行緒來完成一些可能消耗時間足夠長以至於被使用者察覺的事情,比如讀取一個大檔案或者發出一個網路請求。因為子執行緒獨立於主執行緒,所以即使出現阻塞也不會影響主執行緒的執行。但是子執行緒有一個侷限:一旦發射了以後就會與主執行緒失去同步,我們無法確定它的結束,如果結束之後需要處理一些事情,比如處理來自伺服器的資訊,我們是無法將它合併到主執行緒中去的。

為了解決這個問題,JavaScript 中的非同步操作函式往往通過回撥函式來實現非同步任務的結果處理。

回撥函式

回撥函式就是一個函式,它是在我們啟動一個非同步任務的時候就告訴它:等你完成了這個任務之後要幹什麼。這樣一來主執行緒幾乎不用關心非同步任務的狀態了,他自己會善始善終。


function print() {
    document.getElementById("demo").innerHTML="RUNOOB!";
}
setTimeout(print, 3000);

這段程式中的 setTimeout 就是一個消耗時間較長(3 秒)的過程,它的第一個引數是個回撥函式,第二個引數是毫秒數,這個函式執行之後會產生一個子執行緒,子執行緒會等待 3 秒,然後執行回撥函式 "print",在命令列輸出 "RUNOOB!"。

當然,JavaScript 語法十分友好,我們不必單獨定義一個函式 print ,我們常常將上面的程式寫成:


setTimeout(function () {
    document.getElementById("demo").innerHTML="RUNOOB!";
}, 3000);

這段程式的執行結果是:

RUNOOB-1!
RUNOOB-2!

非同步 AJAX

除了 setTimeout 函式以外,非同步回撥廣泛應用於 AJAX 程式設計。

XMLHttpRequest 常常用於請求來自遠端伺服器上的 XML 或 JSON 資料。一個標準的 XMLHttpRequest 物件往往包含多個回撥:


var xhr = new XMLHttpRequest();
 
xhr.onload = function () {
    // 輸出接收到的文字資料
    document.getElementById("demo").innerHTML=xhr.responseText;
}
 
xhr.onerror = function () {
    document.getElementById("demo").innerHTML="請求出錯";
}
 
// 傳送非同步 GET 請求
xhr.open("GET", "https://www.runoob.com/try/ajax/ajax_info.txt", true);
xhr.send();

XMLHttpRequest 的 onload 和 onerror 屬性都是函式,分別在它請求成功和請求失敗時被呼叫。如果你使用完整的 jQuery 庫,也可以更加優雅的使用非同步 AJAX:

$.get("https://www.runoob.com/try/ajax/demo_test.php",function(data,status){
    alert("資料: " + data + "\n狀態: " + status);
});

JavaScript Promise

Promise 是一個 ECMAScript 6 提供的類,目的是更加優雅地書寫複雜的非同步任務。

構造 Promise

現在我們新建一個 Promise 物件:

new Promise(function (resolve, reject) {
    // 要做的事情...
});

Promise的使用

下面我們通過剖析這段 Promise "計時器" 程式碼來講述 Promise 的使用: Promise 建構函式只有一個引數,是一個函式,這個函式在構造之後會直接被非同步執行,所以我們稱之為起始函式。起始函式包含兩個引數 resolve 和 reject。 當 Promise 被構造時,起始函式會被非同步執行:

new Promise(function (resolve, reject) {
    console.log("Run");
});

這段程式會直接輸出 Run。

resolve 和 reject 都是函式,其中呼叫 resolve 代表一切正常,reject 是出現異常時所呼叫的:
Promise 類有 .then() .catch() 和 .finally() 三個方法,這三個方法的引數都是一個函式,.then() 可以將引數中的函式新增到當前 Promise 的正常執行序列,.catch() 則是設定 Promise 的異常處理序列,.finally() 是在 Promise 執行的最後一定會執行的序列。 .then() 傳入的函式會按順序依次執行,有任何異常都會直接跳到 catch 序列:

new Promise(function (resolve, reject) {
    console.log(1111);
    resolve(2222);
}).then(function (value) {
    console.log(value);
    return 3333;
}).then(function (value) {
    console.log(value);
    throw "An error";
}).catch(function (err) {
    console.log(err);
});

執行結果

1111
2222
3333
An error

resolve() 中可以放置一個引數用於向下一個 then 傳遞一個值,then 中的函式也可以返回一個值傳遞給 then。但是,如果 then 中返回的是一個 Promise 物件,那麼下一個 then 將相當於對這個返回的 Promise 進行操作,這一點從剛才的計時器的例子中可以看出來。

reject() 引數中一般會傳遞一個異常給之後的 catch 函式用於處理異常。

但是請注意以下兩點:

  1. resolve 和 reject 的作用域只有起始函式,不包括 then 以及其他序列;
  2. resolve 和 reject 並不能夠使起始函式停止執行,別忘了 return。

Promise 函式

計時器程式

通過新建一個 Promise 物件好像並沒有看出它怎樣 "更加優雅地書寫複雜的非同步任務"。我們之前遇到的非同步任務都是一次非同步,如果需要多次呼叫非同步函式呢?例如,如果我想分三次輸出字串,第一次間隔 1 秒,第二次間隔 4 秒,第三次間隔 3 秒:

程式1
setTimeout(function () {
    console.log("First");
    setTimeout(function () {
        console.log("Second");
        setTimeout(function () {
            console.log("Third");
        }, 3000);
    }, 4000);
}, 1000);

這段程式實現了這個功能,但是它是用 "函式瀑布" 來實現的。可想而知,在一個複雜的程式當中,用 "函式瀑布" 實現的程式無論是維護還是異常處理都是一件特別繁瑣的事情,而且會讓縮排格式變得非常冗贅。

現在我們用 Promise 來實現同樣的功能:

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

"計時器" 程式看上去比函式瀑布還要長,所以我們可以將它的核心部分寫成一個 Promise 函式:

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

然後我們就可以放心大膽的實現程式功能了:

print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});
then、catch 和 finally 序列能否順序顛倒?

答:可以,效果完全一樣。但不建議這樣做,最好按 then-catch-finally 的順序編寫程式。

除了 then 塊以外,其它兩種塊能否多次使用?

可以,finally 與 then 一樣會按順序執行,但是 catch 塊只會執行第一個,除非 catch 塊裡有異常。所以最好只安排一個 catch 和 finally 塊。

then 塊如何中斷?

then 塊預設會向下順序執行,return 是不能中斷的,可以通過 throw 來跳轉至 catch 實現中斷.

什麼時候適合用 Promise 而不是傳統回撥函式?

當需要多次順序執行非同步操作的時候,例如,如果想通過非同步方法先後檢測使用者名稱和密碼,需要先非同步檢測使用者名稱,然後再非同步檢測密碼的情況下就很適合 Promise

Promise 是一種將非同步轉換為同步的方法嗎?

完全不是。Promise 只不過是一種更良好的程式設計風格。

什麼時候我們需要再寫一個 then 而不是在當前的 then 接著程式設計?

當你又需要呼叫一個非同步任務的時候。

非同步函式

非同步函式(async function)是 ECMAScript 2017 (ECMA-262) 標準的規範,幾乎被所有瀏覽器所支援,除了 Internet Explorer。

在 Promise 中我們編寫過一個 Promise 函式:

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

然後用不同的時間間隔輸出了三行文字:

print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});

我們可以將這段程式碼變得更好看:

async function asyncFunc() {
    await print(1000, "First");
    await print(4000, "Second");
    await print(3000, "Third");
}
asyncFunc();

哈!這豈不是將非同步操作變得像同步操作一樣容易了嗎!

這次的回答是肯定的,非同步函式 async function 中可以使用 await 指令,await 指令後必須跟著一個 Promise,非同步函式會在這個 Promise 執行中暫停,直到其執行結束再繼續執行。

非同步函式實際上原理與 Promise 原生 API 的機制是一模一樣的,只不過更便於程式設計師閱讀。

處理異常的機制將用 try-catch 塊實現:

async function asyncFunc() {
    try {
        await new Promise(function (resolve, reject) {
            throw "Some error"; // 或者 reject("Some error")
        });
    } catch (err) {
        console.log(err);
        // 會輸出 Some error
    }
}
asyncFunc();

如果 Promise 有一個正常的返回值,await 語句也會返回它:

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
}
asyncFunc();

程式會輸出:

Return value