ES6中的異步詳解
眾所周知JS是單線程的,這種設(shè)計(jì)讓JS避免了多線程的各種問題,但同時(shí)也讓JS同一時(shí)刻只能執(zhí)行一個(gè)任務(wù),若這個(gè)任務(wù)執(zhí)行時(shí)間很長(zhǎng)的話(如死循環(huán)),會(huì)導(dǎo)致JS直接卡死,在瀏覽器中的表現(xiàn)就是頁面無響應(yīng),用戶體驗(yàn)非常之差。
因此,在JS中有兩種任務(wù)執(zhí)行模式:同步(Synchronous)和異步(Asynchronous)。類似函數(shù)調(diào)用、流程控制語句、表達(dá)式計(jì)算等就是以同步方式運(yùn)行的,而異步主要由setTimeout/setInterval
、事件實(shí)現(xiàn)。
傳統(tǒng)的異步實(shí)現(xiàn)
作為一個(gè)前端開發(fā)者,無論是瀏覽器端還是Node,相信大家都使用過事件吧,通過事件肯定就能想到回調(diào)函數(shù),它就是實(shí)現(xiàn)異步最常用、最傳統(tǒng)的方式。
不過要注意,不要以為回調(diào)函數(shù)就都是異步的,如ES5的數(shù)組方法Array.prototype.forEach((ele) => {})
等等,它們也是同步執(zhí)行的。回調(diào)函數(shù)只是一種處理異步的方式,屬于函數(shù)式編程中高階函數(shù)的一種,并不只在處理異步問題中使用。
舉個(gè)栗子?:
// 最常見的ajax回調(diào)
this.ajax('/path/to/api', {
params: params
}, (res) => {
// do something...
})
你可能覺得這樣并沒有什么不妥,但是若有多個(gè)ajax或者異步操作需要依次完成呢?
this.ajax('/path/to/api', {
params: params
}, (res) => {
// do something...
this.ajax('/path/to/api', {
params: params
}, (res) => {
// do something...
this.ajax('/path/to/api', {
params: params
}, (res) => {
// do something...
})
...
})
})
回調(diào)地獄就出現(xiàn)了。。。?
為了解決這個(gè)問題,社區(qū)中提出了Promise方案,并且該方案在ES6中被標(biāo)準(zhǔn)化,如今已廣泛使用。
Promise
使用Promise的好處就是讓開發(fā)者遠(yuǎn)離了回調(diào)地獄的困擾,它具有如下特點(diǎn):
- 對(duì)象的狀態(tài)不受外界影響:
- Promise對(duì)象代表一個(gè)異步操作,有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失?。?。
- 只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個(gè)狀態(tài)。
- 一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。
- Promise對(duì)象的狀態(tài)改變,只有兩種可能:從Pending變?yōu)镽esolved和從Pending變?yōu)镽ejected。
- 只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會(huì)再變了,會(huì)一直保持這個(gè)結(jié)果。
- 如果改變已經(jīng)發(fā)生了,你再對(duì)Promise對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。
- 這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過了它,再去監(jiān)聽,是得不到結(jié)果的。
- 一旦聲明Promise對(duì)象(new Promise或Promise.resolve等),就會(huì)立即執(zhí)行它的函數(shù)參數(shù),若不是函數(shù)參數(shù)則不會(huì)執(zhí)行
this.ajax('/path/to/api', {
params: params
}).then((res) => {
// do something...
return this.ajax('/path/to/api', {
params: params
})
}).then((res) => {
// do something...
return this.ajax('/path/to/api', {
params: params
})
})
...
看起來就直觀多了,就像一個(gè)鏈條一樣將多個(gè)操作依次串了起來,再也不用擔(dān)心回調(diào)了~?
同時(shí)Promise還有許多其他API,如Promise.all
、Promise.race
、Promise.resolve/reject
等等(可以參考阮老師的文章),在需要的時(shí)候配合使用都是極好的。
API無需多說,不過這里我總結(jié)了一下自己之前使用Promise踩到的坑以及我對(duì)Promise理解不夠透徹的地方,希望也能幫助大家更好地使用Promise:
1.then的返回結(jié)果:我之前天真的以為then
要想鏈?zhǔn)秸{(diào)用,必須要手動(dòng)返回一個(gè)新的Promise才行
Promise.resolve('first promise')
.then((data) => {
// return Promise.resolve('next promise')
// 實(shí)際上兩種返回是一樣的
return 'next promise'
})
.then((data) => {
console.log(data)
})
總結(jié)如下:
- 如果
then
方法中返回了一個(gè)值,那么返回一個(gè)“新的”resolved的Promise,并且resolve回調(diào)函數(shù)的參數(shù)值是這個(gè)值 - 如果
then
方法中拋出了一個(gè)異常,那么返回一個(gè)“新的”rejected狀態(tài)的Promise - 如果
then
方法返回了一個(gè)未知狀態(tài)(pending)的Promise新實(shí)例,那么返回的新Promise就是未知狀態(tài) - 如果
then
方法沒有返回值時(shí),那么會(huì)返回一個(gè)“新的”resolved的Promise,但resolve回調(diào)函數(shù)沒有參數(shù)
2.一個(gè)Promise可設(shè)置多個(gè)then回調(diào),會(huì)按定義順序執(zhí)行,如下
const p = new Promise((res) => {
res('hahaha')
})
p.then(console.log)
p.then(console.warn)
這種方式與鏈?zhǔn)秸{(diào)用不要搞混,鏈?zhǔn)秸{(diào)用實(shí)際上是then方法返回了新的Promise,而不是原有的,可以驗(yàn)證一下:
const p1 = Promise.resolve(123)
const p2 = p1.then(() => {
console.log(p1 === p2)
// false
})
3.then
或catch
返回的值不能是當(dāng)前promise本身,否則會(huì)造成死循環(huán):
const promise = Promise.resolve()
.then(() => {
return promise
})
4.then
或者catch
的參數(shù)期望是函數(shù),傳入非函數(shù)則會(huì)發(fā)生值穿透:
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 1
5.process.nextTick
和promise.then
都屬于microtask,而setImmediate
、setTimeout
屬于macrotask
process.nextTick(() => {
console.log('nextTick')
})
Promise.resolve()
.then(() => {
console.log('then')
})
setImmediate(() => {
console.log('setImmediate')
})
console.log('end')
// end nextTick then setImmediate
有關(guān)microtask及macrotask可以看這篇文章,講得很細(xì)致。
但Promise也存在弊端,那就是若步驟很多的話,需要寫一大串.then()
,盡管步驟清晰,但是對(duì)于我們這些追求極致優(yōu)雅的前端開發(fā)者來說,代碼全都是Promise的API(then
、catch
),操作的語義太抽象,還是讓人不夠滿意呀~
Generator
Generator是ES6規(guī)范中對(duì)協(xié)程的實(shí)現(xiàn),但目前大多被用于異步模擬同步上了。
執(zhí)行它會(huì)返回一個(gè)遍歷器對(duì)象,而每次調(diào)用next
方法則將函數(shù)執(zhí)行到下一個(gè)yield
的位置,若沒有則執(zhí)行到return或末尾。
依舊是不再贅述API,對(duì)它還不了解的可以查閱阮老師的文章。
通過Generator實(shí)現(xiàn)異步:
function* main() {
const res = yield getData()
console.log(res)
}
// 異步方法
function getData() {
setTimeout(() => {
it.next({
name: 'yuanye',
age: 22
})
}, 2000)
}
const it = main()
it.next()
先不管下面的next
方法,單看main
方法中,getData
模擬的異步操作已經(jīng)看起來很像同步了。但是追求完美的我們肯定是無法忍受每次還要手動(dòng)調(diào)用next
方法來繼續(xù)執(zhí)行流程的,為此TJ大神為社區(qū)貢獻(xiàn)了co模塊來自動(dòng)化執(zhí)行Generator,它的實(shí)現(xiàn)原理非常巧妙,源碼只有短短的200多行,感興趣可以去研究下。
const co = require('co')
co(function* () {
const res1 = yield ['step-1']
console.log(res1)
// 若yield后面返回的是promise,則會(huì)等待它resolved后繼續(xù)執(zhí)行之后的流程
const res2 = yield new Promise((res) => {
setTimeout(() => {
res('step-2')
}, 2500)
})
console.log(res2)
return 'end'
}).then((data) => {
console.log('end: ' + data)
})
這樣就讓異步的流程完全以同步的方式展示出來啦?~
Async/Await
ES7標(biāo)準(zhǔn)中引入的async函數(shù),是對(duì)js異步解決方案的進(jìn)一步完善,它有如下特點(diǎn):
- 內(nèi)置執(zhí)行器:不用像generator那樣反復(fù)調(diào)用next方法,或者使用co模塊,調(diào)用即會(huì)自動(dòng)執(zhí)行,并返回結(jié)果
- 返回Promise:generator返回的是iterator對(duì)象,因此還不能直接用
then
來指定回調(diào) - await更友好:相比co模塊約定的generator的yield后面只能跟promise或thunk函數(shù)或者對(duì)象及數(shù)組,await后面既可以是promise也可以是任意類型的值(Object、Number、Array,甚至Error等等,不過此時(shí)等同于同步操作)
進(jìn)一步說,async函數(shù)完全可以看作多個(gè)異步操作,包裝成的一個(gè)Promise對(duì)象,而await命令就是內(nèi)部then命令的語法糖。
改寫后代碼如下:
async function testAsync() {
const res1 = await new Promise((res) => {
setTimeout(() => {
res('step-1')
}, 2000)
})
console.log(res1)
const res2 = await Promise.resolve('step-2')
console.log(res2)
const res3 = await new Promise((res) => {
setTimeout(() => {
res('step-3')
}, 2000)
})
console.log(res3)
return [res1, res2, res3, 'end']
}
testAsync().then((data) => {
console.log(data)
})
這樣不僅語義還是流程都非常清晰,即便是不熟悉業(yè)務(wù)的開發(fā)者也能一眼看出哪里是異步操作。
總結(jié)
本文匯總了當(dāng)前主流的JS異步解決方案,其實(shí)沒有哪一種方法最好或不好,都是在不同的場(chǎng)景下能發(fā)揮出不同的優(yōu)勢(shì)。而且目前都是Promise與其他兩個(gè)方案配合使用的,所以不存在你只學(xué)會(huì)async/await或者generator就可以玩轉(zhuǎn)異步。沒準(zhǔn)以后又會(huì)出現(xiàn)一個(gè)新的方案,將已有的這幾種方案顛覆呢 ~
說實(shí)話,學(xué)過后端的人玩JavaScript會(huì)陷入一種困境,如果讓程序員自己處理可能會(huì)更符合邏輯,比如引入線程之類的,不過優(yōu)化起來又是一個(gè)問題了。。。
來源:https://blog.markeyme.cn/2018/06/09/ES6%E5%BC%82%E6%AD%A5%E6%96%B9%E5%BC%8F%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90/