JavaScript:異步執行機制

使用JavaScript的開發者都知道,JS的異步執行機制在JS中佔據着重要的地位,主要就是體現在回調函數以及事件方面,最近看了很多文章,將自己的一些感受和理解跟各位分享一下。

前面的博客中也有提到,JavaScript是一個單線程執行機制的程序,這樣雖然說避免了併發訪問的問題,但是這樣也致使JS中的異步執行不能按照傳統的多線程方式執行異步,JS所有的異步的實現需要插到同一個隊列中,從而依次在主線中執行。一般來說,瀏覽器內存在主要的線程:JS執行引擎,HTTP線程和事件觸發線程,JS內部的所有邏輯都需要在JS執行引擎中執行。我們先看一個問題:

setTimeout(function (args) {
    console.log('1')
}, 1000);

setInterval(function (args) {
    console.log('2')
}, 1000);

看上面的代碼,我們知道timeout函數只會執行一次,而interval函數會執行多次,那麼,JS的定時器函數是如何在JS中調用和處理呢?

其實,JS中的異步任務執行機制跟setTimeout函數和setInterval函數執行方式很類似,在實際開發中,難免會遇到大量的發送給後端的請求,最常見的就是Ajax請求,當在JS主線程運行過程中,當遇到xhr.send()請求時,主線程會立即向http線程(PS:在瀏覽器內包含一些常駐線程,分別是:渲染引擎線程,JS引擎線程,定時觸發器線程,事件觸發線程,異步http請求線程)發送指令,命令http線程向後臺服務器發送請求,當JS主線程發送完這個指令後,這時的主線程不會等待這個Ajax任務的執行過程,而是繼續沿着主線程的任務隊列去執行下一個任務。等到http線程向服務器發送請求得到服務器的回覆後,就會回到回調函數的隊列中執行,當JS主線程中的所有同步任務執行完畢後,再來開始執行異步事件的回調。因爲回調函數在JS中的等級是最低的,所以會排到最後來執行,就類似於setTimeout函數和setInterval函數。

實際上,JS中的定時器setTimeout函數會在初始化之後就開始執行它自身的定時任務,等定時的時間達到後,若此時的JS引擎沒有被佔用,就會直接執行定時任務,反之定時任務會進入到等待隊列中等待時機執行。當進入JS的等待隊列後,待JS引擎沒有佔用的時候再不斷進行執行,藉此來說,就是當JS引擎被佔用時,則setTimeout會等待一段時間後再執行。

而setInterval函數,同樣需要加入到JS的等待隊列內,但是區別在於setInterval函數不會因進入等待隊列而停止計時,如果當前一個setInterval函數沒有執行的時候,又到了下一個setInterval函數,這時候,隊列中就會包含兩個setInterval函數的可能,如果一直累加,這樣就會導致線程嚴重阻塞,不管從哪個角度來看這個,都是不允許的,所以,瀏覽器的JS就做了一個優化處理,當隊列中存在setInterval函數時,這個setInterval函數不會進入到隊列中(但是如果這個setInterval函數已經出隊列並開始執行,還是會加入到隊列)。所以,我們可以得到一個結論,在JS單線程的執行機制中,使用setInterval在JS的異步執行機制中的執行頻率會比setTimeout更高,因爲setTimeout會在一個執行完成後再執行下一個定時的任務,而setInterval時持續執行定時。

所以,所謂的JS異步請求的執行方式就是JS的主線程跟其餘的輔助線程一起完成,當輔助線程執行完畢後JS的主線程任務隊列就會將已經完成的輔助線程的回調插入到JS主線程任務隊列的後面等待JS主線程的處理。

再來說下JS主要的幾種異步處理方式:callback,promise,generator,async/await。

方式一:callback回調函數

上面我們所介紹的setTimeout函數就是一個最經典的回調函數的例子,雖說這種方式解決了JS同步的問題,但是缺點很明顯,極易造成回調地獄現象(回調地獄是指在使用回調函數的時候層層嵌套,如果嵌套過多,會極大影響代碼可讀性和邏輯),而且不能使用try/catch捕獲錯誤,同時也不能使用return。

方式二:promise構造函數

Promise構造函數是ES6中提出來的一種異步解決方案,Promise解決了回調等解決方案嵌套的問題並且使代碼更加易讀,有種在寫同步方法的既視感,用於一個異步操作的最終完成(或最終失敗)及其結果的表示。就相當於如果成功了則如何如何,如果失敗了就如何如何,基本語法就是new Promise(參數),這裏的參數指的是一個函數,函數中又包含兩個參數:resolve和reject,其中resolve是成功回調函數,reject是失敗的回調函數,而且resolve和reject需要通過.then(resolve,reject)的方法來執行。

var Promise = new Promise(function (resolve, reject) {
    if (true) {
        resolve('成功啦');
    } else {
        reject('失敗啦');
    }
});
Promise.then(function (value) {
    console.log(value);
});
Promise.catch(function (reason) {
    console.log(reason);
});
Promise.finally(function () {
    console.log(3);
});

方式三:generator函數

generator函數也是ES6中新提出來的一種處理異步的函數,它有一個很大的特點,就是能夠讓函數在執行過程中暫停,基本語法如下:

function* generator() {
    yield '1';
    yield '2';
    return '3';
}

var g = generator();

g.next(); // {value: "1", done: false}
g.next(); // {value: "2", done: false}
g.next(); // {value: "3", done: true}

需要注意以下幾點:

  • 在使用generator函數時,只需要在聲明函數function後面加上*,同時配合函數內的yield關鍵字一起使用;
  • 在執行generator函數的時候,實際上是會返回一個iterator遍歷器對象,然後通過next()方法,將函數內的代碼根據yield關鍵字爲界分步執行;
  • generator函數執行中,函數本身不會執行,實際上是在調用iterator遍歷器對象中的next()方法,此時程序就會執行從前一個yield到下一個yield或者return之間的代碼,並且會將yield後面的值,包裝成json對象返回;
  • value取的yield或者return後面的值,否則就是undefined,done的值如果碰到return或者執行完成則返回true,否則返回false;

方式四:async/await

async/await方法就是萬精油方式,需要注意的是:

  • async函數返回的是一個Promise對象,所以async函數執行後可以繼續使用then等方式繼續執行後面的邏輯;
  • await函數後面一般跟隨的是Promsie對象,async函數執行時,遇到await,等待後面的Promise對象的狀態從pending變成resolve的後,將resolve的參數返回並自動執行到下一個await或者結束;
  • await函數後面也可以跟一個async函數進行嵌套;

 

關於JavaScript執行異步執行機制就說到這兒,這部分的相關知識很多,這裏也是簡單的介紹,有不足的地方希望大家指出來,相互交流,相互學習!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章