JavaScript異步——callback、promise、async/await

背景

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ì)列示意圖:

3310_1.png

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)
3312_1.png

可以看到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í)行時間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容