將"回調地獄"按在地上摩擦的Promise

這是一段旁白

「異步虐我千百遍,我待異步如初戀」!!
作前端的同窗作異步確定都不陌生。由於JavaScript是單線程語言(也就是說不支持多線程編程,這不是廢話麼啊喂!),因此在JavaScript中處理異步問題也是通過了幾代人的踩坑和開荒纔有了今天的「花裏胡哨」的解決方案。前端

回調(CallBack)

利用回調來實現異步是一直以來很是有效的解決方案,並且經久不衰。其背後的原理很簡單,就是利用JavaScript中能夠將函數做爲參數傳入另外一個函數(萬物皆對象)。舉個栗子:node

function callBack() {
    console.log('回調啦回調啦!!!');
}

function main(cb) {
    console.log('我會運行好久!')
    cb();
}

main(callBack);

下面一段代碼中實現兩個函數 callBackmain。隨後將 callBack 傳入到 main 函數中,當 main 函數執行到一個階段時候會調用傳入的回調函數 ( 此處是當main函數運行到底部時候就調用了回調函數 )。運行結果不言而喻:git

clipboard.png

這樣的寫法看起來貌似還行,寫法簡單明瞭,一看就懂。可是這裏筆者要吐槽下去年本身的智商,且聽慢慢道來:
去年在重構項目的時候,有一個頁面須要展現 4 個下拉框並且下拉框的數據須要從後臺拉取。因此筆者在ComponentWillMount(React項目)方法中執行了拉取數據的動做並且是分開獨立拉取,相似於:編程

......

ComponentWillMount() {
    let data = {};
    fetchSelect1();
    fetchSelect2();
    fetchSelect3();
    fetchSelect4();
}

......

最後在四個方法中將數據存儲到 data 對象中以供渲染選擇框,可是後面出現了一個意想不到問題:總會有一個下拉框數據拉取失敗。因此不得已採用了回調方式來處理,這裏再狠狠得吐槽一下本身,若是那時候會用Promise,也不會那麼尷尬。下面貼一下當時的代碼:json

/* fetch data source prop selects */
router.get("/fetch-selects", function(req, resp, next) {
    let path1 = config.BACKEND_API.BASE_URL + '/datasource/frequency';
    var reponseData = {};
    httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path1, "GET", function(data) {
        reponseData.frequency = data;
        let path2 = config.BACKEND_API.BASE_URL + '/datasource/category';
        httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path2, "GET", function(data) {
            reponseData.category = data;
            let path3 = config.BACKEND_API.BASE_URL + '/datasource/type';
            httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path3, "GET", function(data) {
                reponseData.type = data;
                let path4 = config.BACKEND_API.BASE_URL + '/datasource/process/type';
                httpAgent.httpRequest({}, "JSON", config.BACKEND_API.TYPE, config.BACKEND_API.HOST, config.BACKEND_API.PORT, path4, "GET", function(data) {
                    reponseData.process = data;
                    resp.json(reponseData);
                }, function(code, body) {

                })
            }, function(code, body) {

            })
        }, function(code, body) {

        })
    }, function(code, body) {

    })
});

當時用的Node項目作的中間層,這是一個路由。能夠看出來其實就是在拉取完第一條數據後再調用另外一個函數來拉取第二條數據,如此嵌套下去。好在只須要拉取 4 條數據,那若是有10條乃至100條數據須要拉取怎麼辦?那豈不是須要嵌套出一個很深很深的代碼結構麼?這就是臭名昭著的「回調地獄」。「回調地獄」的問題在於寫法過於繁瑣不夠優雅、代碼維護炒雞蛋疼,因此一直被前端程序猿所詬病,尤爲是維護相似代碼的時候簡直日了一羣哈士奇。不只僅是想死的心了,徹底想刪庫走人啊喂!數組

Promise

當前端異步工做處於水深火熱中時,一個英雄踏着七彩祥雲而來,他,就是 Promise。讓咱們相信:一個承諾,終究會被兌現。promise

Promise的由來

Promise 先由社區提出來的概念,主要是用於解決前端的異步問題,慶幸的是它在ES6中也獲得了實現。

什麼是Promise

Promise 是一個狀態機。這麼說可能有點很差懂,上個代碼先:多線程

new Promise(function(resolve, reject) {
    try {
        resolve('Success')
    } catch (e) {
        reject(e);
    }
})

從上面能夠看出幾個重要的點:
1,Promise是一個構造函數
2,新建Promise對象須要傳入執行器函數 (executor function)。
3,執行器函數中有兩個參數 resolvereject。這兩個也是執行器函數。異步

對此來解釋下什麼叫狀態機
Promise對象有三個狀態:pending, fulfilled, rejected,沒圖說個JB?函數

clipboard.png

從圖中能夠看出,Promise對象的初始狀態是pending ,若是通過了 resolve 方法,狀態置爲 fulfilled ;若是通過了 reject 方法,狀態置爲 rejected 。並且有三點須要明確:
1,Promise對象的狀態轉換隻有 pending--->fulfilled 或者 pending--->rejected。沒有其它形式的轉換。
2,Promise 對象的狀態一經轉換則永久凍結,意思就是說好比狀態被置爲 fulfilled 後,沒法再回到 pending。
3,Promise對象狀態以resolvereject爲分水嶺。調用這個兩個方法以前,都處於pending狀態。

Promise.resolve()

Promise.resolve(value)方法返回一個以給定值 value 解析後的 Promise 對象

摘自MDN對 Promise.resolve() 的解釋。簡單的理解就是它用來返回任務執行成功後的返回值。Promise對象調用完這個方法後狀態就被置爲 fulfilled。

Promise.reject()

Promise.reject(reason)方法返回一個帶有拒絕緣由reason參數的 Promise 對象

摘自MDN對 Promise.reject() 的解釋。Promise對象調用完這個方法後狀態就被置爲 rejected。

Promise.prototype.then()

看到這裏可能會有這麼一個問題:既然Promise用 resolve 和reject 返回處理結果,那如何獲取到這個結果呢?那麼then()就大有可爲了。從小標題能夠看出 then 方法被放在Promise的原型上,也就是說任何一個Promise對象均可以調用這個方法,無論什麼時候何地。then()方法的參數爲兩個個執行器函數,第一個函數用來處理 resolve() 返回值,第二個函數用來處理 reject() 返回值。而且then()返回的也是一個 Promise 對象。舉個🌰:
首先模擬 resolve() 返回

new Promise(function(reslove, reject) {
    try {
        reslove('Success')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {
    console.log(reslove_response);
}, function(reject_response) {
    console.log(reject_response);
})

執行結果:

clipboard.png

首先模擬 reject() 返回

new Promise(function(reslove, reject) {
    try {
        throw new Error('發生錯誤了!')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {
    console.log(reslove_response);
}, function(reject_response) {
    console.log(reject_response);
})

運行結果:

clipboard.png

檢查 then() 返回的對象類型

let promise = new Promise(function(reslove, reject) {
    try {
        throw new Error('發生錯誤了!')
    } catch (e) {
        reject(e);
    }
}).then(function(reslove_response) {}, function(reject_response) {})

console.log(promise instanceof Promise)

運行結果:

clipboard.png

運行狀況如預想同樣。

說到這裏會不會有這樣一個問題:爲何這幾個方法都是返回一個Promise對象? 請接着往下看。

Promise 鏈式調用

什麼叫鏈式調用?
簡單的來講就是醬紫的:
new Promise(function() {}).then(function() {}).then(function()).....

就是調用完 then() 之後還能夠繼續 then 下去,並且咱們前面說了只有Promise對象裏有then()方法,因此每一個方法都須要返回一個Promise對象以支持鏈式調用。而且下一個then()能夠拿到前一個then()的返回值,前提是前一個then()的確返回了一個值,繼續舉🌰:

new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    console.log(result + 1);
})

咱們拿一個數字 1 出來,而且一直傳遞下去,每到一個then都給它加 1,最終結果是4。親測無誤,這就是Promise鏈式調用的基本形式,寫法相對於回調函數那簡直是一個優雅。另外,Promise的鏈式調用筆者以爲用的最多的地方就是連續處理多個任務,而且後一個任務須要用到前一個任務的返回值。若是不大理解,請再瞄一下剛剛的例子。

Promise.prototype.catch()

catch()這個方法你們都很熟悉,就是去捕獲一個錯誤,固然在Promise裏也是一樣的做用。通常狀況下會與鏈式調用搭配起來用,它會捕獲前面任一步所拋出(reject 或者 throw)的錯誤。🌰來了:

new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    return result + 1;
}).then(function(result) {
    throw new Error('出錯啦!')
}).catch(function(err) {
    console.log(err);
})

運行結果:
clipboard.png

注:catch()實際上是then(null,function(){})的語法糖,將上述例子中的catch改爲後者一樣有效

Promise.all()

all()這個方法就厲害了。咱們能夠向其中傳入一個可迭代的對象,對象中存有多個待執行的任務,Promise.all()會順序執行並將結果按照原任務順序存入數組返回或者當遇到任何一個任務出錯的時候會拋出相應錯誤而且不作任何返回。🌰:

let p1 = 'Promise1';
let p2 = Promise.resolve('Promise2');
let p3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 200, 'Promise3');
});

Promise.all([p1, p2, p3]).then(function(values) {
    console.log(values);
});

運行結果:

clipboard.png

Promise.race()

race()方法的參數與all()的參數一致,不過不一樣的是,all()方法須要所有執行直到所有執行成功或者任意一個任務執行失敗。而race()只須要等到某一個任務執行完畢(不論是成功仍是失敗);🌰:

let p1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'promise1');
});

let p2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 200, 'promise1');
});

Promise.race([p1, p2]).then(function(value) {
    console.log(value);
});

執行結果:

clipboard.png

Promise走進科學

下面咱們來打開腦洞,經過幾個情景來猜想結果

問題:Promise鏈式調用時候(尾部包含catch()),若是中間的then()出錯了會影響會面then的運行嗎?
new Promise(function(resolve, reject) {
    resolve(1);
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    throw new Error('中間報錯啦!')
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).catch(function(err) {
    console.log(err);
})

運行結果:

clipboard.png

結論:使用Promise鏈式調用,一旦某個任務出錯將會直接中止向下調用並拋出錯誤

如何把catch()放在第一個then()前面,後面的then出錯的話,錯誤會被catch嗎?
new Promise(function(resolve, reject) {
    resolve(1);
}).catch(function(err) {
    console.log(err);
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
}).then(function(result) {
    throw new Error('中間報錯啦!')
    console.log(result);
    return result + 1;
}).then(function(result) {
    console.log(result);
    return result + 1;
})

運行結果:

clipboard.png

結論:catch()須要放在Promise鏈最後,否則沒法catch到錯誤,只會被執行環境catch

最後筆者用Promise寫了一個例子,功能是獲取新聞數據而且存儲到文本中,運行方法是命令行進入文件夾運行node index.js 就能夠了前提是已經安裝了Node傳送門

好了,大概就這麼多了。在此作一下感慨:忽然以爲寫博客是多麼享受的一件事,不論是本身的研究成果仍是學習成果經過文字寫出來,自己就是一件了不得的事情,不只能夠對本身懂得的技術的進一步昇華,並且若是還能幫助到別人那更是功德無量的事呀;

相關文章
相關標籤/搜索