這篇文章主要為大家展示了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主要應(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)的閱讀鏈接,希望能夠幫助到有需要的讀者。
瀏覽器端
Node 端
閱讀鏈接
解析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 })
在 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è)主要特征:
狀態(tài)總數(shù) ( state ) 是有限的
任一時(shí)刻,只處在一種狀態(tài)之中
某種條件下,會(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)的需求:
多個(gè)異步任務(wù)同時(shí)執(zhí)行,等待所有任務(wù)都返回結(jié)果后才開(kāi)始進(jìn)行下一步的操作。
多個(gè)異步任務(wù)同時(shí)執(zhí)行,只返回最先完成異步操作的那個(gè)任務(wù)的結(jié)果然后再進(jìn)行下一步的操作。
場(chǎng)景一:
同時(shí)讀取多個(gè)含有英文文章的 txt 文件內(nèi)容,計(jì)算其中單詞 of 的個(gè)數(shù)。
等待所有文件中的 of 個(gè)數(shù)計(jì)算完畢,再計(jì)算輸出總的 of 數(shù)。
直接輸出第一個(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è)資訊頻道。