溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript異步編程的示例分析

發(fā)布時間:2021-02-23 11:18:03 來源:億速云 閱讀:156 作者:清風 欄目:web開發(fā)

這篇文章主要為大家展示了JavaScript異步編程的示例分析,內(nèi)容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶大家一起來研究并學習一下“JavaScript異步編程的示例分析”這篇文章吧。

Java可以用來干什么

Java主要應用于:1. web開發(fā);2. Android開發(fā);3. 客戶端開發(fā);4. 網(wǎng)頁開發(fā);5. 企業(yè)級應用開發(fā);6. Java大數(shù)據(jù)開發(fā);7.游戲開發(fā)等。

前言

自己著手準備寫這篇文章的初衷是覺得如果想要更深入的理解 JS,異步編程則是必須要跨過的一道坎。由于這里面涉及到的東西很多也很廣,在初學 JS 的時候可能無法完整的理解這一概念,即使在現(xiàn)在來看還是有很多自己沒有接觸和理解到的知識點,但是為了跨過這道坎,我仍然愿意鼓起勇氣用我已經(jīng)掌握的部分知識盡全力講述一下 JS 中的異步編程。如果我所講的一些概念或術語有錯誤,請讀者向我指出問題所在,我會立即糾正更改。

同步與異步

我們知道無論是在瀏覽器端還是在服務器 ( Node ) 端,JS 的執(zhí)行都是在單線程下進行的。我們以瀏覽器中的 JS 執(zhí)行線程為例,在這個線程中 JS 引擎會創(chuàng)建執(zhí)行上下文棧,之后我們的代碼就會作為執(zhí)行上下文 ( 全局、函數(shù)、eval ) 像一系列任務一樣在執(zhí)行上下文棧中按照后進先出 ( LIFO ) 的方式依次執(zhí)行。而同步最大的特性就是會阻塞后面任務的執(zhí)行,比如此時 JS 正在執(zhí)行大量的計算,這個時候就會使線程阻塞從而導致頁面渲染加載不連貫 ( 在瀏覽器端的 Event Loop 中每次執(zhí)行棧中的任務執(zhí)行完畢后都會去檢查并執(zhí)行事件隊列里面的任務直到隊列中的任務為空,而事件隊列中的任務又分為微隊列與宏隊列,當微隊列中的任務執(zhí)行完后才會去執(zhí)行宏隊列中的任務,而在微隊列任務執(zhí)行完到宏隊列任務開始之前瀏覽器的 GUI 線程會執(zhí)行一次頁面渲染 ( UI rendering ),這也就解釋了為什么在執(zhí)行棧中進行大量的計算時會阻塞頁面的渲染 ) 。

與同步相對的異步則可以理解為在異步操作完成后所要做的任務,它們通常以回調(diào)函數(shù)或者 Promise 的形式被放入事件隊列,再由事件循環(huán) ( Event Loop ) 機制在每次輪詢時檢查異步操作是否完成,若完成則按事件隊列里面的執(zhí)行規(guī)則來依次執(zhí)行相應的任務。也正是得益于事件循環(huán)機制的存在,才使得異步任務不會像同步任務那樣完全阻塞 JS 執(zhí)行線程。

異步操作一般包括  網(wǎng)絡請求 、文件讀取 、數(shù)據(jù)庫處理

異步任務一般包括  setTimout / setInterval 、Promise 、requestAnimationFrame ( 瀏覽器獨有 ) 、setImmediate ( Node 獨有 ) 、process.nextTick ( Node 獨有 ) 、etc ...

注意: 在瀏覽器端與在 Node 端的 Event Loop 機制是有所不同的,下面給出的兩張圖簡要闡述了在不同環(huán)境下事件循環(huán)的運行機制,由于 Event Loop 不是本文內(nèi)容的重點,但是 JS 異步編程又是建立在它的基礎之上的,故在下面給出相應的閱讀鏈接,希望能夠幫助到有需要的讀者。

瀏覽器端

JavaScript異步編程的示例分析

Node 端

JavaScript異步編程的示例分析

閱讀鏈接

解析Node.js的事件循環(huán)機制

詳解javascript瀏覽器的事件循環(huán)機制

為異步而生的 JS 語法

回望歷史,在最近幾年里 ECMAScript 標準幾乎每年都有版本的更新,也正是因為有像 ES6 這種在語言特性上大版本的更新,到了現(xiàn)今的 8102 年, JS 中的異步編程相對于那個只有回調(diào)函數(shù)的遠古時代有了很大的進步。下面我將介紹 callback 、Promise 、generator 、async / await 的基本用法以及如何在異步編程中使用它們。

callback

回調(diào)函數(shù)并不算是 JS 中的語法但它卻是解決異步編程問題中最常用的一種方法,所以在這里有必要提出來,下面舉一個例子,大家看一眼就懂。

const foo = function (x, y, cb) {
    setTimeout(() => {
        cb(x + y)
    }, 2000)
}

// 使用 thunk 函數(shù),有點函數(shù)柯里化的味道,在最后處理 callback。
const thunkify = function (fn) {
    return function () {
        let args = Array.from(arguments)
        return function (cb) {
            fn.apply(null, [...args, cb])
        }
    }
}

let fooThunkory = thunkify(foo)

let fooThunk1 = fooThunkory(2, 8)
let fooThunk2 = fooThunkory(4, 16)

fooThunk1((sum) => {
    console.log(sum) // 10
})

fooThunk2((sum) => {
    console.log(sum) // 20
})
Promise

在 ES6 沒有發(fā)布之前,作為異步編程主力軍的回調(diào)函數(shù)一直被人詬病,其原因有太多比如回調(diào)地獄、代碼執(zhí)行順序難以追蹤、后期因代碼變得十分復雜導致無法維護和更新等,而 Promise 的出現(xiàn)在很大程度上改變了之前的窘境。話不多說先直接上代碼提前感受下它的魅力,然后我再總結下自己認為在 Promise 中很重要的幾個點。

const foo = function () {
    let args = [...arguments]
    let cb = args.pop()
    setTimeout(() => {
        cb(...args)
    }, 2000)
}

const promisify = function (fn) {
    return function () {
        let args = [...arguments]
        return function (cb) {
            return new Promise((resolve, reject) => {
                fn.apply(null, [...args, resolve, reject, cb])
            })
        }
    }
}

const callback = function (x, y, isAdd, resolve, reject) {
    if (isAdd) {
        resolve(x + y)
    } else {
        reject('Add is not allowed.')
    }
}

let promisory = promisify(foo)

let p1 = promisory(4, 16, false)
let p2 = promisory(2, 8, true)

p1(callback)
.then((sum) => {
    console.log(sum)
}, (err) => {
    console.error(err) // Add is not allowed.
})
.finally(() => {
    console.log('Triggered once the promise is settled.')
})

p2(callback)
.then((sum) => {
    console.log(sum) // 10
    return 'evil '
})
.then((unknown) => {
    throw new Error(unknown)
})
.catch((err) => {
    console.error(err) // Error: evil 
})

要點一:反控制反轉 ( 關注點分離 )

什么是反控制反轉呢?要理解它我們應該先弄清楚控制反轉的含義,來看一段偽代碼。

const request = require('request')

// 某購物系統(tǒng)獲取用戶必要信息后執(zhí)行收費操作
const purchase = function (url) {
    request(url, (err, response, data) => {
        if (err) return console.error(err)
        if (response.statusCode === 200) {
            chargeUser(data)
        }
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')

顯然在這里 request 模塊屬于第三方庫是不能夠完全信任的,假如某一天該模塊出了 bug , 原本只會向目標 url 發(fā)送一次請求卻變成了多次,相應的我們的 chargeUser 函數(shù)也就是收費操作就會被執(zhí)行多次,最終導致用戶被多次收費,這樣的結果完全就是噩夢!然而這就是控制反轉,即把自己的代碼交給第三方掌控,因此是不可完全信任的。

那么反控制反轉現(xiàn)在我們可以猜測它的含義應該就是將控制權交還到我們自己寫的代碼中,而要實現(xiàn)這點通常我們會引入一個第三方協(xié)商機制,在 Promise 之前我們會通過事件監(jiān)聽的形式來解決這類問題。現(xiàn)在我們將代碼更改如下:

const request = require('request')
const events = require('events')

const listener = new events.EventEmitter()

listener.on('charge', (data) => {
    chargeUser(data)
})

const purchase = function (url) {
    request(url, (err, response, data) => {
        if (err) return console.error(err)
        if (response.statusCode === 200) {
            listener.emit('charge', data)
        }
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')

更改代碼之后我們會發(fā)現(xiàn)控制反轉的恢復其實是更好的實現(xiàn)了關注點分離,我們不用去關心 purchase 函數(shù)內(nèi)部具體發(fā)生了什么,只需要知道它在什么時候完成,之后我們的關注點就從 purchase 函數(shù)轉移到了 listener 對象上。我們可以把 listener 對象提供給代碼中多個獨立的部分,在 purchase 函數(shù)完成后,它們同樣也能收到通知并進行下一步的操作。以下是維基百科上關于關注點分離的一部分介紹。

關注點分離的價值在于簡化計算機程序的開發(fā)和維護。當關注點分開時,各部分可以重復使用,以及獨立開發(fā)和更新。具有特殊價值的是能夠稍后改進或修改一段代碼,而無需知道其他部分的細節(jié)必須對這些部分進行相應的更改。

一一    維基百科

顯然在 Promise 中 new Promise() 返回的對象就是關注點分離中分離出來的那個關注對象。

要點二:不可變性 ( 值得信任 )

細心的讀者可能會發(fā)現(xiàn),要點一中基于事件監(jiān)聽的反控制反轉仍然沒有解決最重要的信任問題,收費操作仍舊可以因為第三方 API 的多次調(diào)用而被觸發(fā)且執(zhí)行多次。幸運的是現(xiàn)在我們擁有 Promise 這樣強大的機制,才得以讓我們從信任危機中解脫出來。所謂不可變性就是:

Promise 只能被決議一次,如果代碼中試圖多次調(diào)用 resolve(..) 或者 reject(..) ,Promise 只會接受第一次決議,決議后就是外部不可變的值,因此任何通過 then(..) 注冊的回調(diào)只會被調(diào)用一次。

現(xiàn)在要點一中的示例代碼就可以最終更改為:

const request = require('request')

const purchase = function (url) {
    return new Promise((resolve, reject) => {
        request(url, (err, response, data) => {
            if (err) reject(err)
            if (response.statusCode === 200) {
                resolve(data)
            }
        })
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')
.then((data) => {
    chargeUser(data)
})
.catch((err) => {
    console.error(err)
})

要點三:錯誤處理及一些細節(jié)

還記得最開始講 Promise 時的那一段代碼嗎?我們把打印結果的那部分代碼再次拿出來看看。

p1(callback)
.then((sum) => {
    console.log(sum)
}, (err) => {
    console.error(err) // Add is not allowed.
})
.finally(() => {
    console.log('Triggered once the promise is settled.')
})

p2(callback)
.then((sum) => {
    console.log(sum) // 10
    return 'evil '
})
.then((unknown) => {
    throw new Error(unknown)
})
.catch((err) => {
    console.error(err) // Error: evil 
})

首先我們說下 then(..) ,它的第一個參數(shù)作為函數(shù)接收 promise 對象中 resolve(..) 的值,第二個參數(shù)則作為錯誤處理函數(shù)處理在 Promise 中可能發(fā)生的錯誤。

而在 Promise 中有兩種錯誤可能會出現(xiàn),一種是顯式 reject(..) 拋出的錯誤,另一種則是代碼自身有錯誤會被 Promise 捕捉,通過 then(..) 中的錯誤處理函數(shù)我們可以接收到它前面 promise 對象中出現(xiàn)的錯誤,而如果在 then(..) 接收 resolve(..) 值的函數(shù)中也出現(xiàn)錯誤,該錯誤則會被下一個 then(..) 的錯誤處理函數(shù)所接收 ( 有兩個前提,第一是要寫出這個 then(..) 否則該錯誤最終會在全局拋出,第二個則是要確保前一個 then(..) 在它的 Promise 決議后調(diào)用的是第一個參數(shù)即接收 resolve(..) 值的函數(shù)而不是錯誤處理函數(shù) )。

一些值得注意的細節(jié):

catch(..) 相當于 then(..) 中的錯誤處理函數(shù) ,只是省略了第一個參數(shù)。

finally(..) 在 Promise 一旦決議后 ( 無論是 resolve 還是 reject ) 都會被執(zhí)行。

then(..) 、catch(..) 、finally(..) 都是異步調(diào)用,作為 Event Loop 里事件隊列中的微隊列任務執(zhí)行。

generator

generator 也叫做生成器,它是 ES6 中引入的一種新的函數(shù)類型,在函數(shù)內(nèi)部它可以多次啟動和暫停,從而形成阻塞同步的代碼。下面我將先講述它的基本用法然后是它在異步編程中的使用最后會簡單探究一下它的工作原理。

生成器基本用法

let a = 2

const foo = function *(x, y) {
    let b = (yield x) + a
    let c = (yield y) + b
    console.log(a + b + c)
}

let it = foo(6, 8)

let x = it.next().value
a++
let y = it.next(x * 5).value
a++

it.next(x + y) // 84

從上面的代碼我們可以看到與普通的函數(shù)不同,生成器函數(shù)執(zhí)行后返回的是一個迭代器對象,用來控制生成器的暫停和啟動。在常見的設計模式中就有一種模式叫做迭代器模式,它指的是提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內(nèi)部表示。迭代器對象 it 包含一個 next(..) 方法且在調(diào)用之后返回一個 { done: .. , value: .. } 對象,現(xiàn)在我們先來自己實現(xiàn)一個簡單的迭代器。

const iterator = function (obj) {
    let current = -1
    return {
        [Symbol.iterator]() {
            return this
        },
        next() {
            current++
            return { done: current < obj.length ? false : true, value: obj[current] }
        }
    }
}

let it1 = iterator([1,2,3,4])

it1.next().value // 1
it1.next().value // 2
it1.next().value // 3
it1.next().value // 4

let it2 = iterator([5,6,7,8])

for (let v of it2) { console.log(v) } // 5 6 7 8

可以看到我們自己實現(xiàn)的迭代器不僅能夠手動進行迭代,還能被 for..of 自動迭代展開,這是因為在 ES6 中只要對象具有 Symbol.iterator 屬性且該屬性返回的是一個迭代器對象,就能夠被 for..of 所消費。

回頭來看最開始的那個 generator 示例代碼中生成器產(chǎn)生的迭代器對象 it ,似乎它比普通的迭代器有著更強大的功能,其實就是與 yield 表達式緊密相連的消息雙向傳遞?,F(xiàn)在我先來總結一下自己認為在生成器中十分重要的點,然后再來分析下那段示例代碼的完整執(zhí)行過程。

每次調(diào)用 it.next() 后生成器函數(shù)內(nèi)的代碼就會啟動執(zhí)行且返回一個 { done: .. , value: .. } 對象,一旦遇到 yield 表達式就會暫停執(zhí)行,如果此時 yield 表達式后面跟有值例如 yield val,那么這個 val 就會被傳入返回對象中鍵名 value 對應的鍵值,當再次調(diào)用 it.next() 時 yield 的暫停效果就會被取消,如果此時的 next 為形如 it.next(val) 的調(diào)用,yield 表達式就會被 val 所替換。這就是生成器內(nèi)部與迭代器對象外部之間的消息雙向傳遞。

弄清了生成器中重要的特性后要理解開頭的那段代碼就不難了,首先執(zhí)行第一個 it.next().value ,遇到第一個 yield 后生成器暫停執(zhí)行,此時變量 x 接受到的值為 6。在全局環(huán)境下執(zhí)行 a++ 后再次執(zhí)行 it.next(x * 5).value 生成器繼續(xù)執(zhí)行且傳入值 30,因此變量 b 的值就為 33,當遇到第二個 yield 后生成器又暫停執(zhí)行,并且將值 8 傳出給變量 y 。再次執(zhí)行 a++ ,然后執(zhí)行 it.next(x + y) 恢復生成器執(zhí)行并傳入值 14,此時變量 c 的值就為 47,最終計算 a + b + c 便可得到值 84。

在異步編程中使用生成器

既然現(xiàn)在我們已經(jīng)知道了生成器內(nèi)部擁有能夠多次啟動和暫停代碼執(zhí)行的強大能力,那么將它用于異步編程中也便是理所當然的事情了。先來看一個異步迭代生成器的例子。

const request = require('request')

const foo = function () {
    request('https://cosmos-alien.com/some.url', (err, response, data) => {
        if (err) it.throw(err)
        if (response.statusCode === 200) {
            it.next(data)
        }
    })
}

const main = function *() {
    try {
        let result = yield foo()
        console.log(result)
    }
    catch (err) {
        console.error(err)
    }
}

let it = main()

it.next()

這個例子的邏輯很簡單,調(diào)用 it.next() 后生成器啟動,遇到 yield 時生成器暫停運行,但此時 foo 函數(shù)已經(jīng)執(zhí)行即網(wǎng)絡請求已經(jīng)發(fā)出,等到有響應結果時如果出錯則調(diào)用 it.throw(err) 將錯誤拋回生成器內(nèi)部由 try..catch 同步捕獲,否則將返回的 data 作為傳回生成器的值在恢復執(zhí)行的同時將 data 賦值給變量 result ,最后打印 result 得到我們想要的結果。

在 ES6 中最完美的世界就是生成器 ( 看似同步的異步代碼 ) 和 Promise ( 可信任可組合 ) 的結合,因此我們現(xiàn)在再來看一個由生成器 + Promise 實現(xiàn)異步操作的例子。

const axios = require('axios')

const foo = function () {
    return axios({
        method: 'GET',
        url: 'https://cosmos-alien.com/some.url'
    })
}

const main = function *() {
    try {
        let result = yield foo()
        console.log(result)
    }
    catch (err) {
        console.error(err)
    }
}

let it = main()

let p = it.next().value

p.then((data) => {
    it.next(data)
}, (err) => {
    it.throw(err)
})

這個例子跟前面異步迭代生成器的例子幾乎是差不多的,唯一不同的就是 yield 傳遞出去的是一個 promise 對象,之后我們在 then(..) 中來恢復執(zhí)行生成器里下一步的操作或是拋出一個錯誤。

生成器工作原理

在講了那么多關于 generator 生成器的使用后,相信讀者也跟我一樣想知道生成器究竟是如何實現(xiàn)能夠控制函數(shù)內(nèi)部代碼的暫停和啟動,從而形成阻塞同步的效果。

我們先來簡單了解下有限狀態(tài)機 ( FSM ) 這個概念,維基百科上給出的解釋是表示有限個狀態(tài)以及在這些狀態(tài)之間的轉移和動作等行為的數(shù)學模型。簡單的來說,它有三個主要特征:

  1. 狀態(tài)總數(shù) ( state ) 是有限的

  2. 任一時刻,只處在一種狀態(tài)之中

  3. 某種條件下,會從一種狀態(tài)轉變 ( transition ) 到另一種狀態(tài)

其實生成器就是通過暫停自己的作用域 / 狀態(tài)來實現(xiàn)它的魔法的,下面我們就以上文的生成器 + Promise 的例子為基礎,用有限狀態(tài)機的方式來闡述生成器的基本工作原理。

let stateRequest = {
    done: false,
    transition(message) {
        this.state = this.stateResult
        console.log(message)
        // state 1
        return foo()
    }
}

let stateResult = {
    done: true,
    transition(data) {
        // state 2
        let result = data
        console.log(result)
    }
}

let stateError = {
    transition(err) {
        // state 3
        console.error(err)
    }
}

let it = {
    init() {
        this.stateRequest = Object.create(stateRequest)
        this.stateResult = Object.create(stateResult)
        this.stateError = Object.create(stateError)
        this.state = this.stateRequest
    },
    next(data) {
        if (this.state.done) {
            return {
                done: true,
                value: undefined
            }
        } else {
            return {
                done: this.state.done,
                value: this.state.transition.call(this, data)
            }
        }
    },
    throw(err) {
        return {
            done: true,
            value: this.stateError.transition(err)
        }
    }
}

it.init()
it.next('The request begins !')

在這里我使用了行為委托模式和狀態(tài)模式實現(xiàn)了一個簡單的有限狀態(tài)機,而它卻展現(xiàn)了生成器中核心部分的工作原理,下面我們來逐步分析它是如何運行的。

首先這里我們自己創(chuàng)建的 it 對象就相當于生成器函數(shù)執(zhí)行后返回的迭代器對象,我們把上文生成器 + Promise 示例中的 main 函數(shù)代碼分為了三個狀態(tài)并將跟該狀態(tài)有關的行為封裝到了 stateRequest 、stateResult 、stateError 三個對象中。然后我們再調(diào)用 init(..) 將 it 對象上的行為委托到這三個對象上并初始化當前的狀態(tài)對象。在準備工作完成后調(diào)用 next(..) 啟動生成器,這個時候我們就進入了狀態(tài)一,即執(zhí)行 foo 函數(shù)發(fā)出網(wǎng)絡請求。在 foo 函數(shù)內(nèi)部當?shù)玫秸埱箜憫獢?shù)據(jù)后就執(zhí)行 it.next(data) 觸發(fā)狀態(tài)機內(nèi)部的狀態(tài)改變,此時執(zhí)行狀態(tài)二內(nèi)部的代碼即打印網(wǎng)絡請求返回的結果。如果網(wǎng)絡請求中出現(xiàn)錯誤就會執(zhí)行 it.throw(err) ,這個時候的狀態(tài)就會轉換到狀態(tài)三即錯誤處理狀態(tài)。

在這里我們似乎忽略了一個很重要的地方,就是生成器是如何做到將其內(nèi)部的代碼分為多個狀態(tài)的,當然我們知道這肯定是 yield 表達式的功勞,但是其內(nèi)部又是怎么實現(xiàn)的呢?由于本人能力還不夠,而且還有很多東西來不及去學習和了解,因此暫時無法解決這個問題,但我還是愿意把這個問題提出來,如果讀者確實有興趣能夠通過查閱資料找到答案或者已經(jīng)知道它的原理還是可以分享出來,畢竟經(jīng)歷這樣刨根問底的過程還是滿有趣的。

async / await

終于講到最后一個異步語法了,作為壓軸的身份出場,據(jù)說 async / await 是 JS 異步編程中的終極解決方案。話不多說,先直接上代碼看看它的基本用法,然后我們再來探討一下它的實現(xiàn)原理。

const foo = function (time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(time + 200)
        }, time)
    })
}

const step1 = time => foo(time)
const step2 = time => foo(time) 
const step3 = time => foo(time)

const main = async function () {
    try {
        console.time('run')
        let time1 = 200
        let time2 = await step1(time1)
        let time3 = await step2(time2)
        await step3(time3)
        console.log(`All steps took ${time1 + time2 + time3} ms.`)
        console.timeEnd('run')
    } catch(err) {
        console.error(err)
    }
}

main()
// All steps took 1200 ms.
// run: 1222.87939453125ms

我們可以看到 async 函數(shù)跟生成器函數(shù)極為相似,只是將之前的 * 變成了 async ,yield 變成了 await 。其實它就是一個能夠自動執(zhí)行的 generator 函數(shù),我們不用再通過手動執(zhí)行 it.next(..) 來控制生成器函數(shù)的暫停與啟動。

await 幫我們做到了在同步阻塞代碼的同時還能夠監(jiān)聽 Promise 對象的決議,一旦 promise 決議,原本暫停執(zhí)行的 async 函數(shù)就會恢復執(zhí)行。這個時候如果決議是 resolve ,那么返回的結果就是 resolve 出來的值。如果決議是 reject ,我們就必須用 try..catch 來捕獲這個錯誤,因為它相當于執(zhí)行了 it.throw(err) 。

下面直接給出一種主流的 async / await 語法版本的實現(xiàn)代碼:

const runner = function (gen) {
    return new Promise((resolve, reject) => {
        var it = gen()
        const step = function (execute) {
            try {
                var next = execute()
            } catch (err) {
                reject(err)
            }
            
            if (next.done) return resolve(next.value)
            
            Promise.resolve(next.value)
            .then(val => step(() => it.next(val)))
            .catch(err => step(() => it.throw(err)))
        }
        step(() => it.next())
    })
}

async function fn() {
    // ...
}

// 等同于

function fn() {
    const gen = function *() {
        // ...
    }
    runner(gen)
}

從上面的代碼我們可以看出 async 函數(shù)執(zhí)行后返回的是一個 Promise 對象,然后使用遞歸的方法去自動執(zhí)行生成器函數(shù)的暫停與啟動。如果調(diào)用 it.next().value 傳出來的是一個 promise ,則用 Promise.resolve() 方法將其異步展開,當這個 promise 決議時就可以重新啟動執(zhí)行生成器函數(shù)或者拋出一個錯誤被 try..catch 所捕獲并最終在 async 函數(shù)返回的 Promise 對象的錯誤處理函數(shù)中處理。

關于 async / await 的執(zhí)行順序

下面給出一道關于 async / await 執(zhí)行順序的經(jīng)典面試題,網(wǎng)上給出的解釋給我感覺似乎很含糊。在這里我們結合上文所講的 generator 函數(shù)運行機制和 async / await 實現(xiàn)原理來具體闡述下為什么執(zhí)行順序是這樣的。

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}

async function async2(){
    console.log('async2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
})

async1()

new Promise((resolve) => {
    console.log('promise1')
    resolve()
})
.then(() => {
    console.log('promise2')
})

console.log('script end')

將這段代碼放在瀏覽器中運行,最終的結果這樣的:

script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout

其實最主要的地方還是要分清在執(zhí)行棧中同步執(zhí)行的任務與事件隊列中異步執(zhí)行的任務。首先我們執(zhí)行同步任務,打印 script start ,調(diào)用函數(shù) async1 ,在我們遇到 await 表達式后就會暫停函數(shù) async1 的執(zhí)行。因為在這里它相當于 yield async2() ,根據(jù)上文的 async / await 原理實現(xiàn)代碼可以看出,當自動調(diào)用 it.next() 時遇到第一個 yield 后會暫停執(zhí)行,但此時函數(shù) async2 已經(jīng)執(zhí)行。上文還提到過 async 函數(shù)在執(zhí)行完后會返回一個 Promise 對象,故此時 it.next().value 的值就是一個 promise 。接下來要講的就是重點啦 ?。?!

我們用 Promise.resolve() 去異步地展開一個 promise ,因此第一個放入事件隊列中的微隊列任務其實就是這個 promise 。之后我們再繼續(xù)運行執(zhí)行棧中剩下的同步任務,此時打印出 promise1 和 script end ,同時第二個異步任務被加入到事件隊列中的微隊列。同步的任務執(zhí)行完了,現(xiàn)在來執(zhí)行異步任務,首先將微隊列中第一個放入的那個 promise 拿到執(zhí)行棧中去執(zhí)行,這個時候之前 Promise.resolve() 后面注冊的回調(diào)任務才會作為第三個任務加入到事件隊列中的微隊列里去。然后我們執(zhí)行微隊列中的第二個任務,打印 promise2,再執(zhí)行第三個任務即調(diào)用 step(() => it.next(val)) 恢復 async 函數(shù)的執(zhí)行,打印 async1 end 。最后,因為微隊列總是搶占式的在宏隊列之前插入執(zhí)行,故只有當微隊列中沒有了任務以后,宏隊列中的任務才會開始執(zhí)行,故最終打印出 setTimeout 。

常見異步模式

在軟件開發(fā)中有著設計模式這一專業(yè)術語,通俗一點來講設計模式其實就是在某種場合下針對某個問題的一種解決方案。

在 JS 異步編程的世界里,很多時候我們也會遇到因為是異步操作而出現(xiàn)的特定問題,而針對這些問題所提出的解決方案 ( 邏輯代碼 ) 就是異步編程的核心,似乎在這里它跟設計模式的概念很相像,所以我把它叫做異步模式。下面我將介紹幾種常見的異步模式在實際場景下的應用。

并發(fā)交互模式

當我們在同時執(zhí)行多個異步任務時,這些任務返回響應結果的時間往往是不確定的,因而會產(chǎn)生以下兩種常見的需求:

  1. 多個異步任務同時執(zhí)行,等待所有任務都返回結果后才開始進行下一步的操作。

  2. 多個異步任務同時執(zhí)行,只返回最先完成異步操作的那個任務的結果然后再進行下一步的操作。

場景一:

同時讀取多個含有英文文章的 txt 文件內(nèi)容,計算其中單詞 of 的個數(shù)。

  1. 等待所有文件中的 of 個數(shù)計算完畢,再計算輸出總的 of 數(shù)。

  2. 直接輸出第一個計算完 of 的個數(shù)。

const fs = require('fs')
const path = require('path')

const addAll = (result) => console.log(result.reduce((prev, cur) => prev + cur))

let dir = path.join(__dirname, 'files')

fs.readdir(dir, (err, files) => {
    if (err) return console.error(err)
    let promises = files.map((file) => {
        return new Promise((resolve, reject) => {
            let fileDir = path.join(dir, file)
            fs.readFile(fileDir, { encoding: 'utf-8' }, (err, data) => {
                if (err) reject(err)
                let count = 0
                data.split(' ').map(word => word === 'of' ? count++ : null)
                resolve(count)
            })
        })
    })
    Promise.all(promises).then(result => addAll(result)).catch(err => console.error(err))
    Promise.race(promises).then(result => console.log(result)).catch(err => console.error(err))
})

并發(fā)控制模式

有時候我們會遇到大量異步任務并發(fā)執(zhí)行而且還要處理返回數(shù)據(jù)的情況,即使擁有事件循環(huán) ( Event Loop ) 機制,在并發(fā)量過高的情況下程序仍然會崩潰,所以這個時候就應該考慮并發(fā)控制。

場景二:

利用 Node.js 實現(xiàn)圖片爬蟲,控制爬取時的并發(fā)量。一是防止 IP 被封掉 ,二是防止并發(fā)請求量過高使程序崩潰。

const fs = require('fs')
const path = require('path')
const request = require('request')
const cheerio = require('cheerio')

const target = `http://www.zimuxia.cn/${encodeURIComponent('我們的作品')}`

const isError = (err, res) => (err || res.statusCode !== 200) ? true : false

const getImgUrls = function (pages) {
    return new Promise((resolve) => {
        let limit = 8, number = 0, imgUrls = []
        const recursive = async function () {
            pages = pages - limit
            limit = pages >= 0 ? limit : (pages + limit)
            let arr = []
            for (let i = 1; i <=limit; i++) {
                arr.push(
                    new Promise((resolve) => {
                        request(target + `?set=${number++}`, (err, res, data) => {
                            if (isError(err, res)) return console.log('Request failed.')
                            let $ = cheerio.load(data)
                            $('.pg-page-wrapper img').each((i, el) => {
                                let imgUrl = $(el).attr('data-cfsrc')
                                imgUrls.push(imgUrl)
                                resolve()
                            })
                        })
                    })
                )
            }
            await Promise.all(arr)
            if (limit === 8) return recursive()
            resolve(imgUrls)
        }
        recursive()
    })
}

const downloadImages = function (imgUrls) {
    console.log('\n Start to download images. \n')
    let limit = 5
    const recursive = async function () {
        limit = imgUrls.length - limit >= 0 ? limit : imgUrls.length
        let arr = imgUrls.splice(0, limit)
        let promises = arr.map((url) => {
            return new Promise((resolve) => {
                let imgName = url.split('/').pop()
                let imgPath = path.join(__dirname, `images/${imgName}`)
                request(url)
                .pipe(fs.createWriteStream(imgPath))
                .on('close', () => {
                    console.log(`${imgName} has been saved.`)
                    resolve()
                })
            })
        })
        await Promise.all(promises)
        if (imgUrls.length) return recursive()
        console.log('\n All images have been downloaded.')
    }
    recursive()
}

request({
    url: target,
    method: 'GET'
}, (err, res, data) => {
    if (isError(err, res)) return console.log('Request failed.')
    let $ = cheerio.load(data)
    let pageNum = $('.pg-pagination li').length
    console.log('Start to get image urls...')
    getImgUrls(pageNum)
    .then((result) => {
        console.log(`Finish getting image urls and the number of them is ${result.length}.`)
        downloadImages(result)
    })
})

發(fā)布 / 訂閱模式

我們假定,存在一個"信號中心",當某個任務執(zhí)行完成,就向信號中心"發(fā)布" ( publish ) 一個信號,其他任務可以向信號中心"訂閱" ( subscribe ) 這個信號,從而知道什么時候自己可以開始執(zhí)行,當然我們還可以取消訂閱這個信號。

我們先來實現(xiàn)一個簡單的發(fā)布訂閱對象:

class Listener {
    constructor() {
        this.eventList = {}
    }
    on(event, fn) {
        if (!this.eventList[event]) this.eventList[event] = []
        if (fn.name) {
            let obj = {}
            obj[fn.name] = fn
            fn = obj
        }
        this.eventList[event].push(fn)
    }
    remove(event, fn) {
        if (!fn) return console.error('Choose a named function to remove!')
        this.eventList[event].map((item, index) => {
            if (typeof item === 'object' && item[fn.name]) {
                this.eventList[event].splice(index, 1)
            }
        })
    }
    emit(event, data) {
        this.eventList[event].map((fn) => {
            if (typeof fn === 'object') {
                Object.values(fn).map((f) => f.call(null, data))
            } else {
                fn.call(null, data)
            }
        })
    }
}

let listener = new Listener()

function foo(data) { console.log('Hello ' + data) }

listener.on('click', (data) => console.log(data))

listener.on('click', foo)

listener.emit('click', 'RetroAstro')

// Hello
// Hello RetroAstro

listener.remove('click', foo)

listener.emit('click', 'Barry Allen')

// Barry Allen

場景三:

監(jiān)聽 watch 文件夾,當里面的文件有改動時自動壓縮該文件并保存到 done 文件夾中。

// gzip.js
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')

const gzipFile = function (file) {
    let dir = path.join(__dirname, 'watch')
    fs.readdir(dir, (err, files) => {
        if (err) console.error(err)
        files.map((filename) => {
            let watchFile = path.join(dir, filename)
            fs.stat(watchFile, (err, stats) => {
                if (err) console.error(err)
                if (stats.isFile() && file === filename) {
                    let doneFile = path.join(__dirname, `done/${file}.gz`)
                    fs.createReadStream(watchFile)
                    .pipe(zlib.createGzip())
                    .pipe(fs.createWriteStream(doneFile))
                }
            })
        })
    })
}

module.exports = {
    gzipFile: gzipFile
}

開始監(jiān)聽 watch 文件夾

// watch.js
const fs = require('fs')
const path = require('path')

const { gzipFile } = require('./gzip')
const { Listener } = require('./listener')

let listener = new Listener()

listener.on('gzip', (data) => gzipFile(data))

let dir = path.join(__dirname, 'watch')

let wait = true

fs.watch(dir, (event, filename) => {
    if (filename && event === 'change' && wait) {
        wait = false
        setTimeout(() => wait = true, 100)
        listener.emit('gzip', filename)
    }
})

以上就是關于“JavaScript異步編程的示例分析”的內(nèi)容,如果改文章對你有所幫助并覺得寫得不錯,勞請分享給你的好友一起學習新知識,若想了解更多相關知識內(nèi)容,請多多關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI