JavaScript異步編程 | 掘金技術徵文

還記得一年前寫過一篇關於JavaScript異步編程簡述的文章,主要介紹了JavaScript的單線程特性與異步編程實現方式:
回調函數,發佈訂閱模式,Promise對象三種,關於Promise介紹的比較簡略,決定再詳細總結一下,既是對上一篇文章的補充,也能以更深入的方式分享本身關於異步編程的理解。javascript

前言

若是你有志於成爲一個優秀的前端工程師,或是想要深刻學習JavaScript,異步編程是必不可少的一個知識點,這也是區分初級,中級或高級前端的依據之一。若是你對異步編程沒有太清晰的概念,那麼我建議你花點時間學習JavaScript異步編程,若是你對異步編程有本身的獨特理解,也歡迎閱讀本文,一塊兒交流。前端

同步與異步

介紹異步以前,回顧一下,所謂同步編程,就是計算機一行一行按順序依次執行代碼,當前代碼任務耗時執行會阻塞後續代碼的執行。java

同步編程,便是一種典型的請求-響應模型,當請求調用一個函數或方法後,需等待其響應返回,而後執行後續代碼。web

通常狀況下,同步編程,代碼按序依次執行,能很好的保證程序的執行,可是在某些場景下,好比讀取文件內容,或請求服務器接口數據,須要根據返回的數據內容執行後續操做,讀取文件和請求接口直到數據返回這一過程是須要時間的,網絡越差,耗費時間越長,若是按照同步編程方式實現,在等待數據返回這段時間,JavaScript是不能處理其餘任務的,此時頁面的交互,滾動等任何操做也都會被阻塞,這顯然是及其不友好,不可接受的,而這正是須要異步編程大顯身手的場景,以下圖,耗時任務A會阻塞任務B的執行,等到任務A執行完才能繼續執行B:ajax

同步編程任務阻塞流程

當使用異步編程時,在等待當前任務的響應返回以前,能夠繼續執行後續代碼,即當前執行任務不會阻塞後續執行。編程

異步編程,不一樣於同步編程的請求-響應模式,其是一種事件驅動編程,請求調用函數或方法後,無需當即等待響應,能夠繼續執行其餘任務,而以前任務響應返回後能夠經過狀態、通知和回調來通知調用者。跨域

多線程

前面說明了異步編程能很好的解決同步編程阻塞的問題,那麼實現異步的方式有哪些呢?一般實現異步方式是多線程,如C#, 即同時開啓多個線程,不一樣操做能並行執行,以下圖,耗時任務A執行的同時,在線程二中任務B也能夠執行:瀏覽器

多線程異步編程無阻塞流程

JavaScript單線程

JavaScript語言執行環境是單線程的,單線程在程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,後面的纔會執行,而使用異步實現時,多個任務能夠併發執行。那麼JavaScript的異步編程如何實現呢,下一節將詳細闡述其異步機制。服務器

並行與併發

前文提到多線程的任務能夠並行執行,而JavaScript單線程異步編程能夠實現多任務併發執行,這裏有必要說明一下並行與併發的區別。網絡

  • 並行,指同一時刻內多任務同時進行;
  • 併發,指在同一時間段內,多任務同時進行着,可是某一時刻,只有某一任務執行;

一般所說的併發鏈接數,是指瀏覽器向服務器發起請求,創建TCP鏈接,每秒鐘服務器創建的總鏈接數,而假如,服務器處10ms能處理一個鏈接,那麼其併發鏈接數就是100。

JavaScript異步機制

本節介紹JavaScript異步機制,首先來看一個例子:

for (var i = 0; i < 5; i ++) {
        setTimeout(function(){
            console.log(i);
        }, 0);
    }
    console.log(i);
    //5 ; 5 ; 5 ; 5; 5複製代碼

應該明白最後輸出的全是5:

  1. i在此處是for循環所在上下文環境的變量,有且只有一個i;
  2. 循環結束時i==5;
  3. JavaScript單線程事件處理器在線程空閒前不會執行下一事件。

如上面第三點所述,若是要真正理解以上例子中的setTimeout(),及JavaScript異步機制,須要理解JavaScript的事件循環和併發模型。

併發模型(Concurrency model)

目前,咱們已經知道,JavaScript執行異步任務時,不須要等待響應返回,能夠繼續執行其餘任務,而在響應返回時,會獲得通知,執行回調或事件處理程序。那麼這一切具體是如何完成的,又以什麼規則或順序運做呢?接下來咱們須要解答這個問題。

注:回調和事件處理程序本質上並沒有區別,只是在不一樣狀況下,不一樣的叫法。

前文已經提到,JavaScript異步編程使得多個任務能夠併發執行,而實現這一功能的基礎是JavScript擁有一個基於事件循環的併發模型。

堆棧與隊列

介紹JavaScript併發模型以前,先簡單介紹堆棧和隊列的區別:

  • 堆(heap):內存中某一未被阻止的區域,一般存儲對象(引用類型);
  • 棧(stack):後進先出的順序存儲數據結構,一般存儲函數參數和基本類型值變量(按值訪問);
  • 隊列(queue):先進先出順序存儲數據結構。

事件循環(Event Loop)

JavaScript引擎負責解析,執行JavaScript代碼,但它並不能單獨運行,一般都得有一個宿主環境,通常如瀏覽器或Node服務器,前文說到的單線程是指在這些宿主環境建立單一線程,提供一種機制,調用JavaScript引擎完成多個JavaScript代碼塊的調度,執行(是的,JavaScript代碼都是按塊執行的),這種機制就稱爲事件循環(Event Loop)。

注:這裏的事件與DOM事件不要混淆,能夠說這裏的事件包括DOM事件,全部異步操做都是一個事件,諸如ajax請求就能夠看做一個request請求事件。

JavaScript執行環境中存在的兩個結構須要瞭解:

  • 消息隊列(message queue),也叫任務隊列(task queue):存儲待處理消息及對應的回調函數或事件處理程序;
  • 執行棧(execution context stack),也能夠叫執行上下文棧:JavaScript執行棧,顧名思義,是由執行上下文組成,當函數調用時,建立並插入一個執行上下文,一般稱爲執行棧幀(frame),存儲着函數參數和局部變量,當該函數執行結束時,彈出該執行棧幀;

注:關於全局代碼,因爲全部的代碼都是在全局上下文執行,因此執行棧頂老是全局上下文就很容易理解,直到全部代碼執行完畢,全局上下文退出執行棧,棧清空了;也便是全局上下文是第一個入棧,最後一個出棧。

任務

分析事件循環流程前,先闡述兩個概念,有助於理解事件循環:同步任務和異步任務。

任務很好理解,JavaScript代碼執行就是在完成任務,所謂任務就是一個函數或一個代碼塊,一般以功能或目的劃分,好比完成一次加法計算,完成一次ajax請求;很天然的就分爲同步任務和異步任務。同步任務是連續的,阻塞的;而異步任務則是不連續,非阻塞的,包含異步事件及其回調,當咱們談及執行異步任務時,一般指執行其回調函數。

事件循環流程

關於事件循環流程分解以下:

  1. 宿主環境爲JavaScript建立線程時,會建立堆(heap)和棧(stack),堆內存儲JavaScript對象,棧內存儲執行上下文;
  2. 棧內執行上下文的同步任務按序執行,執行完即退棧,而當異步任務執行時,該異步任務進入等待狀態(不入棧),同時通知線程:當觸發該事件時(或該異步操做響應返回時),需向消息隊列插入一個事件消息;
  3. 當事件觸發或響應返回時,線程向消息隊列插入該事件消息(包含事件及回調);
  4. 當棧內同步任務執行完畢後,線程從消息隊列取出一個事件消息,其對應異步任務(函數)入棧,執行回調函數,若是未綁定回調,這個消息會被丟棄,執行完任務後退棧;
  5. 當線程空閒(即執行棧清空)時繼續拉取消息隊列下一輪消息(next tick,事件循環流轉一次稱爲一次tick)。

使用代碼能夠描述以下:

var eventLoop = [];
    var event;
    var i = eventLoop.length - 1; // 後進先出

    while(eventLoop[i]) {
        event = eventLoop[i--]; 
        if (event) { // 事件回調存在
            event();
        }
        // 不然事件消息被丟棄
    }複製代碼

這裏注意的一點是等待下一個事件消息的過程是同步的。

併發模型與事件循環
var ele = document.querySelector('body');

    function clickCb(event) {
        console.log('clicked');
    }
    function bindEvent(callback) {
        ele.addEventListener('click', callback);
    }    

    bindEvent(clickCb);複製代碼

針對如上代碼咱們能夠構建以下併發模型:

JavaScript併發模型

如上圖,當執行棧同步代碼塊依次執行完直到碰見異步任務時,異步任務進入等待狀態,通知線程,異步事件觸發時,往消息隊列插入一條事件消息;而當執行棧後續同步代碼執行完後,讀取消息隊列,獲得一條消息,而後將該消息對應的異步任務入棧,執行回調函數;一次事件循環就完成了,也即處理了一個異步任務。

再談setTimeout(...0)

瞭解了JavaScript事件循環後咱們再看前文關於setTimeout(...0)的例子就比較清晰了:

setTimeout(...0)所表達的意思是:等待0秒後(這個時間由第二個參數值肯定),往消息隊列插入一條定時器事件消息,並將其第一個參數做爲回調函數;而當執行棧內同步任務執行完畢時,線程從消息隊列讀取消息,將該異步任務入棧,執行;線程空閒時再次從消息隊列讀取消息。

再看一個實例:

var start = +new Date();
    var arr = [];

    setTimeout(function(){
        console.log('time: ' + (new Date().getTime() - start));
    },10);

    for(var i=0;i<=1000000;i++){
        arr.push(i);
    }複製代碼

執行屢次輸出以下:

setTimeout(...0)

setTimeout異步回調函數裏咱們輸出了異步任務註冊到執行的時間,發現並不等於咱們指定的時間,並且兩次時間間隔也都不一樣,考慮如下兩點:

  • 在讀取消息隊列的消息時,得等同步任務完成,這個是須要耗費時間的;
  • 消息隊列先進先出原則,讀取此異步事件消息以前,可能還存在其餘消息,執行也須要耗時;

因此異步執行時間不精確是必然的,因此咱們有必要明白不管是同步任務仍是異步任務,都不該該耗時太長,當一個消息耗時太長時,應該儘量的將其分割成多個消息。

Web Workers

每一個Web Worker或一個跨域的iframe都有各自的堆棧和消息隊列,這些不一樣的文檔只能經過postMessage方法進行通訊,當一方監聽了message事件後,另外一方纔能經過該方法向其發送消息,這個message事件也是異步的,當一方接收到另外一方經過postMessage方法發送來的消息後,會向本身的消息隊列插入一條消息,然後續的併發流程依然如上文所述。

JavaScript異步實現

關於JavaScript的異步實現,之前有:回調函數,發佈訂閱模式,Promise三類,而在ES6中提出了生成器(Generator)方式實現,關於回調函數和發佈訂閱模式實現可參見另外一篇文章,後續將推出一篇詳細介紹Promise和Generator。

歡迎踩踩個人我的博客

參考:

Concurrency model and Event Loop

掘金技術徵文活動的連接: https://juejin.im/post/58d8e99261ff4b006cd6874d

相關文章
相關標籤/搜索