這樣你都不懂Promise,算我輸!

viyoung發表於2018-07-25

一、Promise的身世

1、Promise歷史

Promise 是非同步程式設計的一種解決方案,比我們傳統的回撥函式事件更加合理,最早由社群提出並實現,ES6覺得很ok,就寫進了語言標準中,統一了語法,並且提供了原生的Promise物件。

所謂的Promise,其實就是一個容器,裡面儲存著未來才會結束的事件(通常是一個非同步操作)的結果,從javascript的語法上講就是一個物件,從裡面能獲取非同步操作的資訊,Promise提供統一的api,各種非同步操作都可以使用同樣的方法來進行處理。

2、Promise的特點

特點一:Promise物件的狀態不受外界影響。代表著一個非同步操作,有三種狀態:Pending(進行中)、Fulfilled(已成功)和Reject(已失敗)。只有非同步操作的結果才可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態,這也就是Promise的由來。“承諾”,就是其他手段無法更改。

特點二:一旦狀態改變了,就不會再改變。任何時候都可以得到這個結果。Promise物件的狀態的改變只會是兩種可能:從Pending變成Fulfilled和Pending變為Rejected。這要這兩種情況發生,狀態就凝固了,不會再改變了,而是一直保持這樣一個結果這時候就成Resolved(已定型)。就算改變已經發生,再對Promise物件新增回撥函式,也會立即得到這個結果。這與事件(event)完全不同,事件的特點我們都知道,如果錯過它,就監聽不到結果的。

二、基本用法

ES6規定,Promise物件是一個構造給函式,是用來生成Promise例項的。

let Promise=new Promise(function(resolve,reject){
/*邏輯程式碼*/
    if(/*非同步操作成功*/){
        resolve(value)
    }else{
        reject(error)
    }
})
複製程式碼

Promise建構函式式會接受兩個引數的,該函式的兩個引數是resolve和reject,是javascript引擎提供的,不用自己部署。

resolve函式的作用是,將Promise物件的狀態從“未完成”到“成功”(就是從Pending變為Resolved),在非同步操作成功的時候,並將非同步操作的結果作為引數傳遞進去;reject函式的作用是將Promise物件的狀態從“未完成”變成為“失敗”(即從Pending變為Rejected),在非同步操作失敗的時候呼叫,將非同步錯誤傳遞出去。

Promise例項生成以後,可以用then方法分別指定Resolve狀態和Reject狀態的回撥函式。

let Promise=new Promise(function(resolve,reject){})
Promise.then(function(value){
    //success
},function(error){
    //failuer
})
複製程式碼

then方法可以接受兩個回撥函式作為引數,第一個是Promise物件變成Resolved時呼叫,第二個是Promise物件的狀態變為Rejectd的時候呼叫,第二個函式式可選的,也可以不寫,這兩個函式都接受Promise物件傳出值作為引數。舉一個栗子吧:

function timeout(ms){
    return new Promise((resolve,reject)=>{
        setTimeout(resolve,ms,'done')
    })
}
timeout(100).then((value)=>{
    console.log(value)
})
複製程式碼

上面的程式碼,timeout方法會返回一個Promise的例項,一段時間以後才會發生的結果,過了指定時間(ms)以後,Promise例項就會變成Resolve,就會觸發then的方法繫結的回撥函式。

Promise初始化例項與以後就會立即執行,不妨看一下下面的列印驗證:

let promise=new Promise(function(resolve,reject){
    console.log("我是Promise")
    resolve()
})
promise.then(function(){
    comsole.log("我時候then的回撥")
})
console.log('hello,promise!')

//我是Promise
//hello,promise!
//我時候then的回撥
複製程式碼

上面的程式碼中,Promise例項化之後會立即執行,所以首先輸出的是Promise,然後,then方法指定的回撥函式將在當前指令碼所有同步指令碼執行完才會執行,所以上述程式碼中“我時候then的回撥”最後輸出。

舉一個頁面圖片非同步載入的栗子:

function loadImageAsync(url){
    retutn new Promise(function(resolve,reject){
        var image=new Image();
        image.onload=function(){
            resolve(image)
        }
        image.onerror=function(){
            reject(new Error('could not load image at'+url))
        }
        image.src=url
    })
    
}
複製程式碼

上面舉出了常用防止頁面阻塞非同步載入圖片的栗子,如果載入成功,就呼叫resolve方法,否則就呼叫reject方法。

如果不過癮的話,就簡單來實現一下當前單頁面比較火的Vue、React框架,開發所用的Http請求axios和fetch的基於Promise實現Ajax的過程:

let getJSON=function(url){
    let promise=new Promise(function(){
        let client=new XMLHttpRequest()
        client.open('GET',url)
        client.onreadystatechange=hander
        cilent.responseType='json'
        client.setRequestHeader('Accept',"application/json")
        client.send()
        function hander(){
            if(this.readystate!=4){
                return
            }
            if(this.statues===200){
                resolve(this.response)
            }else{
                reject(new Error(this.stautsText))
            }
        }
    })
    return promise
}

//使用
getJSON('/xxx/xx.json').then((json)=>{
    console.log('contents'+json)
},(error)=>{
    console.log("請求出錯"+error)
})
複製程式碼

上面的程式碼中,getJSON是對XMLHttpRequest物件基於promise的封裝,可以發出一個針對json資料的HTTP請求,並返回一個promise物件,可以看見getJSON內部resolve和reject函式都帶有引數,那麼這些引數會被傳遞給回撥函式,reject函式的引數通常是Error物件的例項,表示丟擲錯誤,resolve函式的引數除了正常的值外,還可以是另外一個promise例項,比方說:

let pro1=new Promise(function(resolve,reject){
    //....
})
let pro2=new Promise(function(resolve,reject){
    //...
    resolve(pro1)
})
複製程式碼

上面的程式碼中,pro1和pro2都是promise的例項,但是pro2的resolve方法將pro1作為引數,即一個非同步操作的結果返回另一個非同步操作結果。此時pro1的狀態就會傳遞給pro2,也就是說pro1決定了pro2的狀態,如果pro1狀態是pending,那麼pro2的回撥函式就會等待pro1的狀態已經resolve或者rejected,那麼pro2的回撥函式才立即執行。

三、Promise.prototype.then()

Promise例項具有then方法,即then方法是定義在原型物件Promise.prototype上的,作用是給Promise例項新增狀態改變時的回撥函式。then方法的第一個引數是Resolve狀態的回撥函式,第二個是Rejected(可選)狀態的回撥函式。

then方法返回的是一個新的Promise例項,不是原來那個Promise例項,因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

getJSON('xxx/xx.json').then(function(json){
    return json.post
}).tnen(function(post){
    
})
複製程式碼

採用鏈式的then可以指定一組按照次序呼叫的回撥函式,這時,前一個回撥函式有可能返回的還是一個非同步操作,即Promise例項,而後一個回撥函式會等待該Promise物件狀態改變才呼叫。

四、Promise.prototype.catch()

Promise.prototype.catch的方法是.then(null,rejection)的別名,用於指定發生錯誤的回撥函式。

舉個例子:

getJSON('xxx/xx.json').then(function(json){
   //...
}).catch(function(err){
    console.log('錯誤'+error)
})
複製程式碼

上面的程式碼,只要then中某一步非同步操作丟擲錯誤,狀態變成Rejected,都會被catch()所捕獲到,然後指定其回撥函式執行,處理這個錯誤。Promise物件的錯誤具有冒泡機制,會一直向後傳遞,一直到被catch()處理函式所捕獲,也就是說錯誤會被下一個catch語句捕獲。

getJSON('xxx/xx.json').then(function(json){
   //...some code1
}).then(function(json){
   //...some code2
}).catch(function(err){
    console.log('錯誤'+error)
})
複製程式碼

上面的程式碼中一共有四個Promise物件,getJSON產生,兩個then產生兩個,其中任何一個報錯,都會被最後一個catch所捕獲,一般使用的時候,注意不要在then()中定義Rejected回撥函式,官方建議總是使用catch方法處理錯誤異常回撥。

.catch()和傳統的try/catch不同是,如果沒有使用catch方法指定錯誤的的回撥函式,Peomise物件丟擲的錯是不會傳遞到外層程式碼的,也就是說try/catch是沒有反應的。

var errorNotCatch=function(){
    return new Promise((resolve,reject)=>{
        fs.readFile('././xxx',(err)=>{
         resolve(data)
        })
    }) 
}
errorNotCatch().then(data=>{
    console.log('good');
})
複製程式碼

上面的程式碼。是不會輸出good的try/catch 是捕捉不到錯誤的,但是瀏覽器會打出“data is not defined”

五、

小夥伴們,以上的基礎介紹,希望對你們在vue、react開發中的Promise實現的一些東西有一個更好的認識。

相關文章