背景
JavaScript是單線程工作,這意味著兩段腳本不能同時運(yùn)行,而且必須一個接一個的運(yùn)行。
其實(shí)JavaScript的單線程與它的用途有很大的關(guān)系,JavaScript作為瀏覽器腳本語言,主要實(shí)現(xiàn)與用戶的交互。利用JavaScript可以對DOM做各種各樣的操作。若JavaScript是多線程的話,一個線程在一個DOM節(jié)點(diǎn)中增加內(nèi)容,另一個線程要刪除這個DOM節(jié)點(diǎn)。那么這個DOM節(jié)點(diǎn)就很糾結(jié),這個DOM節(jié)點(diǎn)到底要增加內(nèi)容還是要刪除呢?因此JavaScript是單線程的。
同步任務(wù)與異步任務(wù)
由于JavaScript的單線程特性,因此同一時間只能處理同一個任務(wù),所有任務(wù)都需要排隊(duì),前一個任務(wù)執(zhí)行完,才可以執(zhí)行下一個任務(wù)。
但是如果前一個任務(wù)的執(zhí)行時間很長,比如說是文件的讀取操作或Ajax操作,后一個任務(wù)就不得不等待。就比如是Ajax,當(dāng)用戶向后臺獲取大量數(shù)據(jù)時,必須等到所有的數(shù)據(jù)都獲取完才能進(jìn)行下一步的操作,用戶就只能等待,嚴(yán)重影響用戶體驗(yàn)。
在JavaScript的設(shè)計(jì)之初就考慮到了這個問題。主線程可以完全不管設(shè)備I/O這種耗時的任務(wù),會掛起處于等待任務(wù);先運(yùn)行排在后面的任務(wù)。等到掛起的任務(wù)返回了接軌后,再對掛起的任務(wù)進(jìn)行后續(xù)處理。因此任務(wù)可以分為同步任務(wù)和異步任務(wù)。
- 同步任務(wù):同步任務(wù)指在主線程上排隊(duì)的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能繼續(xù)執(zhí)行下一個任務(wù)。例如WEB頁面的渲染過程就是一個同步任務(wù)。
- 異步任務(wù):異步任務(wù)是指不進(jìn)入主線程,而進(jìn)入任務(wù)隊(duì)列的任務(wù)。只有任務(wù)隊(duì)列通知主線程,某個異步任務(wù)可以執(zhí)行了。該任務(wù)才進(jìn)入主線程執(zhí)行。例如圖片、音樂資源的加載都是一個異步任務(wù)。
具體來說JavaScript任務(wù)的執(zhí)行機(jī)制如下:
1. 所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
2. 主線程之外,還存在一個“任務(wù)隊(duì)列”(task queue),只要異步任務(wù)有了運(yùn)行結(jié)果,就在“任務(wù)隊(duì)列”中放置一個事件。
3. 一旦“執(zhí)行?!敝械乃型饺蝿?wù)執(zhí)行完畢,系統(tǒng)就會讀取“任務(wù)隊(duì)列”,看看里邊有哪些事件。哪些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入“執(zhí)行?!遍_始執(zhí)行。
4. 主線程不斷重復(fù)上邊的第三步。
JavaScript的主線程和任務(wù)隊(duì)列示意圖:

JavaScript異步編程
在JavaScript中通常使用回調(diào)函數(shù)、Promise以及async/await的方式實(shí)現(xiàn)異步。
回調(diào)函數(shù)
回調(diào)函數(shù)是實(shí)現(xiàn)異步編程最簡單的方式。具體的做法是定義一個函數(shù),將這個函數(shù)綁定到事件上,當(dāng)觸發(fā)事件后會自動調(diào)用這個函數(shù),不需要主動去調(diào)用這個函數(shù),稱這個函數(shù)為回調(diào)函數(shù)。
例如:
var req = new XMLHttpReauest()
req.open("GET", url)
req.send(null)
req.onreadystatechange=function() {}
在onreadystatechange函數(shù)上綁定的這個函數(shù)就稱為是回調(diào)函數(shù)。
回調(diào)具體可以分為具名回調(diào)、匿名回調(diào)和回調(diào)地獄
具名回調(diào)
function getUserInfo(fn) {
fn.call(undefined, 'name: xxx')
}
function userinfo(info) {
console.log(info)
}
getUserInfo.call(undefined, userinfo)
// name: xxx
匿名回調(diào)
function getUserInfo(fn) {
fn.call(undefined, 'name: xxx')
}
getUserInfo.call(undefined, function(info) {
console.log(info)
})
多層嵌套的匿名回調(diào)(回調(diào)地獄)
function getUserInfo(fn) {
fn.call(undefined, 'name: xxx')
}
function saveUserInfo(fn) {
fn.call(undefined, 'name: xxx')
}
function getOtherUserInfo(fn) {
fn.call(undefined, 'name: xxx')
}
getUserInfo.call(undefined, function(info) {
console.log(info)
saveUserInfo.call(undefined, function() {
getOtherUserInfo.call(undefined, function() {
saveUserInfo.call(undefined, function() {
......
})
})
})
})
向上面這種多層匿名回調(diào)嵌套就很難讀懂和維護(hù),這種代碼就稱為回調(diào)地獄。
回調(diào)函數(shù)的優(yōu)點(diǎn)是寫法簡單,但是容易出現(xiàn)回調(diào)地獄。
Promise
Promise對象是CommonJS定義的一種規(guī)范,目的為異步編程提供統(tǒng)一的接口。
Promise包括以下幾個規(guī)范:
- 一個promise可能有三種狀態(tài):等待( pending )、已完成( fulfilled )和已拒絕( rejected )
- 一個promise的狀態(tài)只可能從
等待轉(zhuǎn)到完成或拒絕,不能逆向轉(zhuǎn)換,同時完成和拒絕不能相互轉(zhuǎn)換。 - promise必須實(shí)現(xiàn)一個
then方法,而且then方法必須返回一個promise,同一個promise的then可以調(diào)用多次,并且回調(diào)的執(zhí)行順序跟它們被定義時的順序一致。 -
then方法接受兩個參數(shù),第一個參數(shù)是成功時的回調(diào),在promise由等待轉(zhuǎn)換為完成時調(diào)用;另一個參數(shù)是失敗時的回調(diào),在promise由等待轉(zhuǎn)換為拒絕時調(diào)用。同時then可以接受另一個promise傳入,也接受一個類then的對象或方法。
function wait(time) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, time)
})
}
wait(1000).then(function() {
console.log(1)
})
async / await
從字面是理解async是異步的意思,而await是等待的意思。所以async用于聲明一個異步function,而await用于等待一個異步任務(wù)執(zhí)行完成的結(jié)果。
其中await只能出現(xiàn)在async函數(shù)中,async函數(shù)的返回值是一個promise對象。
function test() {
return new Promise(reslove => {
setTimeout(() => reslove("test"), 2000)
})
}
async function test2() {
const result = await test()
console.log(result)
}
test2()
console.log('end')
async的作用
通常情況下使用async命令是因?yàn)楹瘮?shù)內(nèi)部有await命令,因?yàn)閍wait命令只能出現(xiàn)在async函數(shù)里面,否則會報語法,這就是為什么async/await成對出現(xiàn)的原因,但是如果對一個普通函數(shù)單獨(dú)加個async會是什么結(jié)果呢?來看個例子:
async function test () {
let a = 2
return a
}
const res = test()
console.log(res)

可以看到async函數(shù)的返回是一個promise對象。如果函數(shù)有返回值,async會把這個返回值通過promise.resole()封裝成promise對象。通過then就可以將這個返回值取出來。
res.then(a => {
console.log(a) // 2
})
在沒有await的情況下async函數(shù)會立即執(zhí)行,并返回一個promise,那么加上await會有什么變化呢?
await的作用
一般情況下await命令后面接的是一個promise對象,等待promise對象狀態(tài)發(fā)生變化,得到返回值,但是也可以接任意表達(dá)式的返回結(jié)果,例如:
function a () {
return 'a'
}
async function b () {
return 'b'
}
const c = await a()
const d = await b()
console.log(c, d)
可以看到await后面不管接什么表達(dá)式,都可以等到結(jié)果的返回。當(dāng)?shù)鹊降牟皇莗romise對象時,就將等到結(jié)果返回,當(dāng)?shù)鹊降氖且粋€promise對象時,會阻塞后面的代碼,等待promiset對象狀態(tài)變化,得到對應(yīng)的值作為await等待的結(jié)果,這里的阻塞是指async內(nèi)部的阻塞,async函數(shù)的調(diào)用不會阻塞。
解決了什么問題
promise對象已經(jīng)解決了回調(diào)地獄的問題,那么為什么還要async/await呢?看下面一段代碼:
function login () {
return new Promise(resolve => {
resolve('aaa')
})
}
function getUserInfo (token) {
return new Promise(resolve => {
if (token) {
resolve({
isVip: true
})
}
})
}
function getVipGoods (userInfo) {
return new Promise(resolve => {
if (userInfo.isVip) {
resolve({
id: 'xxx',
price: 'xxx'
})
}
})
}
function showVipGoods (vipGoods) {
console.log(vipGoods.id + '----' + vipGoods.price)
}
login()
.then(token => getUserInfo(token))
.then(userInfo => getVipGoods(userInfo))
.then(vipGoods => showVipGoods(vipGoods))
上面的例子中,每一個promise都相對于是一個異步的網(wǎng)絡(luò)請求,通常一個業(yè)務(wù)流對應(yīng)了對個網(wǎng)絡(luò)請求,上面的例子描述了每個網(wǎng)絡(luò)請求都依賴前一個請求的結(jié)果的場景,下面采用async/awite重寫。
async function call() {
const token = await login()
const userInfo = await getUserInfo(token)
const vipGoods = await getVipGoods(userInfo)
showVipGoods(vipGoods)
}
call()
相比于promise/then語法結(jié)構(gòu),使用async/await的調(diào)用更加清晰,和同步代碼一樣。
帶來的問題
使用async/await會因?yàn)橥綀?zhí)行造成時間的積累,導(dǎo)致程序變慢。本質(zhì)上async/await將并發(fā)執(zhí)行的任務(wù)變?yōu)榱死^發(fā)。
在多個任務(wù)不關(guān)心執(zhí)行順序的情況下,繼發(fā)會浪費(fèi)很多的執(zhí)行時間。