異步編程誕生的原因
JavaScript 在 1992 年發(fā)布
這里致敬一下 JavaScript 主要?jiǎng)?chuàng)造者與架構(gòu)師,布蘭登·艾克
感謝“祖師爺”賞的飯碗
JS的單線程
JS設(shè)計(jì)的初衷是為了表單校驗(yàn)和 dom 操作
為了防止一個(gè)線程對(duì)dom操作時(shí),另一個(gè)線程刪除這個(gè)dom ,因此將其設(shè)計(jì)為單線程
單線程的優(yōu)缺點(diǎn)
單線程的模式有它的好處,但同時(shí)也帶來(lái)了問(wèn)題,那就是阻塞
- 同步運(yùn)行:?jiǎn)尉€程意味著兩段代碼不能同時(shí)運(yùn)行,而是必須逐步地運(yùn)行
- 阻塞操作:如果有非常耗時(shí)的任務(wù),會(huì)出現(xiàn)用戶長(zhǎng)時(shí)間等待,并且在當(dāng)前任務(wù)完成前,其他操作都無(wú)法響應(yīng)的情況
所以在同步代碼執(zhí)行過(guò)程中,需要將這類耗時(shí)的任務(wù)進(jìn)行異步處理
避免阻塞正常的邏輯執(zhí)行
JS異步編程的核心原理
JS異步編程的核心是 Event loop
在 Web 端和 Node 端各有不同
在這里不是主要內(nèi)容,簡(jiǎn)單描述一下
Web 端
Event loop 是由 HTML5 規(guī)范明確定義,由各大瀏覽器廠商各自實(shí)現(xiàn)的一套 JavaScript 在瀏覽器環(huán)境下的事件循環(huán)機(jī)制
Node端
Nodejs 的 Event loop 是基于 libuv,并且 libuv 已經(jīng)對(duì) Event loop 作出了實(shí)現(xiàn)
階段一:回調(diào)函數(shù)
最基本也是最原始的異步編程模式就是回調(diào)函數(shù)
// taks1 -> cb1 -> task2 -> cb2 -> task3 -> cb3
task1(function cb1() {
task2(function cb2() {
task3(function cb3() {
cb3()
})
})
})
回調(diào)函數(shù)給人一種什么樣的感覺(jué)?像什么?
像是俄羅斯套娃,大的套小的,隨著套娃越來(lái)越多。某一天,我突然想把最里面的一個(gè)拿出來(lái),這時(shí)候就絕望了。
這說(shuō)明伴隨著回調(diào)函數(shù)的嵌套增加,帶來(lái)了一些問(wèn)題,比如:
- 修改成本過(guò)高
- 當(dāng)我們需要修改回調(diào)函數(shù)時(shí),發(fā)現(xiàn)無(wú)從下手
- 局部作用域嵌套
- 由于使用函數(shù)嵌套,最內(nèi)層的函數(shù)擁有最大的作用域范圍。
- 極有可能不小心重定義或者修改了上層作用域的變量或函數(shù)
- 代碼混亂,不夠直觀
回調(diào)函數(shù)的嵌套問(wèn)題有一個(gè)統(tǒng)稱——回調(diào)地獄
為了解決回調(diào)地獄的問(wèn)題,到 ES6 發(fā)布,出現(xiàn)了第二種異步編程模式 Promise
階段二:Promise
Promise 是 ES6 中引入的新特性,與傳統(tǒng)回調(diào)函數(shù)寫法相比,有兩個(gè)區(qū)別:
- Promise 使用 then 函數(shù)進(jìn)行鏈?zhǔn)秸{(diào)用,不再是以往的那種嵌套結(jié)構(gòu)了
- 每個(gè) then 函數(shù)中的回調(diào)函數(shù)互相獨(dú)立,不再有作用域的干擾
將之前的例子改寫,可以看到代碼邏輯變得更加清晰
// taks1 -> then -> task2 -> then -> task3
task1()
.then(function() {
task2()
})
.then(function() {
task3()
})
但是 Promise 本身還是有一堆的 then 函數(shù),then函數(shù)中還是寫了一堆的回調(diào)函數(shù)
依舊不能讓我們像寫同步代碼一樣寫異步的代碼,更像是一個(gè)偽同步寫法
這個(gè)時(shí)候同樣伴隨 ES6 發(fā)布的 Generator 提供了一種思路
階段三:Generator
Generator 也是 ES6 引入的新特性,原本是為了實(shí)現(xiàn)一種新的狀態(tài)機(jī)制管理
為我們提供控制函數(shù)執(zhí)行階段的能力
function* task1() {
yield task2()
yield task3()
}
let result = task1() // task1
result.next() // task2 返回值
result.next() // task3 返回值
這段示例代碼向我們展示 Generator 的幾個(gè)特點(diǎn)
- 必須使用 * 來(lái)聲明
- 使用 yield 關(guān)鍵字,使得函數(shù)內(nèi)部寫法真正像是同步任務(wù)
- 可中斷執(zhí)行,但需要手動(dòng)執(zhí)行next,否則后續(xù)代碼不會(huì)執(zhí)行
但還不夠,我們看 Generator 有什么樣的問(wèn)題
- 繁瑣的 next 方法調(diào)用
- 晦澀難懂的函數(shù)語(yǔ)義,單純看 * 和 yield,誰(shuí)能明白它要干嘛
- 用 Generator 來(lái)進(jìn)行異步編程,不是開箱即用。Generator 本身和異步編程無(wú)關(guān),但在使用過(guò)程中發(fā)現(xiàn)在異步編程中有巨大的價(jià)值,基本需要進(jìn)行較為完善的二次封裝(增加執(zhí)行器),才能成為一種異步編程模式,例如 co 庫(kù)
npm install co
let co = require('co')
function* task1() {
yield task2()
yield task3()
}
co(task1())
我們希望它能夠更簡(jiǎn)單直接一點(diǎn),然后 Async/Await 隆重登場(chǎng)了
階段四:Async/Await
Async 是 ES8 中引入的新特性,是 Generator 的語(yǔ)法糖
可以近似的認(rèn)為是 Generator + 執(zhí)行器 + Promise 的封裝
同樣修改一下上面的例子
// task1 -> task2 -> task3
async task1() {
await task2()
await task3()
}
優(yōu)點(diǎn)很明顯:
- 語(yǔ)義化清晰明確:Async 異步,Await 等待,沒(méi)有歧義。其實(shí)大家也看的出來(lái),就是把 * 和 yield 換了一下
- 同步任務(wù)的寫法:這點(diǎn)上也沿用了 Generator 的設(shè)計(jì)
- 開箱即用:專門為異步編程設(shè)計(jì),不需要像 Generator 進(jìn)行二次封裝(執(zhí)行器)
- Async / Await 可以嵌套使用
- 隱式返回 Promise:可直接使用 Promise Api,進(jìn)行并發(fā)異步等模式的開發(fā)
綜合來(lái)看
Async/Await 是目前為止最完善的異步編程解決方案,解決了之前的痛點(diǎn)
總結(jié)
我們從時(shí)間線上來(lái)看 JavaScript 異步編程的演進(jìn)過(guò)程
ES6 以前,無(wú)論是事件監(jiān)聽、發(fā)布訂閱還是定時(shí)器,使用的還是原始的 Callback 方式
2015年 ES6 正式發(fā)布,同時(shí)將 Promise 和 Generator 引入標(biāo)準(zhǔn)。但是在社區(qū),Promise 和 Generator 都早有自己的雛形,Promise 的概念出現(xiàn)的時(shí)間相對(duì)而言還要更早一些。
所以在這條時(shí)間線上,我把他們兩個(gè)都定為 ES6 的正式發(fā)布的時(shí)間,但是 Promise 處在更早的時(shí)間節(jié)點(diǎn)
到了 2017 年 ES8 發(fā)布,Async 引入標(biāo)準(zhǔn),成為最新的解決方案,異步編程帶來(lái)的問(wèn)題告一段落。