我们知道 JavaScript 语言的执行环境是“单线程”,代码是一行接着一行、一个函数接着一个函数的执行,不能跳着执行。倘若其中一个方法耗时很长,后面的代码都要等待,这种执行模式叫做“同步”。
但是等待的这段时间,CPU通常是空闲的,其实可以用来执行后面的代码的。于是就有了“异步”这种执行模式的诞生,典型的像代码执行到 ajax 请求时,并不等待返回结果,而是接着往下执行,等到 ajax 请求拿到结果后再执行回调中的代码。
js 异步编程有多种方式,本文总结了常用的几种方式:
回调函数
这是异步编程最基本的方式,如 ajax 请求
axios.get('xxx', (res) => {
console.log('执行回调函数')
})
事件监听
另外一种思路是通过事件驱动,例如可以监听 ModuleA 的某一事件,当 ModuleA 触发该事件时,就执行 ModuleB 的某一方法。原理如图所示:
用代码表示大致如下:
ModuleA.on('eventSay', ModuleB.say); // 监听 ModuleA 的 eventSay 事件,当该事件被触发时,调用 ModuleB 的 say 方法
setTimeout(() => {
ModuleA.trigger('eventSay'); // 触发 ModuleA 的 eventSay 事件
}, 1000)
优点:比较直观,有利于模块化
缺点:当众多模块之间相互监听时,将会形成复杂的关系网,程序的执行流程将变得混乱不行,代码也将难以维护。
发布/订阅
这种模式和事件监听很相似,但又做了明显的优化。模块之间不再是直接监听,而是全部通过消息中心去调度,这样模块之间就完全解耦了,代码的执行逻辑也变得清晰明了。
如图所示,ModuelB 监听了消息中心的两个事件 eventSay 和 eventEat,当 ModuleA 或 ModuleC 完成了自己的异步操作后,把相应的事件 push 到消息中心,此时便可以触发 ModuleB 中的方法。这样一来各个模块之间并没有直接的关系,降低了模块间的耦合度。
Promise
Promise 的本意是承诺,承诺过一段时间会给出结果,这个时间通常是指异步调用之后。
它的出现是为了解决回调场景中出现的多层回调函数嵌套的问题,我们称之为“回调地狱”。比起回调,Promise 在用法上更加优雅。
Promise 有三种状态:
- Pending:Promise 实例在创建之初的初始状态
- Fullfilled:成功状态
- Rejected:失败状态
这个状态只能从 Pending -> Fullfilled,或者从 Pending -> Rejected,且不可逆。
用法
例如我们通常会将调用后端接口的方法写在 service 层。
function getUserInfo () {
return new Promise((resolve, reject) => {
ajax('/api/userinfo', (info) => {
if (info.success === true) {
resolve(info)
} else {
reject('获取用户信息错误')
}
})
})
}
view 层也不再需要将处理方法传入 service
function renderUserInfo (info) {
document.body = info.username;
}
service.getUserInfo().then(info => {
renderUserInfo(info)
}).catch(msg => {
alert(msg)
})
我们可以看到 Promise 是一个构造函数,参数为一个函数,这个函数接收两个参数 resolve 和 reject。真正的异步调用会被封装在这个函数内部,当异步调用成功时,调用 resolve,进入 then 函数。失败时调用 reject,进入catch。
也就是说 .then 其实就是注册异步成功时的回调,也就是 resolve 函数。
.catch 其实就是注册异步失败时的回调,也就是 reject 函数。
自己写一个 Promise
现在我们知道了 Promise 的功能之后,我们尝试自己写一个 Promise 方法
class Promise {
constructor (exector) {
this.value = null;
this.reason = null;
this.status = 'pending';
this.resolveCallback = [];
this.rejectCallback = [];
try {
// 传入 Promise 的函数是需要立即执行的
exector(this.resolve.bind(this), this.reject.bind(this))
} catch (e) {
console.log(e)
}
}
then (successFunc, errorFunc) {
if (this.status === 'fullfilled') {
// 如果 exector 内部没有异步调用,则会先执行到 resolve 函数,状态变成 fullfiled,再执行 then 函数。这时候就不是把 successFunc 注册到 resolveCallback 队列里了,而是需要直接执行
successFunc(this.value)
}
if (this.status === 'rejected') {
// 道理同上
errorFunc(this.reason)
}
if (this.status === 'pending') {
// exector 里有异步调用的话,则会先走到这里
this.resolveCallback.push(successFunc)
this.rejectCallback.push(errorFunc)
}
}
catch (func) {
this.rejectCallback.push(func)
}
resolve (res) {
if (this.status === 'pending') {
this.status = 'fullfilled';
this.value = res;
while(this.resolveCallback.length) {
// 之所以用 shift,是为了把内存释放,因为一个 Promise 实例生命周期内只会执行一次 resolve 方法,执行完之后这个 Promise 实例也就没用了,加入这个时候 resolveCallback 还存在着外部一个函数的引用,那么这个 Promise 实例就不能够被释放,空耗内存。
this.resolveCallback.shift()()
}
}
}
reject (e) {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = e;
while (this.rejectCallback.length) {
// 道理同上
this.rejectCallback.shift()()
}
}
}
}
好了,这就基本完成了一个简易的 Promise 类了。可是我们看到 Promise 是可以链式调用的,then 之后还能 then,所以可以确定的是,then 函数里需要返回一个 Promise 实例,让我们来改造一下这个 then
then (successFunc, errorFunc) {
return new Promise((resolve, reject) => {
if (this.status === 'fullfilled') {
x = successFunc(this.value)
if (x instanceof Promise) {
x.then(v => resolve(v))
} else {
resolve(x)
}
}
if (this.status === 'rejected') {
x = errorFunc(this.reason)
if (x instanceof Promise) {
x.then(v => resolve(v))
} else {
reject(x)
}
}
if (this.status === 'pending') {
// exector 里有异步调用的话,则会先走到这里
this.resolveCallback.push(() => {
x = successFunc(this.value)
if (x instanceof Promise) {
x.then(v => resolve(v))
} else {
resolve(x)
}
})
...
}
})
}
在 then 方法中返回一个全新的 Promise 实例,这样就可以完成我们想要的链式调用。
先判断一下前一个 then 函数里注册的回调函数执行结果是不是一个 Promise 实例,如果是,那么等到这个 Promise 的状态改变后,再去执行下一个 then 函数注册的回调函数。
如此一来,原先需要相互一来的一个异步调用的“回调地狱”写法,就可以写成 promise.then().then().then()…这样的链式调用,代码是不是优雅了很多呢!
此外,Promise 还有另外一个原型链上的方法也一并贴在这里
class Promise {
...
// 等所有的异步任务都成功时,才算成功
static all (list) {
return new Promise((resolve, reject) => {
let count = 0;
let res = []
list.forEach((item,index) => {
item.then((res) => {
count++;
res[index] = res;
if (count === list.length) {
resolve(res)
}
// 只要错了一个就结束 Promise.all,并进去它的 catch
}, e=> reject(e))
})
})
}
// 竞赛模式,只要有一个异步执行结束,就算结束
static race (list) {
return new Promise((resolve, reject) => {
list.forEach(item=> {
item.then(res => {
resolve(res)
})
})
})
}
// 不管是走到成功 resolve 还是走到失败 reject,都要执行这个方法
static finally (callback) {
// 所以我们只要在成功回调队列和失败回调队列都注册一下callback就行了
return this.then(value=> {
callback()
}, reason => {
callback()
})
}
static resolve (value) {
if (value instanceof Promise) {
return value;
} else {
return new Promise((resolve, reject) => {
resolve(value)
})
}
}
...
}
这样就完成了一个功能完整的 Promise,你学废了吗?
async/await
async/await 有以下几个特点:
- 基于普通的 promise 实现,它不能用于普通的回调函数
- 与 promise 一样,是非阻塞式的
- 它使得异步代码看起来更像同步代码,这正是它的魔力所在
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
async function readResult(params) {
try {
let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
let p2 = await read(p1, 'utf8')
let p3 = await read(p2, 'utf8')
console.log('p1', p1)
console.log('p2', p2)
console.log('p3', p3)
return p3
} catch (error) {
console.log(error)
}
}
readResult('1.txt').then( // async函数返回的也是个promise
data => {
console.log(data)
},
err => console.log(err)
)