什么是同步和異步?
你可能知道, JavaScript 語(yǔ)言 的執(zhí)行環(huán)境是“單線(xiàn)程”
所謂“單線(xiàn)程”, 就是指一次只能完成一件任務(wù), 如果有多個(gè)任務(wù), 就必須排隊(duì), 前面一個(gè)任務(wù)完成, 再執(zhí)行后面一個(gè)任務(wù), 以此類(lèi)推
例如現(xiàn)實(shí)生活中的排隊(duì)
這種模式的好處是實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單, 執(zhí)行環(huán)境相對(duì)單純, 壞處是只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng), 后面的任務(wù)都必須排隊(duì)等著, 會(huì)拖延整個(gè)程序的執(zhí)行
常見(jiàn)的瀏覽器無(wú)響應(yīng)(假死), 往往就是因?yàn)槟骋欢?JavaScript 代碼長(zhǎng)時(shí)間運(yùn)行(比如死循環(huán)), 導(dǎo)致整個(gè)頁(yè)面卡在這個(gè)地方, 其他任務(wù)無(wú)法執(zhí)行
為了解決這個(gè)問(wèn)題, JavaScript 語(yǔ)言將任務(wù)的執(zhí)行模式分成兩種
- 同步(Synchronous)
- 異步(Asynchronous)
這里的 “同步”和“異步” 與我們現(xiàn)實(shí)中的同步膜钓、異步恰恰相反
例如:
- 一邊吃飯一邊打電話(huà), 我們認(rèn)為這是同時(shí)進(jìn)行(同步執(zhí)行)的, 但在計(jì)算機(jī)中, 這種行為叫做異步執(zhí)行
- 吃飯的同時(shí), 必須吃完飯才能打電話(huà), 我們認(rèn)為這是不能同時(shí)進(jìn)行(異步執(zhí)行)的, 但在計(jì)算機(jī)中, 這種行為我們叫做同步執(zhí)行
至于為什么, 那你要問(wèn)英文單詞了, 例如 異步(Asynchronous) 翻譯成中文是異步的, 但在計(jì)算機(jī)中, 表示的是我們認(rèn)知的同時(shí)執(zhí)行的
什么時(shí)候我們需要異步處理事件?
- 一種很常見(jiàn)的場(chǎng)景自然就是網(wǎng)絡(luò)請(qǐng)求了
- 我們封裝一個(gè)網(wǎng)絡(luò)請(qǐng)求的函數(shù), 因?yàn)椴荒芰⒓茨玫浇Y(jié)果, 所以不能像簡(jiǎn)單的 3 + 4 = 7 一樣立刻獲得結(jié)果
- 所以我們往往會(huì)傳入另一個(gè)函數(shù) (回調(diào)函數(shù) callback), 在數(shù)據(jù)請(qǐng)求成功之后, 再將得到的數(shù)據(jù)以參數(shù)的形式傳遞給回調(diào)函數(shù)
JavaScript 和 Node.js 中的異步操作都會(huì)在最后執(zhí)行, 例如 ajax、readFile肠阱、writeFile、setTimeout 等
獲取異步操作的值只能使用回調(diào)函數(shù)的方式, 異步操作都是最后執(zhí)行
回調(diào)函數(shù)
回調(diào)函數(shù)的方式獲取異步操作內(nèi)的數(shù)據(jù)
function sum(a, b, callback) {
console.log(1)
setTimeout(function () {
callback(a + b)
}, 1000)
console.log(2)
}
sum(10, 20, function (res) {
console.log(res)
})
// log: 1 2 30
這種方式雖然看似沒(méi)什么問(wèn)題, 但是, 當(dāng)網(wǎng)絡(luò)請(qǐng)求非常復(fù)雜時(shí), 就會(huì)出現(xiàn)回調(diào)地獄
ok, 我們用一個(gè)非巢犯撸夸張的案例來(lái)說(shuō)明
$.ajax('url1', function (data1) {
$.ajax(data1['url2'], function (data2) {
$.ajax(data2['url3'], function (data3) {
$.ajax(data3['url4'], function (data4) {
console.log(data4)
})
})
})
})
- 我們需要通過(guò)一個(gè) url1 向服務(wù)器請(qǐng)求一個(gè)數(shù)據(jù) data1, data1 中又包含了下一個(gè)請(qǐng)求的 url2
- 我們需要通過(guò)一個(gè) url2 向服務(wù)器請(qǐng)求一個(gè)數(shù)據(jù) data2, data2 中又包含了下一個(gè)請(qǐng)求的 url3
- 我們需要通過(guò)一個(gè) url3 向服務(wù)器請(qǐng)求一個(gè)數(shù)據(jù) data3, data3 中又包含了下一個(gè)請(qǐng)求的 url4
- 發(fā)送網(wǎng)絡(luò)請(qǐng)求 url4, 獲取最終的數(shù)據(jù) data4
上面的代碼有什么問(wèn)題?
- 正常情況下, 不會(huì)有什么問(wèn)題, 可以正常運(yùn)行并且獲取我們想要的數(shù)據(jù)
- 但是, 這樣的代碼閱讀性非常差, 而且非常不利于維護(hù)
- 如果有多個(gè)異步同時(shí)執(zhí)行, 無(wú)法確認(rèn)他們的執(zhí)行順序, 所以通過(guò)嵌套的方式能保證代碼的執(zhí)行順序問(wèn)題
- 我們更加期望的是一種更加優(yōu)雅的方式來(lái)進(jìn)行這種異步操作
Promise
什么是 Promise ?
ES6 中有一個(gè)非常重要和好用的特性就是 Promise
Promise 到底是做什么的?
- Promise 是異步編程的一種解決方案, 比傳統(tǒng)的解決方案回調(diào)函數(shù)和事件更合理和更強(qiáng)大
所謂 Promise, 簡(jiǎn)單說(shuō)就是一個(gè)容器, 里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(通常是一個(gè)異步操作)的結(jié)果
為了解決回調(diào)地獄所帶來(lái)的問(wèn)題, ES6 里引進(jìn)了 Promise, 有了 Promise 對(duì)象, 就可以將異步操作以同步操作的流程表達(dá)出來(lái), 避免了層層嵌套的回調(diào)函數(shù)
Promise 對(duì)象提供統(tǒng)一的接口, 使得控制異步操作更加容易
Promise 的特點(diǎn)
Promise 對(duì)象有以下兩個(gè)特點(diǎn)
- 對(duì)象的狀態(tài)不受外界影響, Promise 對(duì)象代表一個(gè)異步操作, 有三種狀態(tài): pending(進(jìn)行中)倘屹、fulfill(已成功) 和 rejected(已失敗), 只有異步操作的結(jié)果, 可以決定當(dāng)前是哪一種狀態(tài), 任何其他操作都無(wú)法改變這個(gè)狀態(tài), 這也是 Promise 這個(gè)名字的由來(lái), 它的英語(yǔ)意思就是 “承諾”, 表示其他手段無(wú)法改變
- 一旦狀態(tài)改變, 就不會(huì)再變, 任何時(shí)候都可以得到這個(gè)結(jié)果, Promise 對(duì)象的狀態(tài)改變, 只有兩種可能: 從 pending 變?yōu)?fulfill 和 從 pending 變?yōu)?rejected, 只要這兩種情況發(fā)生, 狀態(tài)就凝固了, 不會(huì)再發(fā)生改變, 會(huì)一直保持這個(gè)結(jié)果, 這時(shí)就稱(chēng)為 resolved(已定型), 如果改變已經(jīng)發(fā)生了, 你再對(duì) Promise 對(duì)象添加回調(diào)函數(shù), 也會(huì)立即得到這個(gè)結(jié)果, 這與事件(Event)完全不同, 事件的特點(diǎn)是, 如果你錯(cuò)過(guò)了它, 再去監(jiān)聽(tīng), 是得不到結(jié)果的
Promise 的缺點(diǎn)
- 首先, 無(wú)法取消 Promise, 一旦新建它就會(huì)立即執(zhí)行, 無(wú)法中途取消
- 其次, 如果不設(shè)置回調(diào)函數(shù), Promise 內(nèi)部拋出的錯(cuò)誤, 不會(huì)反應(yīng)到外部
- 第三, 當(dāng)處于 pending 狀態(tài)時(shí), 無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開(kāi)始還是即將完成)
Promise 的三種狀態(tài)
- pending : 等待(wait)狀態(tài), 比如正在進(jìn)行網(wǎng)絡(luò)請(qǐng)求, 或者定時(shí)器沒(méi)有到時(shí)間
- fulfilled : 滿(mǎn)足狀態(tài), 當(dāng)我們主動(dòng)調(diào)用 resolve 時(shí), 就處于該狀態(tài), 并且回調(diào) .then()
- rejected : 拒絕狀態(tài), 當(dāng)我們主動(dòng)調(diào)用 reject 時(shí), 就處于該狀態(tài), 并且回調(diào) .catch()
Promise 基本用法
ES6 規(guī)定, Promise 對(duì)象是一個(gè)構(gòu)造函數(shù), 用來(lái)生成 Promise 實(shí)例
new Promise((resolve, reject) => {
// ... 某些異步代碼
if (/* 異步操作成功 */){
resolve(data); // data 里是異步執(zhí)行后的返回值
} else {
reject(error); // error 里是異步執(zhí)行錯(cuò)誤后的錯(cuò)誤信息
}
}).then(data => {
// 這里對(duì) data 就可以進(jìn)行數(shù)據(jù)拿取操作了
console.log('success')
}).catch(error => {
console.log('failure')
})
Promise 構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù), 該函數(shù)的兩個(gè)參數(shù)分別是 resolve 和 reject
它們是兩個(gè)函數(shù), 由 JavaScript 引擎提供, 不需要自己部署
resolve
- resolve 函數(shù)的作用是將 Promise 對(duì)象的狀態(tài)從 “未完成”變?yōu)椤俺晒Α?即從 pending 變?yōu)?fulfilled), 在異步操作成功時(shí)調(diào)用, 并將異步操作的結(jié)果, 作為參數(shù)傳遞出去
reject
- reject 函數(shù)的作用是將 Promise 對(duì)象的狀態(tài)從 “未完成”變?yōu)椤笆 ?即從 pending 變?yōu)?rejected), 在異步操作失敗時(shí)調(diào)用, 并將異步操作報(bào)出的錯(cuò)誤, 作為參數(shù)傳遞出去
then 方法還可以接受兩個(gè)回調(diào)函數(shù)作為參數(shù), 合并 .catch()
promise.then(data => {
// 這里對(duì) data 就可以進(jìn)行數(shù)據(jù)拿取操作了
console.log('success')
}, error => {
console.log('failure')
})
- 第一個(gè)回調(diào)函數(shù)是 Promise 對(duì)象的狀態(tài)變?yōu)?fulfilled 時(shí)調(diào)用
- 第二個(gè)回調(diào)函數(shù)是 Promise 對(duì)象的狀態(tài)變?yōu)?rejected 時(shí)調(diào)用
- 其中, 第二個(gè)回調(diào)函數(shù)是可選的, 不一定要提供, 這兩個(gè)函數(shù)都接受Promise 對(duì)象傳出的值作為參數(shù)
一般來(lái)說(shuō), 調(diào)用 resolve 或 reject 以后, Promise 的使命就完成了, 后繼操作應(yīng)該放到 then 方法里面, 而不應(yīng)該直接寫(xiě)在 resolve 或 reject 的后面
所以, 最好在將它們加上 return 語(yǔ)句, 這樣就不會(huì)有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的語(yǔ)句不會(huì)執(zhí)行
console.log(2);
})
Promise 鏈?zhǔn)秸{(diào)用
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success2')
}, 1000)
})
}).then(res => {
console.log(res) // success2
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success3')
}, 1000)
})
}).then(res => {
console.log(res) // success3
})
Promise 鏈?zhǔn)秸{(diào)用簡(jiǎn)寫(xiě)
如果我們希望數(shù)據(jù)直接包裝成 Promise.resolve, 那么在 then 中可以直接返回?cái)?shù)據(jù)
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success1')
}, 1000)
}).then(res => {
console.log(res) // success1
return 'success2'
}).then(res => {
console.log(res) // success2
return 'success3'
}).then(res => {
console.log(res) // success3
})
Promise.prototype.finally()
finally()
方法用于指定不管 Promise 對(duì)象最后狀態(tài)如何, 都會(huì)執(zhí)行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中, 不管promise
最后的狀態(tài), 在執(zhí)行完then
或catch
指定的回調(diào)函數(shù)以后, 都會(huì)執(zhí)行finally
方法指定的回調(diào)函數(shù)
finally
方法的回調(diào)函數(shù)不接受任何參數(shù), 這意味著沒(méi)有辦法知道前面的 Promise 狀態(tài)到底是fulfilled
還是rejected
, 這表明, finally
方法里面的操作, 應(yīng)該是與狀態(tài)無(wú)關(guān)的, 不依賴(lài)于 Promise 的執(zhí)行結(jié)果
Promise.all()
Promise.all()
方法用于將多個(gè) Promise 實(shí)例, 包裝成一個(gè)新的 Promise 實(shí)例
const p = Promise.all([p1, p2])
上面代碼中, Promise.all()
方法接受一個(gè)數(shù)組作為參數(shù), p1
记劝、p2
都是 Promise 實(shí)例, Promise.all()
方法的參數(shù)可以不是數(shù)組, 但必須具有 Iterator 接口, 且返回的每個(gè)成員都是 Promise 實(shí)例
p
的狀態(tài)由p1
、p2
決定, 分成兩種情況
- 只有
p1
吕粹、p2
的狀態(tài)都變成fulfilled
,p
的狀態(tài)才會(huì)變成fulfilled
, 此時(shí)p1
种柑、p2
的返回值組成一個(gè)數(shù)組, 傳遞給p
的回調(diào)函數(shù) - 只要
p1
、p2
之中有一個(gè)被rejected
,p
的狀態(tài)就變成rejected
, 此時(shí)第一個(gè)被reject
的實(shí)例的返回值, 會(huì)傳遞給p
的回調(diào)函數(shù)
/* 兩個(gè)異步操作狀態(tài)都為 fulfilled */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})
var p2 = new Promise((resolve, reject) => {
resolve('request2')
})
Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', 'request2']
.catch(e => console.log(e))
/* 其中有一個(gè)異步操作狀態(tài)為 rejected */
var p1 = new Promise((resolve, reject) => {
resolve('request1')
})
var p2 = new Promise((resolve, reject) => {
reject('request2 error')
})
Promise.all([p1, p2])
.then(res => console.log(res))
.catch(e => console.log(e)) // 'request2 error'
注意, 如果作為參數(shù)的 Promise 實(shí)例, 自己定義了catch
方法, 那么它一旦被rejected
, 并不會(huì)觸發(fā)Promise.all()
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('request1')
})
const p2 = new Promise((resolve, reject) => {
throw new Error('報(bào)錯(cuò)了')
}).catch(e => e)
Promise.all([p1, p2])
.then(res => console.log(res)) // ['request1', Error: 報(bào)錯(cuò)了]
.catch(e => console.log(e))
上面代碼中, p1 會(huì) resolved, p2 首先會(huì) rejected, 但是 p2 有自己的catch
方法, 該方法返回的是一個(gè)新的 Promise 實(shí)例, p2 指向的實(shí)際上是這個(gè)實(shí)例
該實(shí)例執(zhí)行完catch
方法后, 也會(huì)變成 resolved, 導(dǎo)致Promise.all()
方法參數(shù)里面的兩個(gè)實(shí)例都會(huì)resolved, 因此會(huì)調(diào)用then
方法指定的回調(diào)函數(shù), 而不會(huì)調(diào)用catch
方法指定的回調(diào)函數(shù)
如果 p2 沒(méi)有自己的catch
方法, 就會(huì)調(diào)用Promise.all()
的catch
方法
Promise.race()
Promise.race()
方法同樣是將多個(gè) Promise 實(shí)例, 包裝成一個(gè)新的 Promise 實(shí)例
const p = Promise.race([p1, p2])
只要
p1
匹耕、p2
之中有一個(gè)實(shí)例率先改變狀態(tài),p
的狀態(tài)就跟著改變那個(gè)率先改變的 Promise 實(shí)例的返回值, 就傳遞給
p
的回調(diào)函數(shù)Promise.race()
方法的參數(shù)與Promise.all()
方法一樣
下面是一個(gè)例子
/* 第一個(gè)異步操作率先完成, 并且狀態(tài)為 fulfilled */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 2000)
})
])
.then(res => console.log(res)) // request success
.catch(e => console.log(e))
/* 第二個(gè)異步操作先完成, 并且狀態(tài)為 rejected */
Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('request success')
}, 1000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject('request timeout')
}, 500)
})
])
.then(res => console.log(res))
.catch(e => console.log(e)) // request timeout