首頁  >  文章  >  web前端  >  JavaScript非同步程式設計的詳細介紹(附範例)

JavaScript非同步程式設計的詳細介紹(附範例)

不言
不言轉載
2018-10-20 16:39:392514瀏覽

這篇文章帶給大家的內容是關於php協成實現的詳解(附程式碼),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

前言

自己著手準備寫這篇文章的初衷是覺得如果想要更深入的理解 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 非同步程式設計又是建立在它的基礎之上的,故在下面給出相應的閱讀鏈接,希望能夠幫助到有需要的讀者。

瀏覽器端

JavaScript非同步程式設計的詳細介紹(附範例)

#Node 端

JavaScript非同步程式設計的詳細介紹(附範例)

#閱讀連結

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

Promise

在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 <p>可以看到我们自己实现的迭代器不仅能够手动进行迭代,还能被 for..of 自动迭代展开,这是因为在 ES6 中只要对象具有 Symbol.iterator 属性且该属性返回的是一个迭代器对象,就能够被 for..of 所消费。</p><p>回头来看最开始的那个 generator 示例代码中生成器产生的迭代器对象 it ,似乎它比普通的迭代器有着更强大的功能,其实就是与 yield 表达式紧密相连的消息双向传递。现在我先来总结一下自己认为在生成器中十分重要的点,然后再来分析下那段示例代码的完整执行过程。</p><p>每次调用 it.next() 后生成器函数内的代码就会启动执行且返回一个 { done: .. , value: .. } 对象,一旦遇到 yield 表达式就会暂停执行,如果此时 yield 表达式后面跟有值例如 yield val,那么这个 val 就会被传入返回对象中键名 value 对应的键值,当再次调用 it.next() 时 yield 的暂停效果就会被取消,如果此时的 next 为形如 it.next(val) 的调用,yield 表达式就会被 val 所替换。这就是生成器内部与迭代器对象外部之间的消息双向传递。</p><p>弄清了生成器中重要的特性后要理解开头的那段代码就不难了,首先执行第一个 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。</p><p><strong>在异步编程中使用生成器</strong></p><p>既然现在我们已经知道了生成器内部拥有能够多次启动和暂停代码执行的强大能力,那么将它用于异步编程中也便是理所当然的事情了。先来看一个异步迭代生成器的例子。</p><pre class="brush:php;toolbar:false">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 ) 这个概念,维基百科上给出的解释是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。简单的来说,它有三个主要特征:

  1. 状态总数 ( state ) 是有限的

  2. 任一时刻,只处在一种状态之中

  3. 某种条件下,会从一种状态转变 ( 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 异步编程的世界里,很多时候我们也会遇到因为是异步操作而出现的特定问题,而针对这些问题所提出的解决方案 ( 逻辑代码 ) 就是异步编程的核心,似乎在这里它跟设计模式的概念很相像,所以我把它叫做异步模式。下面我将介绍几种常见的异步模式在实际场景下的应用。

并发交互模式

当我们在同时执行多个异步任务时,这些任务返回响应结果的时间往往是不确定的,因而会产生以下两种常见的需求:

  1. 多个异步任务同时执行,等待所有任务都返回结果后才开始进行下一步的操作。

  2. 多个异步任务同时执行,只返回最先完成异步操作的那个任务的结果然后再进行下一步的操作。

场景一:

同时读取多个含有英文文章的 txt 文件内容,计算其中单词 of 的个数。

  1. 等待所有文件中的 of 个数计算完毕,再计算输出总的 of 数。

  2. 直接输出第一个计算完 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  {
                        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 异步编程在这里我就讲这么多了,当然还有很多东西自己没有了解和学习到,因此在本篇文章中没有涉及。最后还是给出上面三个场景代码的 GitHub 地址 ,总之在前端学习的路上还得继续加油嘞 。

以上是JavaScript非同步程式設計的詳細介紹(附範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除