(82)Wangdao.com第十六天_JavaScript 異步操做

異步操做git

 

  • 單線程模型
    • 指的是,JavaScript 只在一個線程上運行
    • 也就是說,JavaScript 同時只能執行一個任務,其餘任務都必須在後面排隊等待
      • 注意,JavaScript 只在一個線程上運行,不表明 JavaScript 引擎只有一個線程。

 

    • 事實上,JavaScript 引擎有多個線程,單個腳本只能在一個線程上運行(稱爲主線程),其餘線程都是在後臺配合

 

    • JavaScript 之因此採用單線程,而不是多線程,跟歷史有關係。
      • JavaScript 從誕生起就是單線程,緣由是不想讓瀏覽器變得太複雜,
      • 由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。
      • 好處:
        • 實現起來比較簡單,執行環境相對單純
        • Node 能夠用不多的資源,應付大流量訪問的緣由
      • 壞處:
        • 只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。
        • 常見的瀏覽器無響應(假死),每每就是由於某一段 JavaScript 代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行
      • JavaScript 語言自己並不慢,慢的是讀寫外部數據,好比等待 Ajax 請求返回結果。這個時候,若是對方服務器遲遲沒有響應,或者網絡不通暢,就會致使腳本的長時間停滯。

 

      • JavaScript 語言的設計者意識到,這時 CPU 徹底能夠無論 IO 操做,掛起處於等待中的任務,先運行排在後面的任務。等到 IO 操做返回告終果,再回過頭,把掛起的任務繼續執行下去。這種機制就是 JavaScript 內部採用的 「事件循環」機制(Event Loop)

 

      • 爲了利用多核 CPU 的計算能力,HTML5 提出 Web Worker 標準,容許 JavaScript 腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做 DOM。因此,這個新標準並無改變 JavaScript 單線程的本質。

 

    • 同步任務和異步任務

程序裏面全部的任務,能夠分紅兩類:同步任務(synchronous)異步任務(asynchronous)github

 

      • 同步任務
        • 是那些沒有被引擎掛起、在主線程上排隊執行的任務。
        • 只有前一個任務執行完畢,才能執行後一個任務。

 

      • 異步任務
        • 是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。
        • 只有引擎認爲某個異步任務能夠執行了(好比 Ajax 操做從服務器獲得告終果),該任務(採用回調函數的形式)纔會進入主線程執行。
        • 排在異步任務後面的代碼,不用等待異步任務結束會立刻運行,也就是說,異步任務不具備」堵塞「效應。
        • 舉例來講
            • Ajax 操做能夠看成同步任務處理,也能夠看成異步任務處理,由開發者決定。
              • 若是是同步任務,主線程就等着 Ajax 操做返回結果,再往下執行;
              • 若是是異步任務,主線程在發出 Ajax 請求之後,就直接往下執行,等到 Ajax 操做有告終果,主線程再執行對應的回調函數

 

    • 任務隊列和事件循環

JavaScript 運行時,除了一個正在運行的主線程,引擎還提供一個任務隊列(task queue),編程

裏面是各類須要當前程序處理的異步任務。(實際上,根據異步任務的類型,存在多個任務隊列。爲了方便理解,這裏假設只存在一個隊列。)數組

  • 首先,主線程會去執行全部的同步任務。等到同步任務所有執行完,就會去看任務隊列裏面的異步任務。
  • 若是知足條件,那麼異步任務就從新進入主線程開始執行,這時它就變成同步任務了。
  • 等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。

 

  • 異步任務的寫法一般是回調函數。
    • 一旦異步任務從新進入主線程,就會執行對應的回調函數。
    • 若是一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會從新進入主線程,由於沒有用回調函數指定下一步的操做。

 

  • JavaScript 引擎怎麼知道異步任務有沒有結果,能不能進入主線程呢?
    • 答案就是引擎在不停地檢查,一遍又一遍,只要同步任務執行完了,引擎就會去檢查那些掛起來的異步任務,是否是能夠進入主線程了
    • 這種循環檢查的機制,就叫作事件循環(Event Loop)。
    • 維基百科的定義是:「事件循環是一個程序結構,用於等待和發送消息和事件(a programming construct that waits for and dispatches events or messages in a program)」。

 

    • 異步操做的模式
      • 回調函數
        • 是異步操做最基本的方法
          • 下面是兩個函數f1f2,編程的意圖是f2必須等到f1執行完成,才能執行
            function f1() {
                // ...
            }
            
            function f2() {
                // ...
            }
            
            f1();
            f2();

            上面代碼的問題在於,若是f1是異步操做,f2會當即執行,不會等到f1結束再執行瀏覽器

            • 這時,能夠考慮改寫f1,把f2寫成f1的回調函數
              function f1(callback) {
                  // ...
                  callback();
              }
              
              function f2() {
                  // ...
              }
              
              f1(f2);
        • 回調函數的優勢:
          • 簡單、容易理解和實現
        • 回調函數的缺點:
          • 不利於代碼的閱讀和維護,
          • 各個部分之間高度耦合(coupling),使得程序結構混亂、流程難以追蹤(尤爲是多個回調函數嵌套的狀況),
          • 並且每一個任務只能指定一個回調函數

 

      • 事件監聽
        • 採用事件驅動模式。
        • 異步任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
        • 以 f1 和 f2 爲例。首先,爲 f1 綁定一個事件(這裏採用的 jQuery 的寫法)
          • f1.on('done', f2);    // 當 f1 發生 done 事件,就執行 f2

            f1進行改寫:服務器

            • function f1() {
                  setTimeout(function () {
                      // ...
                      f1.trigger('done');    // 表示,執行完成後,當即觸發 事件,從而開始執行
                  }, 1000);
              }donef2

               

        • 優勢:
          • 比較容易理解,能夠綁定多個事件,
          • 每一個事件能夠指定多個回調函數,
          • 並且能夠」去耦合「(decoupling),有利於實現模塊化
        • 缺點:
          • 整個程序都要變成事件驅動型,運行流程會變得很不清晰。
          • 閱讀代碼的時候,很難看出主流程。

 

      • 」發佈/訂閱模式」(publish-subscribe pattern),又稱「觀察者模式」(observer pattern)
        • 事件徹底能夠理解成」信號「,若是存在一個」信號中心「,
        • 某個任務執行完成,就向信號中心 」發佈「(publish)一個信號,
        • 其餘任務能夠向信號中心」訂閱「(subscribe)這個信號,從而知道何時本身能夠開始執行。

 

        • 能夠用多種方式實現這個模式
          • 採用的是 Ben Alman 的 Tiny Pub/Sub,這是 jQuery 的一個插件
            • 首先,f2 向信號中心 jQuery 訂閱 done 信號
              • jQuery.subscribe('done', f2);

                 

            • 而後,f1 進行以下改寫
              • function f1() {
                    setTimeout(function () {
                        // ...
                        jQuery.publish('done');
                    }, 1000);
                }

                f1 執行完成後,向信號中心 jQuery 發佈 done 信號,從而引起 f2 的執行網絡

            • f2 完成執行後,能夠取消訂閱(unsubscribe)
              • jQuery.unsubscribe('done', f2);

                 

            • 性質與「事件監聽」相似,可是明顯優於後者。
              • 由於能夠經過查看「消息中心」,瞭解存在多少信號、每一個信號有多少訂閱者,從而監控程序的運行

 

    • 異步操做的流程控制
        • 若是有多個異步操做,就存在一個流程控制的問題:如何肯定異步操做執行的順序,以及如何保證遵照這種順序
      • 串行執行:
        • 咱們能夠編寫一個流程控制函數,讓它來控制異步任務,一個任務完成之後,再執行另外一個。
          var items = [ 1, 2, 3, 4, 5, 6 ];
          var results = [];
          
          function async(arg, callback) {
              console.log('參數爲 ' + arg +' , 1秒後返回結果');
              setTimeout(function () { callback(arg * 2); }, 1000);
          }
          
          function final(value) {
              console.log('完成: ', value);
          }
          
          function series(item) {
              if(item) {
                  async( item, function(result) {
                      results.push(result);
                      return series(items.shift());
                  });
              } else {
                  return final(results[results.length - 1]);
              }
          }
          
          series(items.shift());
        • 函數 series() 就是串行函數,它會依次執行異步任務,全部任務都完成後,纔會執行final函數。
        • items[] 數組保存每個異步任務的參數,results[] 數組保存每個異步任務的運行結果。
        • 注意,上面的寫法須要六秒,才能完成整個腳本。

 

      • 並行執行
        • 流程控制函數也能夠並行執行,即全部異步任務同時執行,等到所有完成之後,才執行 final 函數
          • var items = [ 1, 2, 3, 4, 5, 6 ];
            var results = [];
            
            function async(arg, callback) {
                console.log('參數爲 ' + arg +' , 1秒後返回結果');
                setTimeout(function () { callback(arg * 2); }, 1000);
            }
            
            function final(value) {
                console.log('完成: ', value);
            }
            
            items.forEach(function(item) {
                async(item, function(result){
                    results.push(result);
                    if(results.length === items.length) {
                        final(results[results.length - 1]);
                    }
                })
            });
          • 上面代碼中,forEach方法會同時發起六個異步任務,等到它們所有完成之後,纔會執行 final 函數
        • 相比而言,上面的寫法只要一秒,就能完成整個腳本。
        • 這就是說,並行執行的效率較高,比起串行執行一次只能執行一個任務,較爲節約時間。
        • 可是問題在於若是並行的任務較多,很容易耗盡系統資源,拖慢運行速度。所以有了第三種流程控制方式

 

      • 並行與串行的結合
        • 所謂並行與串行的結合,就是設置一個門檻,每次最多隻能並行執行 個異步任務,這樣就避免了過度佔用系統資源。
          • var items = [ 1, 2, 3, 4, 5, 6 ];
            var results = [];
            var running = 0;
            var limit = 2;
            
            function async(arg, callback) {
                console.log('參數爲 ' + arg +' , 1秒後返回結果');
                setTimeout(function () { callback(arg * 2); }, 1000);
            }
            
            function final(value) {
                console.log('完成: ', value);
            }
            
            function launcher() {
                while(running < limit && items.length > 0) {
                    var item = items.shift();
                    async(item, function(result) {
                        results.push(result);
                        running--;
                        if(items.length > 0) {
                            launcher();
                        } else if(running == 0) {
                            final(results);
                        }
                    });
                    running++;
                }
            }
            
            launcher();

            上面代碼中,最多隻能同時運行兩個異步任務。變量 running 記錄當前正在運行的任務數,只要低於門檻值,就再啓動一個新的任務,若是等於 0,就表示全部任務都執行完了,這時就執行 final() 函數。多線程

        • 這段代碼須要三秒完成整個腳本,處在串行執行和並行執行之間。
        • 經過調節limit變量,達到效率和資源的最佳平衡。
相關文章
相關標籤/搜索