ES6中一個非常重要和好用的特性就是Promise,但是初次接觸Promise會一臉懵逼,What the hell is this?看看官方或者一些文章對它的介紹和用法,也是一頭霧水。
Promise到底是做什麼的呢?Promise是異步編程的一種解決方案。
那什麼時候我們會碰到處理異步編程、異步事件的情況呢?
一種很常見的場景應該就是網絡請求了。
我們封裝一個網絡請求的函數,因爲不能立即拿到結果,所以不能像簡單的1+1=2一樣將結果返回(JavaScript代碼一般是從上到下依次執行,是同步過程,非異步;但是由於網絡請求本身需要時間,如果它也是同步的話,勢必代碼執行會出現阻塞的現象,即等待請求完成再繼續執行後面的代碼,所以網絡請求不是同步的而是異步的);
所以往往我們會以參數形式傳入另外一個函數到這個封裝的網絡請求函數,在數據請求成功或失敗時,將數據通過傳入的函數回調出去。
如果只是一個簡單的網絡請求,那麼這種方案不會給我們帶來很大的麻煩。但是,當網絡請求非常複雜時,就會出現“回調地獄”。
我們可以以一個非常誇張的案例來說明:
說明:
我們需要通過ajax傳入一個url1從服務器請求一個數據data1,但是data1中包含了下一個請求的url2;
所以我們需要通過data1取出url2,再從服務器請求數據data2,而data2中包含了下一個請求的url3;
那麼我們還需要通過data2取出url3……一直到發送網絡請求url4,獲取最終的數據data4。
正常情況下上面的代碼沒什麼問題,但是假如每個ajax請求之間存在其他代碼呢?這時是不是就開始複雜起來了,代碼變得不好看也不好維護。
使用Promise就是一種更加優雅的方式了。
Promise基本用法
我們以定時器來說明(定時器也是一種異步事件)。
說明:假設上面的“Hello World”就是我們從網絡上1秒後請求的data,console.log就是我們的處理方式。
如果使用Promise處理呢?
說明:
Promise是一個類,new Promise很明顯是創建一個Promise對象;
小括號中((resolve, reject) => {})也很明顯就是一個函數,這裏用的是箭頭函數。在創建Promise時,傳入的這個箭頭函數是固定的(一般我們都會這樣寫)。
但是resolve、reject它們是什麼呢?
resolve和reject它們兩個也是函數,通常情況下,我們會根據請求數據的成功和失敗來決定調用哪一個。
如果是成功的,那麼通常我們會調用resolve(messsage),這個時候,我們後續的then會被回調。
如果是失敗的,那麼通常我們會調用reject(error),這個時候,我們後續的catch會被回調。
OK,上面就是Promise最基本的使用了。只要存在異步操作,就可以使用Promise對這個異步操作進行封裝。
但是我們會想,這不是脫褲子放屁嗎?代碼還更多了。
其實不是的,雖然代碼更多了,但是結果卻變得很清晰,而且一個setTimeout明顯體現不出Promise的作用來。
如果異步事件數量較多,Promise的作用纔會真正體現出來:每個異步請求成功之後都可以去then裏面對結果做進一步處理,失敗的就去catch,也就是每個resolve都有對應的then、每個reject都有對應的catch,代碼管理和維護就更加方便了。
說明:這種方式也稱爲鏈式操作或鏈式調用。
Promise的另一種寫法
它還有另外一種簡便的寫法,也就是後面不使用catch,而是給then方法傳入第二個參數err(箭頭函數,錯誤信息的回調):
Promise的三種狀態
首先,當我們開發中有異步操作時,就可以給異步操作包裝一個Promise。
異步操作之後會有三種狀態。
我們看一下這三種狀態:
pending:等待狀態,比如正在進行網絡請求,或者定時器沒有到時間;
fulfilled:滿足狀態,當我們主動回調了resolve時,就處於該狀態,並且會回調.then();
rejected:拒絕狀態,當我們主動回調了reject時,就處於該狀態,並且會回調.catch()。
Promise的鏈式調用
從上面的代碼發現,都可以返回一個Promise對象:
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("aaa")
},1000)
}).then(res => {
return new Promise((resolve) =>{
resolve(res + "111")
}
}).then(res => {
console.log(res);
return new Promise((reject) =>{
reject("err message")
}
}).catch(err => {
console.log(err)
})
說明:箭頭函數中,resolve和reject兩個函數可以不用都傳人,它是可選的。
鏈式操作的簡寫形式
上面的鏈式調用可以簡寫,這裏我們直接通過Promise提供的方法resolve包裝了一下新的數據,將Promise對象返回了:
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("aaa")
},1000)
}).then(res => {
console.log(res);
return Promise.resolve(res + "111")
}).then(res => {
console.log(res);
return Promise.reject("err message")
}).catch(err => {
console.log(err)
})
Promise.resovle():將數據包裝成Promise對象,並且在內部回調resolve()函數
Promise.reject():將數據包裝成Promise對象,並且在內部回調reject()函數
如果我們希望數據直接包裝成Promise.resolve(),那麼在then中可以直接返回數據:
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("aaa")
},1000)
}).then(res => {
return res + "111"
})
執行結果是一樣的。
而Promise.reject()其實也可以改寫成下面的形式:
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("aaa")
},1000)
}).then(res => {
console.log(res);
return Promise.resolve(res + "111")
}).then(res => {
console.log(res);
throw "err message"
}).catch(err => {
console.log(err)
})
Promise.all()方法說明
all方法用於處理多個異步操作(比如網絡請求),只有多個異步操作都完成了纔在一個地方統一返回結果,才繼續接下來的操作。
這裏使用setTimeout來模擬多個異步的ajax網絡請求:
Promise.all([
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("result1")
},1000)
}),
new Promise((resolve,reject) => {
setTimeout(() => {
resolve("result2")
},2000)
})
]).then(results => {
console.log(results)
})
// 結果打印 ["result1","result2"]
說明:只有兩個請求都處理完成,纔在then裏對結果統一進行操作。當然,resolve裏可以傳入對象等其他類型,不僅僅是字符串。