共计 3384 个字符,预计需要花费 9 分钟才能阅读完成。
大家都知道 js 的执行环境是单线程的,如果没有异步编程,那么 js 的执行效率会非常低下,导致程序十分卡顿,一提到异步编程大家首先的想到的一定是回调函数,这也是最常用的异步编程的形式,但其实常用的还有 Promise 和 Async 函数,接下来就让我们一起学习这几种常用的异步编程方法。
回调函数
回调函数就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数,来看一个简单的例子:
function print(name, callback) {
setTimeout(() => {
console.log(name)
if (callback) {
callback()
}
}, 1000)
}
print(‘a’, function () {
print(‘b’)
})
上面这个例子中将 print(‘b’) 放在 print(‘a’) 的回调函数中,这样就能按顺序依次打印 a、b, 但是回调函数有一个很明显的问题,就是当回调函数嵌套过深时,会导致代码混乱,不够清晰,这就是人们常说的对调地狱,来看下面这个例子:
function print(name, callback) {
setTimeout(() => {
console.log(name)
if (callback) {
callback()
}
}, 1000)
}
print(‘a’, function () {
print(‘b’, function () {
print(‘c’, function () {
print(‘d’)
})
})
})
当我们想按顺序依次打印 a、b、c、d 时,代码就变成上面的样子,可以看到,我们的代码形成四层嵌套,如果还要加回调函数就要继续嵌套,这样嵌套会越写越深,越来越难以维护,此时我们就必须考虑用新的技术去改进,es6 的 Promise 函数应运而生,接下来让我们看 Promise 函数是如何改进这个问题的。
Promise
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
print(‘a’).then(() => {
return print(‘b’)
})
.then(() => {
return print(‘c’)
})
.then(() => {
return print(‘d’)
})
和之前用回调函数的形式相比,Promise 函数写法更加清晰,由回调函数的嵌套调用变成了链式调用,但是 Promise 也有一个很严重的问题就是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作都是放在 then 函数里面,导致代码的语以变差,有什么更好的解决办法呢?如果您对 Promise 函数还想有更深入的了解,可以去看阮一峰老师 es6 入门
Async
在正式使用异步函数之前,先简单的介绍一下它的用法,async 通常与 await 一起使用,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体后面的语句。做了简单的介绍后,接下来,我们来 async 函数是怎么对 Promise 调用优化的。看下面的例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
async function test () {
await print(‘a’)
await print(‘b’)
await print(‘c’)
await print(‘d’)
}
test()
async 函数来处理之前的问题,代码就是上面的这个例子中所展示的样子,是不是感觉代码瞬间清晰了,而且代码更加好理解了,再仔细思考一下使用 async 异步函数就很完美了吗?其实 async 异步函数也有其固有的问题,接下来我们就看看 async 异步函数还有什么问题需要解决。
错误捕获
异步函数第一个需要解决的问题就是错误捕获的问题,让我们看看一般情况下 async 异步函数是怎么做错误捕获的,来看一个例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
async function test () {
try {
await print(‘a’)
} catch (err) {
console.log(err)
}
}
test()
当使用上述形式的 try,catch 进行错误捕获的时候,是不是觉得代码和使用 Promise 函数时一样啰嗦,那有没有好的解决办法呢?让我们来看另外一个例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve(‘a’)
}, 1000)
})
}
async function test () {
let [err, result] = await to(print(‘a’))
if (err) throw err
return result
}
test()
to.js:
function to(promise, errorExt) {
return promise
.then(function (data) {return [null, data]; })
.catch(function (err) {
if (errorExt) {
Object.assign(err, errorExt);
}
return [err, undefined];
});
}
export {to};
export default to;
上述例子中,将 async 异步函数的错误处理封装到了一个 to.js 中,这里面其实只有一个简单方法,传入一个 Promise 对象,对 Promise 对象进行错误捕获返回值,用解构的形式获取返回值和错误,这样就不需要反复写 try catche 做错误捕获了。to.js 是一个开源库
异步陷阱
什么是异步陷阱呢?在使用 async 异步函数的时候,多个异步操作是可以同时执行,但是有 await 命令变成了继发的形式了,即必须等待前一个执行完了后一个才能执行,还是之前的例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve()
}, 1000)
})
}
async function test () {
await print(‘a’)
await print(‘b’)
await print(‘c’)
await print(‘d’)
}
test()
假设 await print(‘a’)、await print(‘b’)、await print(‘c’)、await print(‘d’) 这四个操作并没有先后的逻辑关系,可以同时执行,那么按照上面的写法就会导致前一个执行完再执行下一个,整个执行过程中的等待时间会有 4s,但是同时执行的等待时间就只有 1s,这是在使用 async 异步函数会经常忽略的一个问题,那么怎么解决呢?介绍一个我经常使用的办法,看例子:
function print(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(name)
resolve(‘a’)
}, 1000)
})
}
async function test () {
Promise.all([print(‘a’), print(‘b’), print(‘c’), print(‘d’)])
}
test()
其实解决办法很简单就是通过 Promise.all() 方法,将所有异步操作作为参数数组传入,这样 print(‘a’)、print(‘b’)、print(‘c’)、print(‘d’) 这四个异步操作就可以并发执行了。
总结
这篇文章简单的介绍了一些常用的异步编程的方法,如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏。最后,打个小广告:阿里云双十一云服务新用户 1 折优惠车票