Promise詳解

      Promise是我最喜歡的es6語法,也是面試中最容易問到的部分。那麼怎麼作到在使用中駕輕就熟,在面試中脫穎而出呢?
      先來個面試題作作:html

面試題:用Promise封裝一下原生ajax

      面試官常常會讓手寫一個Promise封裝,寫出下面這一版就好了(想了解更多的可自行擴展):java

function ajaxMise(url, method, data, async, timeout) {
    var xhr = new XMLHttpRequest()
    return new Promise(function (resolve, reject) {
        xhr.open(method, url, async);
        xhr.timeout = options.timeout;
        xhr.onloadend = function () {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
                resolve(xhr);
            else
                reject({
                    errorType: 'status_error',
                    xhr: xhr
                })
        }
        xhr.send(data);
        //錯誤處理
        xhr.onabort = function () {
            reject(new Error({
                errorType: 'abort_error',
                xhr: xhr
            }));
        }
        xhr.ontimeout = function () {
            reject({
                errorType: 'timeout_error',
                xhr: xhr
            });
        }
        xhr.onerror = function () {
            reject({
                errorType: 'onerror',
                xhr: xhr
            })
        }
    })
}
複製代碼

Promise簡介

      Promise是一個對象,保存着將來將要結束的事件。她有兩個特徵,引用阮一峯老師的描述就是:react

(1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變。
(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。es6

Promise基本用法
let promise1 = new Promise(function (resolve, reject){
    setTimeout(function (){
        resolve('ok') //將這個promise置爲成功態(fulfilled),會觸發成功的回調
    },1000)
})
promise1.then(fucntion success(val) {
    console.log(val) //一秒以後會打印'ok'
})
複製代碼
最簡單代碼實現一個Promise
class PromiseM {
    constructor (process) {
        this.status = 'pending'
        this.msg = ''
        process(this.resolve.bind(this), this.reject.bind(this))
        return this
    }
    resolve (val) {
        this.status = 'fulfilled'
        this.msg = val
    }
    reject (err) {
        this.status = 'rejected'
        this.msg = err
    }
    then (fufilled, reject) {
        if(this.status === 'fulfilled') {
            fufilled(this.msg)
        }
        if(this.status === 'rejected') {
            reject(this.msg)
        }
    }

}
//測試代碼
var mm=new PromiseM(function(resolve,reject){
    resolve('123');
});
mm.then(function(success){
    console.log(success);
},function(){
    console.log('fail!');
});
複製代碼

Micro-task / event loop

      上面提到Promise和事件的不一樣,除此以外還有一個重要不一樣,就是Promise建立是micro-task。再看一道面試題:面試

面試題:寫出下面代碼的輸出順序

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');
複製代碼

      正確答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。緣由就是:ajax

  • setTimeout(或者事件)註冊的是一個task,由Event Loop控制
  • Promise註冊的是一個micro-task

      Event Loop是js的一個重要機制,就是遇到事件或者setTimeout等就會把對應的回調函數放入一個事件隊列(task queue),等到主程序執行完畢就依次把隊列裏的函數壓入棧中執行。能夠參考阮一峯老師的JavaScript 運行機制詳解:再談Event Loop,不過貌似老師的網站被攻擊尚未恢復。
      可是Promise不是上面的機制,她建立的是一個微任務(micro-task),micro-task的執行老是在當前執行棧結束和下一個task執行以前,順序就是「當前執行棧」 -> 「micro-task」 -> 「task queue中取一個回調」 -> 「micro-task」 -> ... (不斷消費task queue) -> 「micro-task」,總之就是當前執行棧爲空時,就到了一個micro-task的檢查點。
      下面是micro-task的定義:數據庫

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
編程

      Promise註冊的是micro-task,因此上面題目中:主線程中'script start'、'script end'先打印,而後清空微任務隊列,'promise1'、'promise2'打印,而後取出task queue中的回調執行,'setTimeout'打印。後端

爲何出現promise

      Promise提供了對js異步編程的新的解決方案,由於咱們一直使用的回調函數實際上是存在很大問題,只是限制於js的單線程等緣由不得不大量書寫。固然Promise並非徹底擺脫回調,她只是改變了傳遞迴調的位置。那麼傳統的回調存在什麼問題呢?promise

嵌套

      這裏所說的嵌套是指大量的回調函數會使得代碼難以讀懂和修改,試想一個這個場景:讓你把下面的url4的調用提到url2以前。你須要很是當心的剪切代碼,而且笨拙的粘貼,result4這個參數你還不敢修改,由於這要額外花費不少功夫而且存在風險。

$.ajax('url1',function success(result1){
    $.ajax('url2',function success(result2){
        $.ajax('url3',function success(result3){
            $.ajax('url4',function success(result4){
                //……
            })
        })
    })
})

複製代碼

      固然,上面的問題有點戲劇成分,現實中極少出現這種難搞的狀況。與此相比,回調函數帶來的思惟上的難以理解是更致命的,由於咱們的大腦更喜歡同步的邏輯,這也是爲何await關鍵字那麼受歡迎的緣由。
      我記得有一次我給後端的同窗作JS新特性分享的時候,說到await關鍵字,有我的驚呼:「哇!這個不錯啊,這就能夠像寫java同樣寫代碼了」。

信任

      除去書寫的不優雅和維護的困難之外,回調函數其實還存在信任問題。
      事實上回調函數不必定會像你指望的那樣被調用。由於控制權不在你的手上。這種問題被稱做「控制反轉」。例以下面的例子:

$.ajax('xxxxxx',function success(result1){
    //好比成功以後我會操做數據庫記錄結算金額
})
複製代碼

      上面是jQuery中的ajax調用,咱們指望在某些事件結束後,讓第三方(jQ)幫咱們執行個人程序(回調)。
      那麼,咱們和第三方之間並無一個契約或者規範能夠遵循,除非你把你想使用的第三方庫通讀一遍,保證它作了你想作的事,但事實上你很難肯定。即便在本身的代碼中,或者本身編寫的工具,咱們都很難作到百分之百信任。

Promise解決方案

      Promise是一個規範,嘗試以一種更加友好的方式書寫代碼。Promise對象接受一個函數做爲參數,函數提供兩個參數:

  • resolve:將promise從未完成切換到成功狀態,也就是上面提到的從pending切換到fufilled,resolve能夠傳遞參數,下一級promise中的成功函數會接收到它
  • reject:將promise從未完成切換到失敗狀態,即從pending切換到rejected
let promise1 = new Promise(function(reslove, reject){
    //reslove或者reject或者出錯
})
promise1.then(fufilled, rejected).then().then() //這是僞代碼
promise1.then(fufilled, rejected)//能夠then屢次

function fufilled(data) {
    console.log(data)
}
function rejected(e){
    console.log(e)
}
複製代碼

      正如上面提到的兩個特徵,一旦狀態改變,這個Promise就已經完成決議(不會再更改),而且返回一個新的Promise,能夠鏈式調用。而且能夠註冊多個then方法,他們同時決議而且互不影響。這種設計明顯比回調函數要優雅的多,也更易於理解和維護。那麼在信任問題上她又有哪些改善呢?
      Promise經過通知的機制將「控制反轉」的關係又「反轉」回來。回調是我傳遞給第三方一個函數,指望它在事件發生時幫我執行,而Promise是在你們都遵循規範的前提下,我會在事件發生時獲得通知,這時我決定作一些事(執行一些函數)。看到了吧,這是有本質差別的。
      此外,回調函數還有如下信任問題,Promise也都作了相關約束:

  • 回調調用過早
  • 回調調用過晚(或者沒有調用)
  • 調用次數太多
  • 沒有把參數成功傳遞給你的回調
  • 吐掉了錯誤或者異常
過早或者過晚

      一個Promise回調必定會在當前棧執行完畢和下一個異步時機點上調用,即便像下面這樣的同步resolve代碼也會異步執行,而你傳給工具庫的回調函數卻可能被同步執行(調用過早)或者被忘記執行(或者過晚)。

new Promise(function (resolve) {
    resolve(111111);
})
複製代碼
次數太多或者沒有傳遞參數

      Promise只能被決議一次,若是你屢次決議,她只會執行第一次決議,例如:

new Promise(function (reslove, reject) {
    resolve()
    setTimeout(function () {
        resolve(2)
    },1000)
    resolve(3)
}).then(function (val) {
    console.log(val)   //undefined
})
複製代碼

      成功回調的參數是經過resolve傳遞的,例如像上面的代碼同樣,沒有傳遞參數,那麼val收到的會是undefined,因此,不管如何都會收到參數。注意:resolve只接收一個參數,以後的參數會被忽略。

吞掉錯誤

      Promise的錯誤處理機制是這樣的:若是顯示的調用reject並傳遞錯誤理由,這個消息會傳遞給拒絕回調。
      此外,若是任意過程當中出現錯誤(例如TypeError或者ReferenceError),這個錯誤會被捕捉,而且使這個Promise拒絕,也就是說這個錯誤消息也會傳遞給拒絕回掉,這與傳統的回調是不一樣的,傳統的回調一旦出錯會引發同步相應,而不出錯則是異步。

promise併發控制

all / race

      allrace兩個函數都是併發執行promise的方法,他們的返回值也是promiseall會等全部的promise都決議以後決議,而race是隻要有一個決議就會決議。

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});
複製代碼

注意:若是參數爲空,all方法會馬上決議,而race方法會掛住。

面試題:封裝一個promise.all方法
Promise.all = function(ary) {
    let num = 0
    let result = []
    return new Promise(function(reslove, reject){
        ary.forEach(promise => {
            promise.then(function(val){
                if(num >= ary.length){
                    reslove(result)
                }else{
                    result.push(val)
                    num++
                }
            },function(e){
                reject(e)
            })
        })
    })
}

複製代碼

thenalbe

如何檢測一個對象是Promise?

      你肯能會想到 instanceof Promise,但遺憾的是不能夠。緣由是每種環境都封裝了本身的Promise,而不是使用原生的ES6 Promise
      因此目前判斷Promise的一種方法就是判斷它是否是thenable對象(若是它是一個對象或者函數,而且它具備then方法)。
      這是一種js常見的類型檢測方法——鴨子類型檢測:

鴨子類型檢測:若是它看起來像鴨子,叫起來也像鴨子,那麼它就是鴨子

resolve/reject

      resolve返回一個當即成功的Promisereject返回一個當即失敗的Promise,他們是new Promise的語法糖,因此下面兩個寫法是等價的:

let p1 = new Promise(function(resolve, reject){
    reslove(11111)
})

let p2 = Promise.resolve(11111) //這和上面的寫法結果同樣
複製代碼

      此外,若是傳入reslove方法的參數不是promise而是一個thenable值,那麼reslove會將它展開。最終的決議值由then方法來決定。

錯誤處理

      上面提到,Promise是異步處理錯誤,也就是說個人錯誤要在下一個Promise才能捕獲到,大多狀況這是好的,可是存在一個問題:若是捕獲錯誤的代碼再出現錯誤呢?
      個人作法一般是在代碼的最後加catch

let p1 = new Promise(function(reslove, reject){
    ajax('xxxxx')
})

p1
    .then(fullfilled, rejected)
    .then(fullfilled, rejected)
    .catch(function(e){
        //處理錯誤
    })
複製代碼

結尾

      文章到這裏就結束了,若是你看完了而且所以思考了一些東西,我很高興。
      接下來會繼續更新Promise+generator、異步函數等Promise相關知識,願共同進步。

相關文章
相關標籤/搜索