一篇文章帶你嘗試拿下js異步

走在前端的大道上html

本篇將本身讀過的相關 js異步 的文章中,對本身有啓發的章節片斷總結在這(會對原文進行刪改),會不斷豐富提煉總結更新。前端

概念

JS 是單線程的語言。

單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。jquery

優缺點
這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段Javascript代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。es6

爲何單線程
JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?面試

因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。ajax

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

應對方法
爲了解決這個問題,Javascript語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)json

因而,全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。數組

具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)promise

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。

(4)主線程不斷重複上面的第三步。

既是單線程又是異步難道不矛盾?

JS的單線程是指一個瀏覽器進程中只有一個JS的執行線程,同一時刻內只會有一段代碼在執行(你可使用IE的標籤式瀏覽試試看效果,這時打開的多個頁面使用的都是同一個JS執行線程,若是其中一個頁面在執行一個運算量較大的function時,其餘窗口的JS就會中止工做)。

而異步機制是瀏覽器的兩個或以上常駐線程共同完成的,例如異步請求是由兩個常駐線程:JS執行線程事件觸發線程 共同完成的,JS的 執行線程 發起異步請求(這時瀏覽器會開一條新的 HTTP請求線程 來執行請求,這時JS的任務已完成,繼續執行線程隊列中剩下的其餘任務),而後在將來的某一時刻 事件觸發線程 監視到以前的發起的HTTP請求已完成,它就會把完成事件插入到JS執行隊列的尾部等待JS處理。又例如定時觸發(settimeout和setinterval)是由瀏覽器的 定時器線程 執行的定時計數,而後在定時時間把定時處理函數的執行請求插入到JS執行隊列的尾端(因此用這兩個函數的時候,實際的執行時間是大於或等於指定時間的,不保證能準肯定時的)。因此,所謂的JS的單線程和異步更多的應該是屬於瀏覽器的行爲,他們之間沒有衝突,更不是同一種事物,沒有什麼區別不區別的。

clipboard.png

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

JS 中異步有幾種

JS 中異步操做還挺多的,常見的分如下幾種:

  • setTimeout (setInterval)
  • AJAX
  • Promise
  • async/await

setTimeout

JavaScript最基礎的異步函數是setTimeout和setInterval。setTimeout會在必定時間後執行給定的函數。它接受一個回調函數做爲第一參數和一個毫秒時間做爲第二參數。

setTimeout(
  function() { 
    console.log("Hello!");
}, 1000);

setTimout(setInterval)並非當即就執行的,這段代碼意思是,等 1s後,把這個 function 加入任務隊列中,若是任務隊列中沒有其餘任務了,就執行輸出 'Hello'。

var outerScopeVar; 
helloCatAsync(); 
console.log(outerScopeVar);

function helloCatAsync() {     
    setTimeout(function() {         
        outerScopeVar = 'hello';     
    }, 2000); 
}

執行上面代碼,發現 outerScopeVar 輸出是 undefined,而不是 hello。之因此這樣是由於在異步代碼中返回的一個值是不可能給同步流程中使用的,由於 console.log(outerScopeVar) 是同步代碼,執行完後纔會執行 setTimout。

helloCatAsync(function(result) {
console.log(result);
});

function helloCatAsync(callback) {
    setTimeout(
        function() {
            callback('hello')
        }
    , 1000)
}

把上面代碼改爲,傳遞一個callback,console 輸出就會是 hello。

有趣的是,直到在同一程序段中全部其他的代碼執行結束後,超時纔會發生。因此若是設置了超時,同時執行了需長時間運行的函數,那麼在該函數執行完成以前,超時甚至都不會啓動。實際上,異步函數,如setTimeout和setInterval,被壓入了稱之爲Event Loop的隊列。

Event Loop是一個回調函數隊列。當異步函數執行時,回調函數會被壓入這個隊列。JavaScript引擎直到異步函數執行完成後,纔會開始處理事件循環。這意味着JavaScript代碼不是多線程的,即便表現的行爲類似。事件循環是一個先進先出(FIFO)隊列,這說明回調是按照它們被加入隊列的順序執行的。

AJAX

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
        console.log(xhr.responseText);
    } else {
        console.log( xhr.status);
    }
}
xhr.open('GET', 'url', false);
xhr.send();

上面這段代碼,xhr.open 中第三個參數默認爲 false 異步執行,改成 true 時爲同步執行。

ES6 Promise

語法:

new Promise( function(resolve, reject) {...});
console.dir(Promise)

ƒ Promise()
    all:ƒ all()
    arguments:(...)
    caller:(...)
    length:1
    name:"Promise"
    prototype:Promise
        catch:ƒ catch()
        constructor:ƒ Promise()
        finally:ƒ finally()
        then:ƒ then()
        Symbol(Symbol.toStringTag):"Promise"__proto__:Object
    race:ƒ race()
    reject:ƒ reject()
    resolve:ƒ resolve()
    Symbol(Symbol.species):(...)
    get Symbol(Symbol.species):ƒ [Symbol.species]()
    __proto__:ƒ ()
    [[Scopes]]:Scopes[0]

這麼一看就明白了,Promise是一個構造函數,本身身上有all、reject、resolve這幾個眼熟的方法,原型上有then、catch等一樣很眼熟的方法。這麼說用Promise new出來的對象確定就有then、catch方法。

那就new一個玩玩吧。

var p = new Promise(function(resolve, reject){
    //作一些異步操做
    setTimeout(function(){
        console.log('執行完成');
        resolve('隨便什麼數據');
    }, 2000);
});

Promise的構造函數接收一個參數,是函數,而且傳入兩個參數:resolve,reject,分別表示異步操做執行成功後的回調函數 和 異步操做執行失敗後的回調函數。其實這裏用「成功」和「失敗」來描述並不許確,按照標準來說,resolve是將Promise的狀態置爲fullfiled,reject是將Promise的狀態置爲rejected,在遇到 resolve 或 reject以前,狀態一直是pending。

不過在咱們開始階段能夠先這麼理解,後面再細究概念。

在上面的代碼中,咱們執行了一個異步操做,也就是setTimeout,2秒後,輸出「執行完成」,而且調用resolve方法。 運行代碼,會在2秒後輸出「執行完成」。注意!我只是new了一個對象,並無調用它,咱們傳進去的函數就已經執行了,這是須要注意的一個細節。因此咱們用Promise的時候通常是包在一個函數中,在須要的時候去運行這個函數,如:

function runAsync(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            console.log('執行完成');
            resolve('隨便什麼數據');
        }, 2000);
    });
    return p;            
}
runAsync()

這時候你應該有兩個疑問:1.包裝這麼一個函數有毛線用?2.resolve('隨便什麼數據');這是幹毛的? 咱們繼續來說。在咱們包裝好的函數最後,會return出Promise對象,也就是說,執行這個函數咱們獲得了一個Promise對象。還記得Promise對象上有then、catch方法吧?這就是強大之處了,看下面的代碼:

runAsync().then(function(data){
    console.log(data);
    //後面能夠用傳過來的數據作些其餘操做
    //......
});

在 runAsync() 的返回上直接調用then方法,then接收一個參數,是函數,而且會拿到咱們在runAsync 中調用resolve時傳的的參數。運行這段代碼,會在2秒後輸出「執行完成」,緊接着輸出「隨便什麼數據」。 這時候你應該有所領悟了,原來then裏面的函數就跟咱們平時的回調函數一個意思,可以在runAsync這個異步任務執行完成以後被執行。這就是Promise的做用了,簡單來說,就是能把原來的回調寫法分離出來,在異步操做執行完後,用鏈式調用的方式執行回調函數。 你可能會不屑一顧,那麼牛逼轟轟的Promise就這點能耐?我把回調函數封裝一下,給runAsync傳進去不也同樣嗎,就像這樣:

function runAsync(callback){
    setTimeout(function(){
        console.log('執行完成');
        callback('隨便什麼數據');
    }, 2000);
}

runAsync(function(data){
    console.log(data);
});

效果也是同樣的,還費勁用Promise幹嗎。那麼問題來了,有多層回調該怎麼辦?若是callback也是一個異步操做,並且執行完後也須要有相應的回調函數,該怎麼辦呢?總不能再定義一個callback2,而後給callback傳進去吧。而Promise的優點在於,能夠在then方法中繼續寫Promise對象並返回,而後繼續調用then來進行回調操做。

鏈式操做的用法

三個函數runAsync一、runAsync二、runAsync3

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            console.log('異步任務1執行完成');
            resolve('隨便什麼數據1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            console.log('異步任務2執行完成');
            resolve('隨便什麼數據2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            console.log('異步任務3執行完成');
            resolve('隨便什麼數據3');
        }, 2000);
    });
    return p;            
}

使用Promise的正確場景是這樣的:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});

這樣可以按順序,每隔兩秒輸出每一個異步回調中的內容,在runAsync2中傳給resolve的數據,能在接下來的then方法中拿到。運行結果以下:

clipboard.png

從表面上看,Promise只是可以簡化層層回調的寫法,而實質上,Promise的精髓是「狀態」,用維護狀態、傳遞狀態的方式來使得回調函數可以及時調用,它比傳遞callback函數要簡單、靈活的多。

在then方法中,你也能夠直接return數據而不是Promise對象,在後面的then中就能夠接收到數據了,好比咱們把上面的代碼修改爲這樣:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return '直接返回數據';  //這裏直接返回數據
})
.then(function(data){
    console.log(data);
});

那麼輸出就變成了這樣:

clipboard.png

reject的用法

到這裏,你應該對「Promise是什麼玩意」有了最基本的瞭解。那麼咱們接着來看看ES6的Promise還有哪些功能。咱們光用了resolve,還沒用reject呢,它是作什麼的呢?事實上,咱們前面的例子都是隻有「執行成功」的回調,尚未「失敗」的狀況,reject的做用就是把Promise的狀態置爲rejected,這樣咱們在then中就能捕捉到,而後執行「失敗」狀況的回調。看下面的代碼。

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的隨機數
            if(num<=5){
                resolve(num);
            }
            else{
                reject('數字太大了');
            }
        }, 2000);
    });
    return p;            
}

getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函數用來異步獲取一個數字,2秒後執行完成,若是數字小於等於5,咱們認爲是「成功」了,調用resolve修改Promise的狀態。不然咱們認爲是「失敗」了,調用reject並傳遞一個參數,做爲失敗的緣由。 運行getNumber而且在then中傳了兩個參數,then方法 能夠接受兩個參數,第一個對應resolve的回調,第二個對應reject的回調。因此咱們可以分別拿到他們傳過來的數據。屢次運行這段代碼,你會隨機獲得下面兩種結果:

rejected
1

rejected
數字太大了

catch的用法

咱們知道Promise對象除了then方法,還有一個catch方法,它是作什麼用的呢?其實它 和then的第二個參數同樣,用來指定reject的回調,用法是這樣:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和寫在then的第二個參數裏面同樣。不過它還有另一個做用:在執行resolve的回調(也就是上面then中的第一個參數)時,若是拋出異常了(代碼出錯了),那麼並不會報錯卡死js,而是會進到這個catch方法中。請看下面的代碼:
getNumber()
.then(function(data){

console.log('resolved');
console.log(data);
console.log(somedata); //此處的somedata未定義

})
.catch(function(reason){

console.log('rejected');
console.log(reason);

});
在resolve的回調中,咱們console.log(somedata);而somedata這個變量是沒有被定義的。若是咱們不用Promise,代碼運行到這裏就直接 在控制檯報錯了,不往下運行了。可是在這裏,會獲得這樣的結果:

clipboard.png

也就是說進到catch方法裏面去了,並且把錯誤緣由傳到了reason參數中。即使是有錯誤的代碼也不會報錯了,這與咱們的try/catch語句有相同的功能。

all的用法

Promise的all方法提供了並行執行異步操做的能力,而且在全部異步操做執行完後才執行回調。咱們仍舊使用上面定義好的runAsync一、runAsync二、runAsync3這三個函數,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

用Promise.all來執行,all接收一個數組參數,裏面的值最終都算返回Promise對象。這樣,三個異步操做的並行執行的,等到它們都執行完後纔會進到then裏面。那麼,三個異步操做返回的數據哪裏去了呢?都在then裏面呢,all會把全部異步操做的結果放進一個數組中傳給then,就是上面的results。因此上面代碼的輸出結果就是:

clipboard.png

有了all,你就能夠並行執行多個異步操做,而且在一個回調中處理全部的返回數據,是否是很酷?有一個場景是很適合用這個的,一些遊戲類的素材比較多的應用,打開網頁時,預先加載須要用到的各類資源如圖片、flash以及各類靜態文件。全部的都加載完後,咱們再進行頁面的初始化。

race的用法

all方法的效果其實是「誰跑的慢,以誰爲準執行回調」,那麼相對的就有另外一個方法「誰跑的快,以誰爲準執行回調」,這就是race方法,這個詞原本就是賽跑的意思。race的用法與all同樣,咱們把上面runAsync1的延時改成1秒來看一下:

Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

這三個異步操做一樣是並行執行的。結果你應該能夠猜到,1秒後runAsync1已經執行完了,此時then裏面的就執行了。結果是這樣的:

clipboard.png

你猜對了嗎?不徹底,是吧。在then裏面的回調開始執行時,runAsync2()和runAsync3()並無中止,仍舊再執行。因而再過1秒後,輸出了他們結束的標誌。 這個race有什麼用呢?使用場景仍是不少的,好比咱們能夠用race給某個異步請求設置超時時間,而且在超時後執行相應的操做,代碼以下:

//請求某個圖片資源
function requestImg(){
    var p = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
        img.src = 'xxxxxx';
    });
    return p;
}

//延時函數,用於給請求計時
function timeout(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('圖片請求超時');
        }, 5000);
    });
    return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
    console.log(results);
})
.catch(function(reason){
    console.log(reason);
});

requestImg函數會異步請求一張圖片,我把地址寫爲"xxxxxx",因此確定是沒法成功請求到的。timeout函數是一個延時5秒的異步操做。咱們把這兩個返回Promise對象的函數放進race,因而他倆就會賽跑,若是5秒以內圖片請求成功了,那麼遍進入then方法,執行正常的流程。若是5秒鐘圖片還未成功返回,那麼timeout就跑贏了,則進入catch,報出「圖片請求超時」的信息。運行結果以下:

clipboard.png

promise 和 ajax 結合例子:

function ajax(url) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 ) {
                resovle(xhr.responseText);
            } else {
                reject( xhr.status);
            }
        }
        xhr.open('GET', url, false);
        xhr.send();
    });
}

ajax('/test.json')
    .then(function(data){
        console.log(data);
    })
    .cacth(function(err){
        console.log(err);
    });

jquery中的Promise

jquery中的Promise,也就是咱們所知道的Deferred對象

事實上,在此以前網上有不少文章在講jquery Deferred對象了,可是總喜歡把ajax和Deferred混在一塊兒講,容易把人搞混。when、done、promise、success、error、fail、then、resolve、reject、always這麼多方法不能揉在一塊兒講,須要把他們捋一捋,哪些是Deferred對象的方法,哪些是ajax的語法糖,咱們須要心知肚明。

先講$.Deferred

jquery用$.Deferred實現了Promise規範,$.Deferred是個什麼玩意呢?仍是老方法,打印出來看看,先有個直觀印象:

var def = $.Deferred();
console.log(def);

輸出以下:
520134-20160329213911379-199799420.png

$.Deferred()返回一個對象,咱們能夠稱之爲Deferred對象,上面掛着一些熟悉的方法如:done、fail、then等。jquery就是用這個Deferred對象來註冊異步操做的回調函數,修改並傳遞異步操做的狀態。

Deferred對象的基本用法以下,爲了避免與ajax混淆,咱們依舊舉setTimeout的例子:

function runAsync(){
    var def = $.Deferred();
    //作一些異步操做
    setTimeout(function(){
        console.log('執行完成');
        def.resolve('隨便什麼數據');
    }, 2000);
    return def;
}
runAsync().then(function(data){
    console.log(data)
});

在runAsync函數中,咱們首先定義了一個def對象,而後進行一個延時操做,在2秒後調用def.resolve(),最後把def做爲函數的返回。調用runAsync的時候將返回def對象,而後咱們就能夠.then來執行回調函數。

是否是感受和ES6的Promise很像呢?咱們來回憶一下第一篇中ES6的例子:

function runAsync(){
    var p = new Promise(function(resolve, reject){
        //作一些異步操做
        setTimeout(function(){
            console.log('執行完成');
            resolve('隨便什麼數據');
        }, 2000);
    });
    return p;           
}
runAsync()

區別在何處一看便知。因爲jquery的def對象自己就有resolve方法,因此咱們在建立def對象的時候並未像ES6這樣傳入了一個函數參數,是空的。在後面能夠直接def.resolve()這樣調用。

這樣也有一個弊端,由於執行runAsync()能夠拿到def對象,而def對象上又有resolve方法,那麼豈不是能夠在外部就修改def的狀態了?好比我把上面的代碼修改以下:

var d = runAsync();
d.then(function(data){
    console.log(data)
});
d.resolve('在外部結束');

現象會如何呢?並不會在2秒後輸出「執行完成」,而是直接輸出「在外部結束」。由於咱們在異步操做執行完成以前,沒等他本身resolve,就在外部給resolve了。這顯然是有風險的,好比你定義的一個異步操做並指定好回調函數,有可能被別人給提早結束掉,你的回調函數也就不能執行了。

怎麼辦?jquery提供了一個promise方法,就在def對象上,他能夠返回一個受限的Deferred對象,所謂受限就是沒有resolve、reject等方法,沒法從外部來改變他的狀態,用法以下:

function runAsync(){
    var def = $.Deferred();
    //作一些異步操做
    setTimeout(function(){
        console.log('執行完成');
        def.resolve('隨便什麼數據');
    }, 2000);
    return def.promise(); //就在這裏調用
}

這樣返回的對象上就沒有resolve方法了,也就沒法從外部改變他的狀態了。這個promise名字起的有點奇葩,容易讓咱們搞混,其實他就是一個返回受限Deferred對象的方法,與Promise規範沒有任何關係,僅僅是名字叫作promise罷了。雖然名字奇葩,可是推薦使用。

then的鏈式調用

既然Deferred也是Promise規範的實現者,那麼其餘特性也必須是支持的。鏈式調用的用法以下:

var d = runAsync();

d.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});

done與fail

咱們知道,Promise規範中,then方法接受兩個參數,分別是執行完成和執行失敗的回調,而jquery中進行了加強,還能夠接受第三個參數,就是在pending狀態時的回調,以下:
deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
除此以外,jquery還增長了兩個語法糖方法,done和fail,分別用來指定執行完成和執行失敗的回調,也就是說這段代碼:

d.then(function(){
    console.log('執行完成');
}, function(){
    console.log('執行失敗');
});

與這段代碼是等價的:

d.done(function(){
    console.log('執行完成');
})
.fail(function(){
    console.log('執行失敗');
});

always的用法

jquery的Deferred對象上還有一個always方法,不論執行完成仍是執行失敗,always都會執行,有點相似ajax中的complete。不贅述了。

$.when的用法

jquery中,還有一個$.when方法來實現Promise,與ES6中的all方法功能同樣,並行執行異步操做,在全部的異步操做執行完後才執行回調函數。不過$.when並無定義在$.Deferred中,看名字就知道,$.when,它是一個單獨的方法。與ES6的all的參數稍有區別,它接受的並非數組,而是多個Deferred對象,以下:

$.when(runAsync(), runAsync2(), runAsync3())
.then(function(data1, data2, data3){
    console.log('所有執行完成');
    console.log(data1, data2, data3);
});

jquery中沒有像ES6中的race方法嗎?就是以跑的快的爲準的那個方法。對的,jquery中沒有。

以上就是jquery中Deferred對象的經常使用方法了,還有一些其餘的方法用的也很少,乾脆就不記它了。接下來該說說ajax了。

ajax與Deferred的關係

jquery的ajax返回一個受限的Deferred對象,還記得受限的Deferred對象吧,也就是沒有resolve方法和reject方法,不能從外部改變狀態。想一想也是,你發一個ajax請求,別人從其餘地方給你取消掉了,也是受不了的。

既然是Deferred對象,那麼咱們上面講到的全部特性,ajax也都是能夠用的。好比鏈式調用,連續發送多個請求:

req1 = function(){
    return $.ajax(/*...*/);
}
req2 = function(){
    return $.ajax(/*...*/);
}
req3 = function(){
    return $.ajax(/*...*/);
}

req1().then(req2).then(req3).done(function(){
    console.log('請求發送完畢');
});

明白了ajax返回對象的實質,那咱們用起來就駕輕就熟了。

success、error與complete

這三個方法或許是咱們用的最多的,使用起來是這樣的:

$.ajax(/*...*/)
.success(function(){/*...*/})
.error(function(){/*...*/})
.complete(function(){/*...*/})

分別表示ajax請求成功、失敗、結束的回調。這三個方法與Deferred又是什麼關係呢?其實就是語法糖,success對應done,error對應fail,complete對應always,就這樣,只是爲了與ajax的參數名字上保持一致而已,更方便你們記憶,看一眼源碼:

deferred.promise( jqXHR ).complete = completeDeferred.add;
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;

complete那一行那麼寫,是爲了減小重複代碼,其實就是把done和fail又調用一次,與always中的代碼同樣。deferred.promise( jqXHR )這句也能看出,ajax返回的是受限的Deferred對象。

jquery加了這麼些個語法糖,雖然上手門檻更低了,可是卻形成了必定程度的混淆。一些人雖然這麼寫了好久,卻一直不知道其中的原理,在面試的時候只能答出一些皮毛,這是很很差的。這也是我寫這篇文章的原因。

jquery中Deferred對象涉及到的方法不少,本文儘可能分門別類的來介紹,但願能幫你們理清思路。總結一下就是:$.Deferred實現了Promise規範,then、done、fail、always是Deferred對象的方法
$.when是一個全局的方法,用來並行運行多個異步任務,與ES6的all是一個功能
$.ajax返回一個Deferred對象,success、error、complete是ajax提供的語法糖,功能與Deferred對象的done、fail、always一致

async/await

語法:

async function name([param[, param[, ... param]]]) { statements }

調用 async 函數時會返回一個 promise 對象。當這個 async 函數返回一個值時,Promise 的 resolve 方法將會處理這個值;當 async 函數拋出異常時,Promise 的 reject 方法將處理這個異常值。

async 函數中可能會有await 表達式,這將會使 async 函數暫停執行,等待 Promise 正常解決後繼續執行 async 函數並返回解決結果(異步)。

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add1(x) {
  var a = resolveAfter2Seconds(20);
  var b = resolveAfter2Seconds(30);
  return x + await a + await b;
}

add1(10).then(v => {
  console.log(v);  // 2s 後打印 60, 兩個 await 是同時發生的,也就是並行的。
});

async function add2(x) {
  var a = await resolveAfter2Seconds(20);
  var b = await resolveAfter2Seconds(30);
  return x + a + b;
}

add2(10).then(v => {
  console.log(v);  // 4s 後打印 60,按順序完成的。
});

上面代碼是 mdn 的一個例子。

說明了 async 返回的是一個 promise對象;
await 後面跟的表達式是一個 promise,執行到 await時,函數暫停執行,直到該 promise 返回結果,而且暫停但不會阻塞主線程。
await 的任何內容都經過 Promise.resolve() 傳遞。
await 能夠是並行(同時發生)和按順序執行的。
看下面兩段代碼:

async function series() {
    await wait(500);
    await wait(500);
    return "done!";
}

async function parallel() {
  const wait1 = wait(500);
  const wait2 = wait(500);
  await wait1;
  await wait2;
  return "done!";
}

第一段代碼執行完畢須要 1000毫秒,這段 await 代碼是按順序執行的;第二段代碼執行完畢只須要 500 毫秒,這段 await 代碼是並行的。

回調函數

這是異步編程最基本的方法。

假定有兩個函數f1和f2,後者等待前者的執行結果。

  f1();

  f2();

若是f1是一個很耗時的任務,能夠考慮改寫f1,把f2寫成f1的回調函數。

  function f1(callback){

    setTimeout(function () {

      // f1的任務代碼

      callback();

    }, 1000);

  }

執行代碼就變成下面這樣:

  f1(f2);

採用這種方式,咱們把同步操做變成了異步操做,f1不會堵塞程序運行,至關於先執行程序的主要邏輯,將耗時的操做推遲執行。

回調函數的優勢是簡單、容易理解和部署,缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,並且每一個任務只能指定一個回調函數。

事件監聽

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

仍是以f1和f2爲例。首先,爲f1綁定一個事件(這裏採用的jQuery的寫法)。

  f1.on('done', f2);

上面這行代碼的意思是,當f1發生done事件,就執行f2。而後,對f1進行改寫:

  function f1(){

    setTimeout(function () {

      // f1的任務代碼

      f1.trigger('done');

    }, 1000);

  }

f1.trigger('done')表示,執行完成後,當即觸發done事件,從而開始執行f2。

這種方法的優勢是比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合"(Decoupling),有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。

參考文章
1.Promise
2.JavaScript 運行機制詳解:再談Event Loop
3.知乎問答--知乎用戶
4.知乎問答--響馬
5.知乎問答--passenger
6.大白話講解Promise(一)
7.搞懂jquery中的Promise
8.JavaScript異步編程

相關文章
相關標籤/搜索