# 温习异步
我们知道 Javascript 语言的执行环境是 单线程。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。
在开始之前请大家思考下,我们在前面的环节中大量使用了 async/await,那么它究竟是什么?
# 什么是异步?
异步指的是每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。这种模式虽然实现起来比较简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 Javascript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
# 回调实现异步
小明在看电视,但他肚子饿了。同步的做法是先吃饭后看电视。异步的做法是一边吃饭一边看电视。实现异步的本质是回调,我们使用 nodejs 的 fs 这个 API 接口来看一下异步与同步的区别:
const fs = require('fs')
// 读取文件
let txt1 = fs.readFileSync('./key.txt')
console.log(txt1.toString(), '1')
let txt2 = fs.readFile('./key.txt', (err, txt3) => {
console.log(txt3.toString(), '2')
})
console.log(txt2, '3')
运行结果如下, readFileSync 是一个同步方法,所以当我们打印 txt1 的时候立刻有返回值。而当我们打印 txt2 的时候可以看到返回值是一个 undefined,这说明没有返回值,而数据在 txt3 的回调里打印出来的。

在 NodeJs 中这种 API 太多了,异步是不能直接返回值的,当我们的项目大时又或者随着项目的不断迭代,久而久之嵌套就会越来越多,这个时候代码是你不愿意去维护也没法维护的,这还牵扯出来一个比较好听的名字,叫回调地狱,我们后面再讲该如何优化。
# 异步函数
我们来动手创建一个异步函数,在这里我们来通过 setTimeout 来模拟异步任务, setTimeout 也是回调,这种回调错误不好处理,默认第一个参数为错误,如果有第一个参数,则说明发生了错误,那么我们就要进行相应的处理。
function asynchronous(text, fn) {
setTimeout(() => {
fn(null, '-' + text + '-')
}, 3000)
}
asynchronous('hello nodejs', (err, text) => {
if(err) console.log(err)
console.log(text)
})
// 3s hello nodejs
# 事件实现异步
事件这个词汇相信大家都不陌生,在学习编程的道路上大家或多或少都接触过前端。在浏览器和页面交互时有各种事件,我们用的比较多的就是点击事件,比如当用户点击某个 HTML 元素时执行一段代码。而在 NodeJs 中,也有一个 events 的事件模块,事件的本质就是发布订阅设计模式。之所以能异步,还是因为回调。
class Evente {
private map: any = {}
// 监听器
public on(name: string, fn: Function) {
if (this.map[name]) {
this.map[name].push(fn)
return this
}
this.map[name] = [fn]
return this
}
// 发射器
public emit(name: string, ...args: any[]) {
if (this.map[name]) {
this.map[name].forEach((fn: Function) => {
fn(...args)
})
}
return this
}
}
我们以 name 作为 key 放到 map 里,也就是事件名称。当触发 emit 方法的时候,获得对应的回调进行调用:
const evente = new Evente()
evente.on('click', (error: string, result: any) => {
if (error) return console.log(`error: ${error}`)
console.log(`result: ${result}`)
})
.emit('click', '执行错误!')
.emit('click', null, '执行成功!')
NodeJs 里不少模块都是基于事件模块构建的,因为事件的里面还是有回调,对于处理错误而言,还是把第一个参数作为错误传递。

我们来使用事件模式重新完成我们一开始的例子:
const evente = new Evente()
evente.on('readFile', (error: string, result: any) => {
if (error) return console.log(`error: ${error}`)
console.log(`result: ${result}`)
})
fs.readFile('./key.txt', (err: any, data: any) => {
evente.emit('readFile', err, data)
})
好像变的更麻烦了一样,对于简单的异步我们没有必要使用事件来完成异步,在这里只是为大家做演示。
# 观察者模式实现异步
观察者模式或者说订阅模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
function observableCreate(fn: Function) {
let ret = false
return ({
next,
complete,
error,
}: any) => {
// 通知:传递给观察者的实际数据
function nextFn(...args: any[]) {
if (ret) return
next(...args)
}
// 通知:按理来说这步不会发送任何值
function completeFn(...args: any[]) {
complete(...args)
ret = true
}
// 通知:发送一个错误或异常
function errorFn(...args: any[]) {
error(...args)
}
fn({
next: nextFn, complete: completeFn, error: errorFn
})
return () => (ret = true)
}
}
当订阅下面代码中的 Observable 的时候会立即(同步地)推送值1、2、3,然后1秒后会推送值4、5,再然后是完成流,因为 complete 调用之后,把 ret 设置为 true,complate 的字面意思就是完成,完成了之后 next 就不会再生效了,有错误的时候用 error 发出即可。
const observerable = observableCreate((observer: any) => {
try {
observer.next(1)
observer.next(2)
observer.next(3)
setTimeout(() => {
observer.next(4)
observer.complete('完成!')
}, 1000)
}catch(err) {
observer.error(err) // 如果捕获到异常会发送一个错误
}
})
要调用 observerable 并看到这些值,我们需要订阅 observerable:
const subject = {
next: (value: any) => console.log(value, 'next'),
complete: (value: any) => console.log(value, 'complate'),
error: (value: any) => console.log(value, 'error')
}
const unsubscribe = observerable(subject)
# Primise 实现异步
Primise 是 ES6 的新特性,为了处理异步而生,本质还是回调。只不过 Promise 比传统的回调和事件更合理且更强大。ES6 将其写入了语言标准,统一了用法,所以我们通常会使用 Promise 来实现异步编程。
// Promise 接受一个函数作为参数,该函数的两个参数分别的是 resolve:成功,reject:失败
const promise = new Promise((resolve, reject) => {
if (/* 条件满足 */) return resolve(result)
reject(error)
})
Promise 实例生成后,可用then 和 catch 方法分别指定两种状态回调参数:
promise.then(console.log)
.catch(console.log)
# async/await 大杀器
相信大家也注意到了我们在实战环节中,都是使用的 async/await , ES7 标准引入了 async 函数,使得异步操作变得更加方便,async 其实本质是Generator函数的语法糖。async/await 应该是目前最简单的异步解决方案了,我们先来看个例子:
const sleep = (time: number) => {
return new Promise(function (resolve: any, reject: any) {
setTimeout(() => {
resolve();
}, time);
})
};
const start = async() => {
console.log('start');
await sleep(3000);
console.log('end');
};
start();
async 表示 是一个异步函数,await 只能用在这个函数里面。await 表示在这里等待 Promise 返回结果了,再继续执行。await 后面跟着的应该是一个 Promise 对象。
← 邮件发送 跨域身份验证_JWT 权限验证 →