您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了JavaScript異步編程的示例分析,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶大家一起來研究并學習一下“JavaScript異步編程的示例分析”這篇文章吧。
Java主要應用于:1. web開發;2. Android開發;3. 客戶端開發;4. 網頁開發;5. 企業級應用開發;6. Java大數據開發;7.游戲開發等。
前言
自己著手準備寫這篇文章的初衷是覺得如果想要更深入的理解 JS,異步編程則是必須要跨過的一道坎。由于這里面涉及到的東西很多也很廣,在初學 JS 的時候可能無法完整的理解這一概念,即使在現在來看還是有很多自己沒有接觸和理解到的知識點,但是為了跨過這道坎,我仍然愿意鼓起勇氣用我已經掌握的部分知識盡全力講述一下 JS 中的異步編程。如果我所講的一些概念或術語有錯誤,請讀者向我指出問題所在,我會立即糾正更改。
同步與異步
我們知道無論是在瀏覽器端還是在服務器 ( Node ) 端,JS 的執行都是在單線程下進行的。我們以瀏覽器中的 JS 執行線程為例,在這個線程中 JS 引擎會創建執行上下文棧,之后我們的代碼就會作為執行上下文 ( 全局、函數、eval ) 像一系列任務一樣在執行上下文棧中按照后進先出 ( LIFO ) 的方式依次執行。而同步最大的特性就是會阻塞后面任務的執行,比如此時 JS 正在執行大量的計算,這個時候就會使線程阻塞從而導致頁面渲染加載不連貫 ( 在瀏覽器端的 Event Loop 中每次執行棧中的任務執行完畢后都會去檢查并執行事件隊列里面的任務直到隊列中的任務為空,而事件隊列中的任務又分為微隊列與宏隊列,當微隊列中的任務執行完后才會去執行宏隊列中的任務,而在微隊列任務執行完到宏隊列任務開始之前瀏覽器的 GUI 線程會執行一次頁面渲染 ( UI rendering ),這也就解釋了為什么在執行棧中進行大量的計算時會阻塞頁面的渲染 ) 。
與同步相對的異步則可以理解為在異步操作完成后所要做的任務,它們通常以回調函數或者 Promise 的形式被放入事件隊列,再由事件循環 ( Event Loop ) 機制在每次輪詢時檢查異步操作是否完成,若完成則按事件隊列里面的執行規則來依次執行相應的任務。也正是得益于事件循環機制的存在,才使得異步任務不會像同步任務那樣完全阻塞 JS 執行線程。
異步操作一般包括 網絡請求 、文件讀取 、數據庫處理
異步任務一般包括 setTimout / setInterval 、Promise 、requestAnimationFrame ( 瀏覽器獨有 ) 、setImmediate ( Node 獨有 ) 、process.nextTick ( Node 獨有 ) 、etc ...
注意: 在瀏覽器端與在 Node 端的 Event Loop 機制是有所不同的,下面給出的兩張圖簡要闡述了在不同環境下事件循環的運行機制,由于 Event Loop 不是本文內容的重點,但是 JS 異步編程又是建立在它的基礎之上的,故在下面給出相應的閱讀鏈接,希望能夠幫助到有需要的讀者。
瀏覽器端
Node 端
閱讀鏈接
解析Node.js的事件循環機制
詳解javascript瀏覽器的事件循環機制
為異步而生的 JS 語法
回望歷史,在最近幾年里 ECMAScript 標準幾乎每年都有版本的更新,也正是因為有像 ES6 這種在語言特性上大版本的更新,到了現今的 8102 年, JS 中的異步編程相對于那個只有回調函數的遠古時代有了很大的進步。下面我將介紹 callback 、Promise 、generator 、async / await 的基本用法以及如何在異步編程中使用它們。
callback
回調函數并不算是 JS 中的語法但它卻是解決異步編程問題中最常用的一種方法,所以在這里有必要提出來,下面舉一個例子,大家看一眼就懂。
const foo = function (x, y, cb) { setTimeout(() => { cb(x + y) }, 2000) } // 使用 thunk 函數,有點函數柯里化的味道,在最后處理 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 沒有發布之前,作為異步編程主力軍的回調函數一直被人詬病,其原因有太多比如回調地獄、代碼執行順序難以追蹤、后期因代碼變得十分復雜導致無法維護和更新等,而 Promise 的出現在很大程度上改變了之前的窘境。話不多說先直接上代碼提前感受下它的魅力,然后我再總結下自己認為在 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') // 某購物系統獲取用戶必要信息后執行收費操作 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 發送一次請求卻變成了多次,相應的我們的 chargeUser 函數也就是收費操作就會被執行多次,最終導致用戶被多次收費,這樣的結果完全就是噩夢!然而這就是控制反轉,即把自己的代碼交給第三方掌控,因此是不可完全信任的。
那么反控制反轉現在我們可以猜測它的含義應該就是將控制權交還到我們自己寫的代碼中,而要實現這點通常我們會引入一個第三方協商機制,在 Promise 之前我們會通過事件監聽的形式來解決這類問題。現在我們將代碼更改如下:
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')
更改代碼之后我們會發現控制反轉的恢復其實是更好的實現了關注點分離,我們不用去關心 purchase 函數內部具體發生了什么,只需要知道它在什么時候完成,之后我們的關注點就從 purchase 函數轉移到了 listener 對象上。我們可以把 listener 對象提供給代碼中多個獨立的部分,在 purchase 函數完成后,它們同樣也能收到通知并進行下一步的操作。以下是維基百科上關于關注點分離的一部分介紹。
關注點分離的價值在于簡化計算機程序的開發和維護。當關注點分開時,各部分可以重復使用,以及獨立開發和更新。具有特殊價值的是能夠稍后改進或修改一段代碼,而無需知道其他部分的細節必須對這些部分進行相應的更改。
一一 維基百科
顯然在 Promise 中 new Promise() 返回的對象就是關注點分離中分離出來的那個關注對象。
要點二:不可變性 ( 值得信任 )
細心的讀者可能會發現,要點一中基于事件監聽的反控制反轉仍然沒有解決最重要的信任問題,收費操作仍舊可以因為第三方 API 的多次調用而被觸發且執行多次。幸運的是現在我們擁有 Promise 這樣強大的機制,才得以讓我們從信任危機中解脫出來。所謂不可變性就是:
Promise 只能被決議一次,如果代碼中試圖多次調用 resolve(..) 或者 reject(..) ,Promise 只會接受第一次決議,決議后就是外部不可變的值,因此任何通過 then(..) 注冊的回調只會被調用一次。
現在要點一中的示例代碼就可以最終更改為:
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) })
要點三:錯誤處理及一些細節
還記得最開始講 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(..) ,它的第一個參數作為函數接收 promise 對象中 resolve(..) 的值,第二個參數則作為錯誤處理函數處理在 Promise 中可能發生的錯誤。
而在 Promise 中有兩種錯誤可能會出現,一種是顯式 reject(..) 拋出的錯誤,另一種則是代碼自身有錯誤會被 Promise 捕捉,通過 then(..) 中的錯誤處理函數我們可以接收到它前面 promise 對象中出現的錯誤,而如果在 then(..) 接收 resolve(..) 值的函數中也出現錯誤,該錯誤則會被下一個 then(..) 的錯誤處理函數所接收 ( 有兩個前提,第一是要寫出這個 then(..) 否則該錯誤最終會在全局拋出,第二個則是要確保前一個 then(..) 在它的 Promise 決議后調用的是第一個參數即接收 resolve(..) 值的函數而不是錯誤處理函數 )。
一些值得注意的細節:
catch(..) 相當于 then(..) 中的錯誤處理函數 ,只是省略了第一個參數。
finally(..) 在 Promise 一旦決議后 ( 無論是 resolve 還是 reject ) 都會被執行。
then(..) 、catch(..) 、finally(..) 都是異步調用,作為 Event Loop 里事件隊列中的微隊列任務執行。
generator
generator 也叫做生成器,它是 ES6 中引入的一種新的函數類型,在函數內部它可以多次啟動和暫停,從而形成阻塞同步的代碼。下面我將先講述它的基本用法然后是它在異步編程中的使用最后會簡單探究一下它的工作原理。
生成器基本用法
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
從上面的代碼我們可以看到與普通的函數不同,生成器函數執行后返回的是一個迭代器對象,用來控制生成器的暫停和啟動。在常見的設計模式中就有一種模式叫做迭代器模式,它指的是提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示。迭代器對象 it 包含一個 next(..) 方法且在調用之后返回一個 { done: .. , value: .. } 對象,現在我們先來自己實現一個簡單的迭代器。
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
可以看到我們自己實現的迭代器不僅能夠手動進行迭代,還能被 for..of 自動迭代展開,這是因為在 ES6 中只要對象具有 Symbol.iterator 屬性且該屬性返回的是一個迭代器對象,就能夠被 for..of 所消費。
回頭來看最開始的那個 generator 示例代碼中生成器產生的迭代器對象 it ,似乎它比普通的迭代器有著更強大的功能,其實就是與 yield 表達式緊密相連的消息雙向傳遞。現在我先來總結一下自己認為在生成器中十分重要的點,然后再來分析下那段示例代碼的完整執行過程。
每次調用 it.next() 后生成器函數內的代碼就會啟動執行且返回一個 { done: .. , value: .. } 對象,一旦遇到 yield 表達式就會暫停執行,如果此時 yield 表達式后面跟有值例如 yield val,那么這個 val 就會被傳入返回對象中鍵名 value 對應的鍵值,當再次調用 it.next() 時 yield 的暫停效果就會被取消,如果此時的 next 為形如 it.next(val) 的調用,yield 表達式就會被 val 所替換。這就是生成器內部與迭代器對象外部之間的消息雙向傳遞。
弄清了生成器中重要的特性后要理解開頭的那段代碼就不難了,首先執行第一個 it.next().value ,遇到第一個 yield 后生成器暫停執行,此時變量 x 接受到的值為 6。在全局環境下執行 a++ 后再次執行 it.next(x * 5).value 生成器繼續執行且傳入值 30,因此變量 b 的值就為 33,當遇到第二個 yield 后生成器又暫停執行,并且將值 8 傳出給變量 y 。再次執行 a++ ,然后執行 it.next(x + y) 恢復生成器執行并傳入值 14,此時變量 c 的值就為 47,最終計算 a + b + c 便可得到值 84。
在異步編程中使用生成器
既然現在我們已經知道了生成器內部擁有能夠多次啟動和暫停代碼執行的強大能力,那么將它用于異步編程中也便是理所當然的事情了。先來看一個異步迭代生成器的例子。
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()
這個例子的邏輯很簡單,調用 it.next() 后生成器啟動,遇到 yield 時生成器暫停運行,但此時 foo 函數已經執行即網絡請求已經發出,等到有響應結果時如果出錯則調用 it.throw(err) 將錯誤拋回生成器內部由 try..catch 同步捕獲,否則將返回的 data 作為傳回生成器的值在恢復執行的同時將 data 賦值給變量 result ,最后打印 result 得到我們想要的結果。
在 ES6 中最完美的世界就是生成器 ( 看似同步的異步代碼 ) 和 Promise ( 可信任可組合 ) 的結合,因此我們現在再來看一個由生成器 + Promise 實現異步操作的例子。
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(..) 中來恢復執行生成器里下一步的操作或是拋出一個錯誤。
生成器工作原理
在講了那么多關于 generator 生成器的使用后,相信讀者也跟我一樣想知道生成器究竟是如何實現能夠控制函數內部代碼的暫停和啟動,從而形成阻塞同步的效果。
我們先來簡單了解下有限狀態機 ( FSM ) 這個概念,維基百科上給出的解釋是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。簡單的來說,它有三個主要特征:
狀態總數 ( state ) 是有限的
任一時刻,只處在一種狀態之中
某種條件下,會從一種狀態轉變 ( transition ) 到另一種狀態
其實生成器就是通過暫停自己的作用域 / 狀態來實現它的魔法的,下面我們就以上文的生成器 + Promise 的例子為基礎,用有限狀態機的方式來闡述生成器的基本工作原理。
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 !')
在這里我使用了行為委托模式和狀態模式實現了一個簡單的有限狀態機,而它卻展現了生成器中核心部分的工作原理,下面我們來逐步分析它是如何運行的。
首先這里我們自己創建的 it 對象就相當于生成器函數執行后返回的迭代器對象,我們把上文生成器 + Promise 示例中的 main 函數代碼分為了三個狀態并將跟該狀態有關的行為封裝到了 stateRequest 、stateResult 、stateError 三個對象中。然后我們再調用 init(..) 將 it 對象上的行為委托到這三個對象上并初始化當前的狀態對象。在準備工作完成后調用 next(..) 啟動生成器,這個時候我們就進入了狀態一,即執行 foo 函數發出網絡請求。在 foo 函數內部當得到請求響應數據后就執行 it.next(data) 觸發狀態機內部的狀態改變,此時執行狀態二內部的代碼即打印網絡請求返回的結果。如果網絡請求中出現錯誤就會執行 it.throw(err) ,這個時候的狀態就會轉換到狀態三即錯誤處理狀態。
在這里我們似乎忽略了一個很重要的地方,就是生成器是如何做到將其內部的代碼分為多個狀態的,當然我們知道這肯定是 yield 表達式的功勞,但是其內部又是怎么實現的呢?由于本人能力還不夠,而且還有很多東西來不及去學習和了解,因此暫時無法解決這個問題,但我還是愿意把這個問題提出來,如果讀者確實有興趣能夠通過查閱資料找到答案或者已經知道它的原理還是可以分享出來,畢竟經歷這樣刨根問底的過程還是滿有趣的。
async / await
終于講到最后一個異步語法了,作為壓軸的身份出場,據說 async / await 是 JS 異步編程中的終極解決方案。話不多說,先直接上代碼看看它的基本用法,然后我們再來探討一下它的實現原理。
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 函數跟生成器函數極為相似,只是將之前的 * 變成了 async ,yield 變成了 await 。其實它就是一個能夠自動執行的 generator 函數,我們不用再通過手動執行 it.next(..) 來控制生成器函數的暫停與啟動。
await 幫我們做到了在同步阻塞代碼的同時還能夠監聽 Promise 對象的決議,一旦 promise 決議,原本暫停執行的 async 函數就會恢復執行。這個時候如果決議是 resolve ,那么返回的結果就是 resolve 出來的值。如果決議是 reject ,我們就必須用 try..catch 來捕獲這個錯誤,因為它相當于執行了 it.throw(err) 。
下面直接給出一種主流的 async / await 語法版本的實現代碼:
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 函數執行后返回的是一個 Promise 對象,然后使用遞歸的方法去自動執行生成器函數的暫停與啟動。如果調用 it.next().value 傳出來的是一個 promise ,則用 Promise.resolve() 方法將其異步展開,當這個 promise 決議時就可以重新啟動執行生成器函數或者拋出一個錯誤被 try..catch 所捕獲并最終在 async 函數返回的 Promise 對象的錯誤處理函數中處理。
關于 async / await 的執行順序
下面給出一道關于 async / await 執行順序的經典面試題,網上給出的解釋給我感覺似乎很含糊。在這里我們結合上文所講的 generator 函數運行機制和 async / await 實現原理來具體闡述下為什么執行順序是這樣的。
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
其實最主要的地方還是要分清在執行棧中同步執行的任務與事件隊列中異步執行的任務。首先我們執行同步任務,打印 script start ,調用函數 async1 ,在我們遇到 await 表達式后就會暫停函數 async1 的執行。因為在這里它相當于 yield async2() ,根據上文的 async / await 原理實現代碼可以看出,當自動調用 it.next() 時遇到第一個 yield 后會暫停執行,但此時函數 async2 已經執行。上文還提到過 async 函數在執行完后會返回一個 Promise 對象,故此時 it.next().value 的值就是一個 promise 。接下來要講的就是重點啦 !!!
我們用 Promise.resolve() 去異步地展開一個 promise ,因此第一個放入事件隊列中的微隊列任務其實就是這個 promise 。之后我們再繼續運行執行棧中剩下的同步任務,此時打印出 promise1 和 script end ,同時第二個異步任務被加入到事件隊列中的微隊列。同步的任務執行完了,現在來執行異步任務,首先將微隊列中第一個放入的那個 promise 拿到執行棧中去執行,這個時候之前 Promise.resolve() 后面注冊的回調任務才會作為第三個任務加入到事件隊列中的微隊列里去。然后我們執行微隊列中的第二個任務,打印 promise2,再執行第三個任務即調用 step(() => it.next(val)) 恢復 async 函數的執行,打印 async1 end 。最后,因為微隊列總是搶占式的在宏隊列之前插入執行,故只有當微隊列中沒有了任務以后,宏隊列中的任務才會開始執行,故最終打印出 setTimeout 。
常見異步模式
在軟件開發中有著設計模式這一專業術語,通俗一點來講設計模式其實就是在某種場合下針對某個問題的一種解決方案。
在 JS 異步編程的世界里,很多時候我們也會遇到因為是異步操作而出現的特定問題,而針對這些問題所提出的解決方案 ( 邏輯代碼 ) 就是異步編程的核心,似乎在這里它跟設計模式的概念很相像,所以我把它叫做異步模式。下面我將介紹幾種常見的異步模式在實際場景下的應用。
并發交互模式
當我們在同時執行多個異步任務時,這些任務返回響應結果的時間往往是不確定的,因而會產生以下兩種常見的需求:
多個異步任務同時執行,等待所有任務都返回結果后才開始進行下一步的操作。
多個異步任務同時執行,只返回最先完成異步操作的那個任務的結果然后再進行下一步的操作。
場景一:
同時讀取多個含有英文文章的 txt 文件內容,計算其中單詞 of 的個數。
等待所有文件中的 of 個數計算完畢,再計算輸出總的 of 數。
直接輸出第一個計算完 of 的個數。
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)) })
并發控制模式
有時候我們會遇到大量異步任務并發執行而且還要處理返回數據的情況,即使擁有事件循環 ( Event Loop ) 機制,在并發量過高的情況下程序仍然會崩潰,所以這個時候就應該考慮并發控制。
場景二:
利用 Node.js 實現圖片爬蟲,控制爬取時的并發量。一是防止 IP 被封掉 ,二是防止并發請求量過高使程序崩潰。
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) }) })
發布 / 訂閱模式
我們假定,存在一個"信號中心",當某個任務執行完成,就向信號中心"發布" ( publish ) 一個信號,其他任務可以向信號中心"訂閱" ( subscribe ) 這個信號,從而知道什么時候自己可以開始執行,當然我們還可以取消訂閱這個信號。
我們先來實現一個簡單的發布訂閱對象:
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
場景三:
監聽 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 }
開始監聽 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異步編程的示例分析”的內容,如果改文章對你有所幫助并覺得寫得不錯,勞請分享給你的好友一起學習新知識,若想了解更多相關知識內容,請多多關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。