本身着手准备写这篇文章的初衷是以为若是想要更深刻的理解 JS,异步编程则是必需要跨过的一道坎。因为这里面涉及到的东西不少也很广,在初学 JS 的时候可能没法完整的理解这一律念,即便在如今来看仍是有不少本身没有接触和理解到的知识点,可是为了跨过这道坎,我仍然愿意鼓起勇气用我已经掌握的部分知识尽全力讲述一下 JS 中的异步编程。若是我所讲的一些概念或术语有错误,请读者向我指出问题所在,我会当即纠正更改。javascript
咱们知道不管是在浏览器端仍是在服务器 ( Node ) 端,JS 的执行都是在单线程下进行的。咱们以浏览器中的 JS 执行线程为例,在这个线程中 JS 引擎会建立执行上下文栈,以后咱们的代码就会做为执行上下文 ( 全局、函数、eval ) 像一系列任务同样在执行上下文栈中按照后进先出 ( LIFO ) 的方式依次执行。而同步最大的特性就是会阻塞后面任务的执行,好比此时 JS 正在执行大量的计算,这个时候就会使线程阻塞从而致使页面渲染加载不连贯 ( 在浏览器端的 Event Loop 中每次执行栈中的任务执行完毕后都会去检查并执行事件队列里面的任务直到队列中的任务为空,而事件队列中的任务又分为微队列与宏队列,当微队列中的任务执行完后才会去执行宏队列中的任务,而在微队列任务执行完到宏队列任务开始以前浏览器的 GUI 线程会执行一次页面渲染 ( UI rendering ),这也就解释了为何在执行栈中进行大量的计算时会阻塞页面的渲染 ) 。html
与同步相对的异步则能够理解为在异步操做完成后所要作的任务,它们一般以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操做是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。也正是得益于事件循环机制的存在,才使得异步任务不会像同步任务那样彻底阻塞 JS 执行线程。前端
异步操做通常包括 网络请求 、文件读取 、数据库处理java
异步任务通常包括 setTimout / setInterval 、Promise 、requestAnimationFrame ( 浏览器独有 ) 、setImmediate ( Node 独有 ) 、process.nextTick ( Node 独有 ) 、etc ...node
注意: 在浏览器端与在 Node 端的 Event Loop 机制是有所不一样的,下面给出的两张图简要阐述了在不一样环境下事件循环的运行机制,因为 Event Loop 不是本文内容的重点,可是 JS 异步编程又是创建在它的基础之上的,故在下面给出相应的阅读连接,但愿可以帮助到有须要的读者。ios
浏览器端git
Node 端github
阅读连接面试
回望历史,在最近几年里 ECMAScript 标准几乎每一年都有版本的更新,也正是由于有像 ES6 这种在语言特性上大版本的更新,到了现今的 8102 年, JS 中的异步编程相对于那个只有回调函数的远古时代有了很大的进步。下面我将介绍 callback 、Promise 、generator 、async / await 的基本用法以及如何在异步编程中使用它们。数据库
回调函数并不算是 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 里事件队列中的微队列任务执行。
补充:手写一个 Promise
function iPromise(fn) {
let state = 'pending',
value = null,
error = null,
callbacks = []
this.then = function (onFulfilled, onRejected) {
return new iPromise((resolve, reject) => {
transition({
onFulfilled: onFulfilled,
onRejected: onRejected,
resolve: resolve,
reject: reject
})
})
}
function transition(callback) {
let result
switch (state) {
case 'pending':
callbacks.push(callback)
return
case 'resolved':
try {
if (callback.onFulfilled) result = callback.onFulfilled(value)
} catch (e) {
if (callback.onRejected) result = callback.onRejected(e)
}
break
case 'rejected':
if (callback.onRejected) result = callback.onRejected(error)
break
}
if (result instanceof iPromise) {
result.then(callback.resolve, callback.reject)
return
}
state === 'resolved' ? callback.resolve(result) : callback.reject(result)
}
function resolve(newValue) {
state = 'resolved'
value = newValue
execute()
}
function reject(err) {
state = 'rejected'
error = err
execute()
}
function execute() {
callbacks.length ? callbacks.map(callback => transition(callback)) : null
}
fn(resolve, reject)
}
var p = new iPromise((resolve) => {
setTimeout(() => resolve(2333), 1000)
})
p.then(res =>
new iPromise((resolve) => {
setTimeout(() => {
resolve(res)
}, 2000)
})
).then(res =>
new iPromise((resolve, reject) => {
reject(res)
})
).then(null, err => console.error(err)) // 2333
复制代码
能够看到实现 Promise 的关键就是为其设置 pending
、resolved
、rejected
三种状态,并且只能由 pending
转换到 resolved
或者 rejected
。须要注意的是咱们用 then(..)
注册的那些回调函数早在执行同步代码的时候就已经被缓存在对应 Promise 中的 callbacks
数组里 ( 若是此时的状态为 pending
),当异步操做完成后咱们执行从 Promise 传递出来的 resolve
或者 rejected
函数去触发 callbacks
数组中相应函数的执行。咱们还会发现 then(..)
方法是链式调用的,即在 Promise 内部当前一个 Promise 的 then(..)
注册的回调函数执行完后就会自动调用下一个 Promise 中的 resolve
函数,而后再去执行该 Promise 中 callbacks
数组里缓存的回调函数。
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 ) 这个概念,维基百科上给出的解释是表示有限个状态以及在这些状态之间的转移和动做等行为的数学模型。简单的来讲,它有三个主要特征:
其实生成器就是经过暂停本身的做用域 / 状态来实现它的魔法的,下面咱们就以上文的生成器 + 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 是 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 异步编程在这里我就讲这么多了,固然还有不少东西本身没有了解和学习到,所以在本篇文章中没有涉及。最后仍是给出上面三个场景代码的 GitHub 地址 ,总之在前端学习的路上还得继续加油嘞 😄。
参考书籍及文章