深入理解javascript系列(十九):從Promise開始到async/await

雲棲直播~發表於2018-06-30

什麼是同步與非同步的定義,在這裡我就不做記錄,直接用程式碼來表示它們之間的區別。

首先使用Promise模擬一個發起請求的函式,該函式執行後,會在1s之後返回數值30。

function fn() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(30);
        }, 1000);
    })

在該函式的基礎上,我們也可以使用async/await語法來模擬同步效果。

var foo = async function() {
    var t = await fn();
    console.log(t);
    console.log(`next`);
}

foo();

輸出結果為:

Promise {<pending>} //1s 之後依次輸出
test:11 30
test:12 next

而非同步效果則會有不同的輸出結果:

var foo = function() {
    fn().then(function(res) {
        console.log(res);
    });
    console.log(`next`);
}

輸出結果:

next
// 1s後
30

好了,接下來我們正式開始記錄Promise

Promise

1.  Ajax

Ajax是網頁與服務端進行資料互動的一種技術。我們可以通過服務端提供的介面,用Ajax向服務端請求我們需要的資料。過程如下:

//簡單的Ajax原生實現

//服務端介面
var url = `api/xxxx`;
var result;

var XHR = new XMLHttpRequest();
XHR.open(`GET`, url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if(XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
    }
}

這樣看上去並沒有什麼問題。但是如果這個時候,還需要做另一個Ajax請求,那麼這個新的Ajax請求中的一個引數,則必須從上一個Ajax請求中獲取,這個時候我們就不得不就得在result得到後在進行一次請求。

當第三個Ajax(甚至更多)仍然依賴上一個請求的時候,此時的程式碼就變成了一場災難。我們需要不停地巢狀回撥函式,以確保下一個介面所需要的引數的正確性,這樣的災難,我們稱為回撥地獄。

所以隨著發展,就出現了Promise,他能解決這個問題。

我們想要確保某程式碼在某某之後執行時,可以利用函式呼叫棧,將想要執行的程式碼放入回撥函式中(這是利用同步阻塞)。

function a(callback) {
    console.log(`先結婚`)
    callback();
}

function b() {
    console.log(`再生孩子`)
}
a(b);

插個題外話:“瀏覽器最早內建的setTimeout與setInterval就是基於回撥的思想實現的”。

但是這裡也有一個問題,我們想要在a中執行的程式碼必須現在callback之前才能輸出我們想輸出的。那該怎麼辦?

其實問題很好解決,除了利用函式呼叫棧的執行順序外,還可以利用佇列機制來確保我們想要的程式碼壓後執行。

function a(callback) {
    //將想要執行的程式碼放入佇列中後,根據事件迴圈機制,
    //就不用把它放到最後面了。
    callback && setTimeout(callback, 0);
    console.log(`先結婚`)

}

function b() {
    console.log(`再生孩子`)
}
a(b);

與setTimeout類似,Promise也可以認為是一種任務分發器,它將任務分配到Promise佇列中,通常的流程是首先發起一個請求,然後等待(等待時間沒法確定)並處理請求結果。

var tag = true;
var p = new Promise(function(resolve, reject) {
    if(tag) {
        resolve(`tag is true`)
    } else {
        reject(`tag is false`)
    }
})

p.then(function(result) {
    console.log(result);
})
.catch(function(err) {
    console.log(err);
})

下面簡單介紹一下Promise的相關基礎知識:

  • new Promise表示建立一個Promise例項物件。
  • Promise函式中的第一引數為一個回撥函式,也可以稱之為executor。通常情況下,在這個函式中,會執行發起請求操作,並修改結果的狀態值。
  • 請求結果有三種狀態,分別是pending(等待中,表示還沒有得到結果)、resolved(得到了我們想要的結果,可以繼續執行),以及rejected(得到了錯誤的,或者不是我們期望的結果,拒絕繼續執行)。請求結果的預設狀態為pending。在executor函式中,可以分別使用resolve與rejected將狀態修改為對應的resolved與rejected。resolve、reject是executor函式的兩個引數,它們能夠將請求結果的具體資料傳遞出去。
  • Promise例項擁有的then方法,可以用來處理當請求結果的狀態變成resolved時的邏輯。then的第一個引數為一個回撥函式,該函式的引數是resolve傳遞出來的資料。在上面的例子中,result = tag is true。
  • Promise例項擁有的catch方法,可用來處理當前請求結果的狀態變成rejectd時的邏輯。catch的第一個引數為一個回撥函式,該函式的引數是一個reject傳遞出來的資料。在上面的例子中,err = tag is false。

下面通過例子來感受一下Promise的用法。

//demo01.js
function fn(num) {
    //建立一個Promise例項
    return new Promise(function(resolve, reject) {
        if(typeof num == `number`) {
           //修改結果狀態值為resolved
           resolve();
        } else {
            // 修改結果狀態值為rejected
            reject();
        }
    }).then(function() {
        console.log(`引數是一個number值`);
    }).catch(function() {
        console.log(`引數不是一個number值`);
    })
}

//修改引數的型別,觀察輸出的結果
fn(`12`);

//注意觀察該語句的執行順序
console.log(`next code`);

then方法可以接收兩個引數,第一個引數用來處理resolved狀態的邏輯,第二個引數用來處理rejected狀態的邏輯。

then方法因為返回的仍是一個Promise例項物件,因此then方法可以巢狀使用。在這個過程中,通過在內部函式末尾return的方式,能夠將資料持續往後傳遞。

下面我們來對Ajax進行一個簡單的封裝。

var url = `api/xxxx`;

//封裝一個get請求的方法
function getJSON(url) {
    return new Promise(function(resolve, reject) {
        //利用Ajax傳送一個請求
        var XHR = new XMLHttpRequest();
        XHR.open(`GET`, url, true);
        XHR.send();

        //等待結果
        XHR.onreadystatechange = function() {
            if(XHR.readyState == 4) {
                if(XHR.status == 200) {
                    try {
                        var res = JSON.parse(XHR.responseText);
                        // 得到正確的結果修改狀態並將資料傳遞出去
                        resolve(response);
                    } catch(e) {
                        reject(e)
                    }
                } else {
                    // 得到錯誤的結果並丟擲異常
                    reject(new Error(XHR.statusText));
                }
            }
        }
    })
}


//封裝好以後,使用就很簡單了
getJSON(url).then(function(res){
    console.log(res)
})

2.  Promise.all

當有一個Ajax請求,它的引數需要另外兩個甚至更多個請求都有返回結果之後才能確定時,就需要用到Promise.all來幫助我們應對這個場景。

Promise.all接收一個Promise物件組成的陣列作為引數,當這個陣列中所有的Promise物件狀態都變成resolved或者rejected時,它才會去呼叫then方法。

var url1 = `xxx`;
var url2 = `xxxxx`;

function renderAll() {
    return Promise.all([getJSON(url1), getJSON(url2)]);
}

renderAll().then(function(value) {
    console.log(value);
})

3.  Promise.race

與Promise.all相似的是,Promise.race也是一個Promise物件組成的陣列作為引數,不同的是,只要當陣列中的其中一個Promise狀態變成了resolved或者rejected時,就可以呼叫then方法。

async/await

非同步問題不僅可以用Promise,還可以用async/await,都說這是終極解決方案。

async/await是ES7中新增的語法,雖然現在有些瀏覽器已經支援了該語法,但在實際使用中,仍然需要在構建工具中配置對該語法的支援才能放心使用。

在函式宣告的前面,加上關鍵字async,這就是async的具體使用。

async function fn() {
    return 30;
}

//或者
const fn = async ()=> {
    return 30;
}

console.log(fn());

//列印結果
Promise {<resolved>: 30}__proto__:Promise[[PromiseStatus]]:"resolved"[[PromiseValue]]:30

可以發現列印結果是一個Promise物件,因此可以猜到async其實是Promise的一個語法糖,目的是為了讓寫法更加簡單,因此也可以使用Promise的相關語法來處理後續的邏輯。

fn().then(res=>{
    console.log(res);
})

await的含義是等待,意思就程式碼需要等待await後面的函式執行完並且有了返回結果之後,才繼續執行下面的程式碼。這正是同步的效果。

但是需要注意的是,await關鍵字只能在async函式中使用,並且await後面的函式執行後必須返回一個Promise物件才能實現同步的效果。

當使用一個變數去接收await的返回值時,該返回值為Promise中resolve傳遞出來的值,也就是PromiseValue。

為了切實感受下async/await的用法。我們結合實際開發中最常遇到的非同步請求介面的場景。

//先定義介面請求的方法,由於jQuery封裝的幾個請求方法都是返回Promise例項。
//因此可以直接使用async/await函式實現同步

const getUserInfo = () => $.get(`api/asdsd`);

const clickHandler = async ()=>{
    try{
        const res = await getUserInfo();
        console.log(res);
        
        // do something
    } catch(e){
        //處理錯誤邏輯
    }

為了保證邏輯的完整性,在實踐中try/catch必不可少。

原文釋出時間為:2018年06月21日
原文作者:Panthon

本文來源: 掘金 如需轉載請聯絡原作者


相關文章