真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

JavaScript異步編程的示例分析

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

創(chuàng)新互聯(lián)建站:從2013年創(chuàng)立為各行業(yè)開(kāi)拓出企業(yè)自己的“網(wǎng)站建設(shè)”服務(wù),為成百上千家公司企業(yè)提供了專(zhuān)業(yè)的成都網(wǎng)站建設(shè)、做網(wǎng)站、網(wǎng)頁(yè)設(shè)計(jì)和網(wǎng)站推廣服務(wù), 定制網(wǎng)站由設(shè)計(jì)師親自精心設(shè)計(jì),設(shè)計(jì)的效果完全按照客戶(hù)的要求,并適當(dāng)?shù)奶岢龊侠淼慕ㄗh,擁有的視覺(jué)效果,策劃師分析客戶(hù)的同行競(jìng)爭(zhēng)對(duì)手,根據(jù)客戶(hù)的實(shí)際情況給出合理的網(wǎng)站構(gòu)架,制作客戶(hù)同行業(yè)具有領(lǐng)先地位的。

Java可以用來(lái)干什么

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

前言

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

同步與異步

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

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

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

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

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

瀏覽器端

JavaScript異步編程的示例分析

Node 端

JavaScript異步編程的示例分析

閱讀鏈接

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

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

為異步而生的 JS 語(yǔ)法

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

callback

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

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

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

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 
})

要點(diǎn)一:反控制反轉(zhuǎn) ( 關(guān)注點(diǎn)分離 )

什么是反控制反轉(zhuǎn)呢?要理解它我們應(yīng)該先弄清楚控制反轉(zhuǎn)的含義,來(lái)看一段偽代碼。

const request = require('request')

// 某購(gòu)物系統(tǒng)獲取用戶(hù)必要信息后執(zhí)行收費(fèi)操作
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 模塊屬于第三方庫(kù)是不能夠完全信任的,假如某一天該模塊出了 bug , 原本只會(huì)向目標(biāo) url 發(fā)送一次請(qǐng)求卻變成了多次,相應(yīng)的我們的 chargeUser 函數(shù)也就是收費(fèi)操作就會(huì)被執(zhí)行多次,最終導(dǎo)致用戶(hù)被多次收費(fèi),這樣的結(jié)果完全就是噩夢(mèng)!然而這就是控制反轉(zhuǎn),即把自己的代碼交給第三方掌控,因此是不可完全信任的。

那么反控制反轉(zhuǎn)現(xiàn)在我們可以猜測(cè)它的含義應(yīng)該就是將控制權(quán)交還到我們自己寫(xiě)的代碼中,而要實(shí)現(xiàn)這點(diǎn)通常我們會(huì)引入一個(gè)第三方協(xié)商機(jī)制,在 Promise 之前我們會(huì)通過(guò)事件監(jiān)聽(tīng)的形式來(lái)解決這類(lèi)問(wè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')

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

關(guān)注點(diǎn)分離的價(jià)值在于簡(jiǎn)化計(jì)算機(jī)程序的開(kāi)發(fā)和維護(hù)。當(dāng)關(guān)注點(diǎn)分開(kāi)時(shí),各部分可以重復(fù)使用,以及獨(dú)立開(kāi)發(fā)和更新。具有特殊價(jià)值的是能夠稍后改進(jìn)或修改一段代碼,而無(wú)需知道其他部分的細(xì)節(jié)必須對(duì)這些部分進(jìn)行相應(yīng)的更改。

一一    維基百科

顯然在 Promise 中 new Promise() 返回的對(duì)象就是關(guān)注點(diǎn)分離中分離出來(lái)的那個(gè)關(guān)注對(duì)象。

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

細(xì)心的讀者可能會(huì)發(fā)現(xiàn),要點(diǎn)一中基于事件監(jiān)聽(tīng)的反控制反轉(zhuǎn)仍然沒(méi)有解決最重要的信任問(wèn)題,收費(fèi)操作仍舊可以因?yàn)榈谌?API 的多次調(diào)用而被觸發(fā)且執(zhí)行多次。幸運(yùn)的是現(xiàn)在我們擁有 Promise 這樣強(qiáng)大的機(jī)制,才得以讓我們從信任危機(jī)中解脫出來(lái)。所謂不可變性就是:

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

現(xiàn)在要點(diǎ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)
})

要點(diǎn)三:錯(cuò)誤處理及一些細(xì)節(jié)

還記得最開(kāi)始講 Promise 時(shí)的那一段代碼嗎?我們把打印結(jié)果的那部分代碼再次拿出來(lái)看看。

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 
})

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

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

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

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

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

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

generator

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

生成器基本用法

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í)行后返回的是一個(gè)迭代器對(duì)象,用來(lái)控制生成器的暫停和啟動(dòng)。在常見(jiàn)的設(shè)計(jì)模式中就有一種模式叫做迭代器模式,它指的是提供一種方法順序訪(fǎng)問(wèn)一個(gè)聚合對(duì)象中的各個(gè)元素,而又不需要暴露該對(duì)象的內(nèi)部表示。迭代器對(duì)象 it 包含一個(gè) next(..) 方法且在調(diào)用之后返回一個(gè) { done: .. , value: .. } 對(duì)象,現(xiàn)在我們先來(lái)自己實(shí)現(xiàn)一個(gè)簡(jiǎ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

可以看到我們自己實(shí)現(xiàn)的迭代器不僅能夠手動(dòng)進(jìn)行迭代,還能被 for..of 自動(dòng)迭代展開(kāi),這是因?yàn)樵?ES6 中只要對(duì)象具有 Symbol.iterator 屬性且該屬性返回的是一個(gè)迭代器對(duì)象,就能夠被 for..of 所消費(fèi)。

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

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

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

在異步編程中使用生成器

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

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()

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

在 ES6 中最完美的世界就是生成器 ( 看似同步的異步代碼 ) 和 Promise ( 可信任可組合 ) 的結(jié)合,因此我們現(xiàn)在再來(lái)看一個(gè)由生成器 + Promise 實(shí)現(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)
})

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

生成器工作原理

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

我們先來(lái)簡(jiǎn)單了解下有限狀態(tài)機(jī) ( FSM ) 這個(gè)概念,維基百科上給出的解釋是表示有限個(gè)狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)模型。簡(jiǎn)單的來(lái)說(shuō),它有三個(gè)主要特征:

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

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

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

其實(shí)生成器就是通過(guò)暫停自己的作用域 / 狀態(tài)來(lái)實(shí)現(xiàn)它的魔法的,下面我們就以上文的生成器 + Promise 的例子為基礎(chǔ),用有限狀態(tài)機(jī)的方式來(lá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)模式實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的有限狀態(tài)機(jī),而它卻展現(xiàn)了生成器中核心部分的工作原理,下面我們來(lái)逐步分析它是如何運(yùn)行的。

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

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

async / await

終于講到最后一個(gè)異步語(yǔ)法了,作為壓軸的身份出場(chǎng),據(jù)說(shuō) async / await 是 JS 異步編程中的終極解決方案。話(huà)不多說(shuō),先直接上代碼看看它的基本用法,然后我們?cè)賮?lái)探討一下它的實(shí)現(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 。其實(shí)它就是一個(gè)能夠自動(dòng)執(zhí)行的 generator 函數(shù),我們不用再通過(guò)手動(dòng)執(zhí)行 it.next(..) 來(lái)控制生成器函數(shù)的暫停與啟動(dòng)。

await 幫我們做到了在同步阻塞代碼的同時(shí)還能夠監(jiān)聽(tīng) Promise 對(duì)象的決議,一旦 promise 決議,原本暫停執(zhí)行的 async 函數(shù)就會(huì)恢復(fù)執(zhí)行。這個(gè)時(shí)候如果決議是 resolve ,那么返回的結(jié)果就是 resolve 出來(lái)的值。如果決議是 reject ,我們就必須用 try..catch 來(lái)捕獲這個(gè)錯(cuò)誤,因?yàn)樗喈?dāng)于執(zhí)行了 it.throw(err) 。

下面直接給出一種主流的 async / await 語(yǔ)法版本的實(shí)現(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í)行后返回的是一個(gè) Promise 對(duì)象,然后使用遞歸的方法去自動(dòng)執(zhí)行生成器函數(shù)的暫停與啟動(dòng)。如果調(diào)用 it.next().value 傳出來(lái)的是一個(gè) promise ,則用 Promise.resolve() 方法將其異步展開(kāi),當(dāng)這個(gè) promise 決議時(shí)就可以重新啟動(dòng)執(zhí)行生成器函數(shù)或者拋出一個(gè)錯(cuò)誤被 try..catch 所捕獲并最終在 async 函數(shù)返回的 Promise 對(duì)象的錯(cuò)誤處理函數(shù)中處理。

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

下面給出一道關(guān)于 async / await 執(zhí)行順序的經(jīng)典面試題,網(wǎng)上給出的解釋給我感覺(jué)似乎很含糊。在這里我們結(jié)合上文所講的 generator 函數(shù)運(yùn)行機(jī)制和 async / await 實(shí)現(xiàn)原理來(lái)具體闡述下為什么執(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')

將這段代碼放在瀏覽器中運(yùn)行,最終的結(jié)果這樣的:

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

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

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

常見(jiàn)異步模式

在軟件開(kāi)發(fā)中有著設(shè)計(jì)模式這一專(zhuān)業(yè)術(shù)語(yǔ),通俗一點(diǎn)來(lái)講設(shè)計(jì)模式其實(shí)就是在某種場(chǎng)合下針對(duì)某個(gè)問(wèn)題的一種解決方案。

在 JS 異步編程的世界里,很多時(shí)候我們也會(huì)遇到因?yàn)槭钱惒讲僮鞫霈F(xiàn)的特定問(wèn)題,而針對(duì)這些問(wèn)題所提出的解決方案 ( 邏輯代碼 ) 就是異步編程的核心,似乎在這里它跟設(shè)計(jì)模式的概念很相像,所以我把它叫做異步模式。下面我將介紹幾種常見(jiàn)的異步模式在實(shí)際場(chǎng)景下的應(yīng)用。

并發(fā)交互模式

當(dāng)我們?cè)谕瑫r(shí)執(zhí)行多個(gè)異步任務(wù)時(shí),這些任務(wù)返回響應(yīng)結(jié)果的時(shí)間往往是不確定的,因而會(huì)產(chǎn)生以下兩種常見(jiàn)的需求:

  1. 多個(gè)異步任務(wù)同時(shí)執(zhí)行,等待所有任務(wù)都返回結(jié)果后才開(kāi)始進(jìn)行下一步的操作。

  2. 多個(gè)異步任務(wù)同時(shí)執(zhí)行,只返回最先完成異步操作的那個(gè)任務(wù)的結(jié)果然后再進(jìn)行下一步的操作。

場(chǎng)景一:

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

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

  2. 直接輸出第一個(gè)計(jì)算完 of 的個(gè)數(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ā)控制模式

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

場(chǎng)景二:

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

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ā)布 / 訂閱模式

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

我們先來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的發(fā)布訂閱對(duì)象:

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

場(chǎng)景三:

監(jiān)聽(tīng) watch 文件夾,當(dāng)里面的文件有改動(dòng)時(shí)自動(dòng)壓縮該文件并保存到 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
}

開(kāi)始監(jiān)聽(tīng) 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)
    }
})

以上就是關(guān)于“JavaScript異步編程的示例分析”的內(nèi)容,如果改文章對(duì)你有所幫助并覺(jué)得寫(xiě)得不錯(cuò),勞請(qǐng)分享給你的好友一起學(xué)習(xí)新知識(shí),若想了解更多相關(guān)知識(shí)內(nèi)容,請(qǐng)多多關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。


標(biāo)題名稱(chēng):JavaScript異步編程的示例分析
文章URL:http://weahome.cn/article/pdgpsd.html

其他資訊

在線(xiàn)咨詢(xún)

微信咨詢(xún)

電話(huà)咨詢(xún)

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部