javascript異步編程詳解

前言

你可能知道,Javascript語言的執行環境是"單線程"(single thread)。
所謂"單線程",就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。
爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous).
"異步模式"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。javascript

setTimeout 函數的弊端

延時處理固然少不了 setTimeout這個神器,不少人對 setTimeout函數的理解就是:延時爲 n 的話,函數會在 n 毫秒以後執行。事實上並不是如此,這裏存在三個問題:
一個是 setTimeout函數的及時性問題, setTimeout是存在必定時間間隔的,並非設定 n 毫秒執行,他就是 n 毫秒執行,可能會有一點時間的延遲,setIntervalsetTimeout 函數運轉的最短週期是 5ms 左右,這個數值在 HTML規範 中也是有提到的:css

  • Let timeout be the second method argument, or zero if the argument was omitted.
    若是 timeout 參數沒有寫,默認爲 0html

  • If nesting level is greater than 5, and timeout is less than 4, then increase timeout to 若是嵌套的層次大於 5 ,而且 timeout 設置的數值小於 4 則直接取 4.java

其次是while循環會阻塞setTimeout的執行jquery

看這段代碼:
var t = true;

window.setTimeout(function (){
    t = false;
},1000);

while (t){}

alert('end');

結果是死循環致使setTimeout不執行,也致使alert不執行
js是單線程,因此會先執行while(t){}alert,但這個循環體是死循環,因此永遠不會執行alert
至於說爲何不執行setTimeout,是由於js的工做機制是:當線程中沒有執行任何同步代碼的前提下才會執行異步代碼,setTimeout是異步代碼,因此setTimeout只能等js空閒纔會執行,但死循環是永遠不會空閒的,因此setTimeout也永遠不會執行。git

第三是,try..catch捕捉不到他的錯誤github

異步編程方法

回調函數

這是異步編程最基本的方法。
假定有兩個函數f1和f2,後者等待前者的執行結果。面試

function f1(callback){
  setTimeout(function () {
    // f1的任務代碼
    callback();
  }, 1000);
}
f1(f2);

採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。
回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,並且每一個任務只能指定一個回調函數。ajax

事件監聽

另外一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生數據庫

f1.on('done', f2);
function f1(){
  setTimeout(function () {
    // f1的任務代碼
    f1.trigger('done');
  }, 1000);
}

JS 和 瀏覽器提供的原生方法基本都是基於事件觸發機制的,耦合度很低,不過事件不能獲得流程控制

Promises對象

Promises對象是CommonJS工做組提出的一種規範,目的是爲異步編程提供統一接口。

Promises能夠簡單理解爲一個事務,這個事務存在三種狀態:

  • 已經完成了 resolved

  • 由於某種緣由被中斷了 rejected

  • 還在等待上一個事務結束 pending

簡單說,它的思想是,每個異步任務返回一個Promises對象,該對象有一個then方法,容許指定回調函數,這樣寫的優勢在於,回調函數變成了鏈式寫法,程序的流程能夠看得很清楚

Promises就是一個事務的管理器。他的做用就是將各類內嵌回調的事務用流水形式表達,其目的是爲了簡化編程,讓代碼邏輯更加清晰。

Promises能夠分爲:

  • 無錯誤傳遞的 Promises,也就是事務不會由於任何緣由中斷,事務隊列中的事項都會被依次處理,此過程當中 Promises只有 pendingresolved兩種狀態,沒有 rejected狀態。

  • 包含錯誤的 Promises,每一個事務的處理都必須使用容錯機制來獲取結果,一旦出錯,就會將錯誤信息傳遞給下一個事務,若是錯誤信息會影響下一個事務,則下一個事務也會 rejected,若是不會,下一個事務能夠正常執行,依次類推。

此處留坑講generator實現異步編程
原本想本身總結下generator與異步的,看了下阮一峯老師的博客算是瞭解個大概,理解也是隻知其一;不知其二,有興趣的話能夠在底下的參考資料裏找到去看看

封裝好的實現

jqueryDeferred對象

簡單說,Deferred對象就是jquery的回調函數解決方案。在英語中,defer的意思是"延遲",因此Deferred對象的含義就是"延遲"到將來某個點再執行。
首先,回顧一下jquery的ajax操做的傳統寫法:

  $.ajax({
    url: "test.html",
    success: function(){
      alert("哈哈,成功了!");
    },
    error:function(){
      alert("出錯啦!");
    }
  });

有了<ode>Deferred對象之後,寫法是這樣的:

$.ajax("test.html")
 .done(function(){ alert("哈哈,成功了!"); })
 .fail(function(){ alert("出錯啦!"); });

能夠看到,done()至關於success方法,fail()至關於error方法。採用鏈式寫法之後,代碼的可讀性大大提升。
瞭解jQuery.Deferred對象能夠看下面這個表格。
圖片描述

when.js

AngularJS內置的Kris Kowal的Q框架,和cujoJS的when.js,二者都是Promises/A規範的實現
when.js實例

var getData = function() {
    var deferred = when.defer();
    $.getJSON(api, function(data){
        deferred.resolve(data[0]);
    });

    return deferred.promise;
}

var getImg = function(src) {
    var deferred = when.defer();

    var img = new Image();

    img.onload = function() {
        deferred.resolve(img);
    };

    img.src = src;

    return deferred.promise;
}

var showImg = function(img) {
    $(img).appendTo($('#container'));
}

getData()
.then(getImg)
.then(showImg);

看最後三行代碼,是否是一目瞭然,很是的語義化

var deferred = when.defer();

定義了一個deferred對象。

deferred.resolve(data);

在異步獲取數據完成時,把數據做爲參數,調用deferred對象的resolve方法。

return deferred.promise;

返回了deferred對象的Promises屬性。

控制流程工具step.js

github地址
step.js是控制流程工具(大小僅 150 行代碼),解決回調嵌套層次過多等問題。適用於讀文件、查詢數據庫等回調函數相互依賴,或者分別獲取內容最後組合數據返回等應用情景。異步執行簡單地能夠分爲「串行執行」和「並行」執行
使用示例:

Step(
  function readSelf() {
    fs.readFile(__filename, this);
  },
  function capitalize(err, text) {
    if (err) throw err;
    return text.toUpperCase();
  },
  function showIt(err, newText) {
    if (err) throw err;
    console.log(newText);
  }
);

Step 的一個約定,回調函數的第一個參數老是 err,第二個纔是值(沿用 Node 回調的風格)。若是上一個步驟發生異常,那麼異常對象將被送入到下一個步驟中。

擴展閱讀

Javascript既是單線程又是異步的,請問這兩者是否衝突,以及有什麼區別?

Answer1:Javascript自己是單線程的,並無異步的特性。

因爲 Javascript的運用場景是瀏覽器,瀏覽器自己是典型的 GUI 工做線程,GUI 工做線程在絕大多數系統中都實現爲事件處理,避免阻塞交互,所以產生了 Javascript異步基因。此後種種都源於此。

Answer2: JS的單線程是指一個瀏覽器進程中只有一個JS的執行線程,同一時刻內只會有一段代碼在執行(你可使用IE的標籤式瀏覽試試看效果,這時打開的多個頁面使用的都是同一個JS執行線程,若是其中一個頁面在執行一個運算量較大的function時,其餘窗口的JS就會中止工做)。
而異步機制是瀏覽器的兩個或以上常駐線程共同完成的,例如異步請求是由兩個常駐線程:JS執行線程和事件觸發線程共同完成的,JS的執行線程發起異步請求(這時瀏覽器會開一條新的HTTP請求線程來執行請求,這時JS的任務已完成,繼續執行線程隊列中剩下的其餘任務),而後在將來的某一時刻事件觸發線程監視到以前的發起的HTTP請求已完成,它就會把完成事件插入到JS執行隊列的尾部等待JS處理。又例如定時觸發(setTimeoutsetinterval)是由瀏覽器的定時器線程執行的定時計數,而後在定時時間把定時處理函數的執行請求插入到JS執行隊列的尾端(因此用這兩個函數的時候,實際的執行時間是大於或等於指定時間的,不保證能準肯定時的)。
因此,所謂的JS的單線程和異步更多的應該是屬於瀏覽器的行爲,他們之間沒有衝突,更不是同一種事物,沒有什麼區別不區別的。

setTimeout(fn,0)當即執行的問題

首先,不會當即執行,緣由:
setTimeout(fn,0)的做用很簡單,就是爲了把fn放到運行隊列的最後去執行。也就是說,不管setTimeout(fn,0)寫在哪,均可以保證在隊列的最後執行。js解析器會把setTimeout(fn,0)裏的fn壓到隊列的最後,由於它是異步操做。有個延時,具體是16ms仍是4ms取決於瀏覽器
當即執行仍是有可能的,只要在你調用setTimeout的時候,知足下面兩個條件:

  1. 恰好執行到了當前這一輪事件循環的底部。

  2. 恰好此時事件隊列爲空。

那麼setTimeout的回調函數就能夠當即執行。固然「當即執行」的意思是在任何其餘代碼前執行。

參考資料

知乎:setTimeout的異步以及js是單線程的面試題?
阮老師的博客
小鬍子哥的博客
知乎:JavaScript 既是單線程又是異步的,請問這兩者是否衝突,以及有什麼區別?
細嗅Promise
whenjs文檔
jQuery的deferred對象詳解
jQuery 中的 Deferred 和 Promises (2)
Step.js 使用教程(附源碼解析)
Generator 函數的含義與用法

相關文章
相關標籤/搜索