之前在業(yè)務(wù)開(kāi)發(fā)中使用promise時(shí),大部分情況都是聲明一個(gè)返回promise實(shí)例的函數(shù),調(diào)用時(shí)在then方法里傳入一個(gè)包含resolve,reject的回調(diào)函數(shù),像下面這樣:
function promiseFn () {
return new Promise((resolve, reject)=>{
//異步函數(shù) ...
if (data) {
resolve(data)
} else {
reject()
}
})
}
promiseFn.then((data)=>{
// ...
}, (err)=>{
// ...
})
但是對(duì)于以下幾點(diǎn)一直沒(méi)有好好理解:
1、相對(duì)于回調(diào)函數(shù),promise的優(yōu)勢(shì)是什么
2、then方法的鏈?zhǔn)秸{(diào)用
3、如何理解 “一旦狀態(tài)改變,就不會(huì)在變,任何時(shí)候都可以得到這個(gè)結(jié)果”
后來(lái)讀到了一篇關(guān)于promise的文章,JavaScript Promise:簡(jiǎn)介,對(duì)promise的理解又有了更深的體會(huì),記錄一下。
一、與回調(diào)函數(shù)的比較
1、捕獲錯(cuò)誤
這里的捕獲錯(cuò)誤指的是處理異步操作的函數(shù)promiseFn發(fā)生了未知錯(cuò)誤,例如某個(gè)變量未定義,這個(gè)時(shí)候promise.catch可以捕獲到這個(gè)錯(cuò)誤,如果是回調(diào)函數(shù)需要使用try ... catch
2、回調(diào)函數(shù)的嵌套
假設(shè)有這樣一個(gè)場(chǎng)景,想在頁(yè)面中打印一篇文章,文章的每個(gè)段落都是通過(guò)接口獲取,所有段落的請(qǐng)求地址,又是通過(guò)另一個(gè)接口下發(fā),模擬請(qǐng)求數(shù)據(jù)如下:
let data = {
story: ['title1', 'title2', 'title3', 'title4', 'title5', 'title6'],
title1: '第一段段落,第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落',
title2: '第二段段落,第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落',
title3: '第三段段落,第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落',
title4: '第四段段落,第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落',
title5: '第五段段落,第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落',
title6: '第六段段落,第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落'
}
function getData (key, callBack) {
// ...根據(jù)key值,異步獲取段落數(shù)據(jù), 不同的key值,返回對(duì)應(yīng)的段落數(shù)據(jù)
setTimeout(()=>{
//content模擬異步獲取數(shù)據(jù)成功
callBack(data[key])
}, 1000)
}
我們用“key”代替請(qǐng)求的url,setTimeout代替請(qǐng)求的異步行為,由于文章是有順序的,所以我們要按照順序請(qǐng)求,即第一段請(qǐng)求回來(lái)后,再請(qǐng)求第二段,第二段請(qǐng)求成功后,再請(qǐng)求第三段...
getData('story', (data)=>{
getData(data[0], (content)=>{
console.log(content)
getData(data[1], (content)=>{
console.log(content)
// 以此類推,一直到獲取到最后一個(gè)段落
//... getcontent(data[n], (content)=>{
//})
})
})
})
這種地獄式的回調(diào)應(yīng)該不是我們大家所接受的,promise卻能很好的解決這種嵌套問(wèn)題
二、then方法的鏈?zhǔn)秸{(diào)用
在學(xué)習(xí)promise的時(shí)候我們大家都知道,then()會(huì)返回一個(gè)新的promise實(shí)例,套用在我們這個(gè)例子中
let data = {
story: ['title1', 'title2', 'title3', 'title4', 'title5', 'title6'],
title1: '第一段段落,第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落第一段段落',
title2: '第二段段落,第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落第二段段落',
title3: '第三段段落,第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落第三段段落',
title4: '第四段段落,第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落第四段段落',
title5: '第五段段落,第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落第五段段落',
title6: '第六段段落,第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落第六段段落'
}
let time = {
story: 1000,
title1: 3000,
title2: 2000,
title3: 1000,
title4: 4000,
title5: 5000,
title6: 6000,
}
var getData = function (key) {
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve(data[key])
}, time[key])
})
}
getData('story').then((titleLists)=>{
// 1秒后,第一個(gè)then方法返回 一個(gè)數(shù)組 ['title1', 'title2', 'title3', 'title4', 'title5', 'title6']
return titleLists
})
.then((titleLists)=>{
// 1秒后,第二個(gè)then方法的回調(diào)拿到第一個(gè)then方法返回的titleLists
return getData(titleLists[0]).then((content)=>{
console.log(content)
})
})
then方法的resolve回調(diào)可以返回一個(gè)基本類型的值,也可以返回一個(gè)promise實(shí)例
1、return 一個(gè)基本類型的數(shù)據(jù)
上面代碼中,第一個(gè)then方法返回的promise實(shí)例會(huì)拿到自己resolve回調(diào)返回的數(shù)據(jù)(titleLists數(shù)組),作為入?yún)鬟f給自己的回調(diào)函數(shù)
2、return 一個(gè)promsie對(duì)象
當(dāng)回調(diào)函數(shù)返回一個(gè)新的promise實(shí)例時(shí)
getData('story').then((titleLists)=>{
//第一個(gè)then方法返回 一個(gè)promise實(shí)例
return getData(titleLists[0])
})
.then((content)=>{
// 4秒后 第二個(gè)then方法的回調(diào)會(huì)拿到新promise實(shí)例狀態(tài)改變后返回的數(shù)據(jù)
console.log(content)
})
上面代碼中,第一個(gè)then方法的resolve回調(diào)返回的是一個(gè)promise實(shí)例,第二個(gè)then方法會(huì)等待這個(gè)新的promise實(shí)例狀態(tài)改變后再執(zhí)行。
總之,我對(duì)這里的理解是,then方法返回的promise實(shí)例的執(zhí)行狀態(tài),依賴它前一個(gè)promise的執(zhí)行狀態(tài),前一個(gè)promise狀態(tài)改變后,當(dāng)前的promise開(kāi)始執(zhí)行;如果前一個(gè)promise回調(diào)中 返回了一個(gè)新的promise實(shí)例,那么當(dāng)前的promise實(shí)例又會(huì)依賴這個(gè)新的promise實(shí)例,一直等待最終返回的promise實(shí)例狀態(tài)改變后,才開(kāi)始執(zhí)行。
通過(guò)這個(gè)例子再來(lái)理解下“一旦狀態(tài)改變,就不會(huì)在變,任何時(shí)候都可以得到這個(gè)結(jié)果”這句話
三、狀態(tài)改變后,任何時(shí)候都可以得到這個(gè)結(jié)果
先來(lái)看下面這段代碼
let storyPromise = null
function getContent (index) {
storyPromise = storyPromise || getData('story')
return storyPromise.then((titleLists)=>{
return getData(titleLists[index])
})
}
getContent(0).then((content)=>{
// 4秒后執(zhí)行,1秒獲取story數(shù)據(jù),3秒獲取第一段落
console.log(content)
return getContent(1)
})
.then((content)=>{
//6秒后執(zhí)行 4秒獲取第一段落,2秒獲取第二段落,沒(méi)有重新獲取story
console.log(content)
})
獲取第一段段落時(shí),還沒(méi)有獲取到titleLists,先獲取titleLists,再獲取第一段段落,總共用時(shí)4秒;當(dāng)獲取第二段段落時(shí),由于storyPromise這個(gè)狀態(tài)已經(jīng)改變,執(zhí)行storyPromise的then方法時(shí),會(huì)直接返回結(jié)果,即立刻執(zhí)行resolve,沒(méi)有再次請(qǐng)求story,所以2秒后打印第二段段落。
四、使用promise
下面我們用promise實(shí)現(xiàn)打印文章的功能
getData('story')會(huì)返回一個(gè)數(shù)組,循環(huán)遍歷這個(gè)數(shù)組去請(qǐng)求段落數(shù)據(jù)
getData('story').then((titleLists)=>{
titleLists.forEach((title)=>{
getData(title).then(content=>console.log(content))
})
})
這種方式有一個(gè)問(wèn)題就是,所有的段落請(qǐng)求是在同一時(shí)間發(fā)出的,哪個(gè)請(qǐng)求用時(shí)短就會(huì)先打印哪個(gè)段落,所以第三段先打印,我們希望的是按順序打印,要先打印第一段,第二段,第三段...
所以,需要將每次循環(huán)獲取段落的行為,變?yōu)橐粋€(gè)promise,代碼如下:
var sequence = Promise.resolve();
getData('story').then((titleLists) => {
titleLists.forEach((title)=>{
sequence = sequence.then(()=>{
return getData(title)
}).then((content)=>{
console.log(content)
})
})
})
這里通過(guò)循環(huán)連續(xù)給sequence賦值6次,每次賦值都是一個(gè)promise,且都是前一個(gè)循環(huán)返回的promise,這樣就將獲取六段內(nèi)容的行為串起來(lái)了,4秒后打印第一段,6秒后打印第二段...
可以用reduce的優(yōu)化下,省掉了聲明sequence
getData('story').then((titleLists)=>{
titleLists.reduce((sequence, title) => {
return sequence.then(()=>{
return getData(title)
}).then((content)=>{
console.log(content)
})
}, Promise.resolve())
})
這種寫(xiě)法有一個(gè)問(wèn)題就是所有請(qǐng)求都是串行的,等第六段下載完成需要22秒了,這里還可以怎么優(yōu)化呢?
可以將獲取所有段落的請(qǐng)求同時(shí)發(fā)出,由于“一旦狀態(tài)改變,就不會(huì)在變,任何時(shí)候都可以得到這個(gè)結(jié)果”,所以,只對(duì)打印段落做一個(gè)promise的串行處理即可,請(qǐng)求段落提前執(zhí)行,代碼如下:
getData('story').then((titleLists)=>{
titleLists.map(getData).reduce((sequence, titlePromise) => {
return sequence.then(()=>{
return titlePromise
}).then((content)=>{
console.log(content)
})
}, Promise.resolve())
})
這里先將所有獲取段落的請(qǐng)求變?yōu)橐粋€(gè)promise數(shù)組,再調(diào)用reduce,這樣的好處是在對(duì)sequence賦值的時(shí)候,獲取“title”的請(qǐng)求可能已經(jīng)執(zhí)行完了(即 titlePromise的狀態(tài)已經(jīng)改變),優(yōu)化后,第六段下載完成只需要6秒。
原文講的更加詳情 JavaScript Promise:簡(jiǎn)介
關(guān)于promise的定義、用法推薦阮一峰老師的文章 es6 promise