JavaScript异步函数同步方法
首先这里非常感谢慕课网蜗牛老湿,本文内容是基于老师讲解koa2实现原理的时候整理出来的,好吧,这里直接进入主题。
在我们平时进行前端开发的时候,经常需要用到异步函数,最常见的是发送网络请求,在发送网络请求之后,在回调函数中对请求结果再进行下一步操作,下面来模拟这个操作步骤:
/*
* 模拟网络请求的函数
*/
function request(callback) {
let time = Math.random() * 1000 //为了模拟每次请求的时长不同,这里每次等待随机时间0到1s
setTimeout(() => callback(), time) //请求结束之后,调用回调函数
}
request(function () {
console.log(1)
})
request(function () {
console.log(2)
})
request(function () {
console.log(3)
})
接下来看结果:
可以看到,这里执行了3次,但是每次输出的顺序都不一致,按照我们想要的结果,是每次都是百分之百顺序执行1 2 3,如果要达到这种效果的话,就得这么写:
/*
* 模拟网络请求的函数
*/
function request(param, callback) {
let time = Math.random() * 1000 //为了模拟每次请求的时长不同,这里每次等待随机时间0到1s
setTimeout(() => callback(param, time), time) //请求结束之后,调用回调函数
}
request(1, function (p1, t1) {
console.log(p1, t1)
request(2, function (p2, t2) {
console.log(p2, t2)
request(3, function (p3, t3) {
console.log(p3, t3)
})
})
})
可以看到,代码一下子变得难维护了很多,现在这里还没有什么逻辑,等每一个request里面加上几十行的逻辑代码,缺点一下子就显现出来了,但是结果却是是没错的:
这里先来对比一下两种做法:
- 第一种,是针对异步方法来说的,也就是说,三个请求之间没有什么关系,可以同时发起,最终等待时间取决于请求时间最长的请求;
- 第二种,是针对同步方法来说的,也就是说,每个请求之间是有依赖关系的,这里的话,request3得等request2请求完毕才能执行,request2要等request1请求完毕才能执行,请求时长为三个请求的总时长;
第一种做法,实际上使用场景非常少,如果不是特殊的业务逻辑,这种使用方式是不被允许的,因为三个请求并没有关系,所以这三个请求,在后台是可以合并为一个请求的,也就是说一次请求就可以完成任务,硬分三次请求的话会留下一些隐患。比如以后要将该功能暴露给外部系统调用,总不能让人家发送三次请求吧,日积月累,也会浪费服务器带宽流量。
第二种实际上是经常要用到的,根据第一个请求的请求结果,处理业务逻辑,然后判断是否需要进行下一步请求。
好在es7给出了一个新的语法,让异步方法同步起来没有那么困难了,看代码(我这里跳过了Promise调用链的说明):
/*
* 模拟网络请求的函数
*/
function request(param) {
return new Promise((resolve) => {
let time = Math.random() * 1000 //为了模拟每次请求的时长不同,这里每次等待随机时间0到1s
setTimeout(() => resolve({param, time}), time) //请求结束之后,调用回调函数
})
}
async function start() {
let {param: p1, time: t1} = await request(1)
console.log(p1, t1)
let {param: p2, time: t2} = await request(2)
console.log(p2, t2)
let {param: p3, time: t3} = await request(3)
console.log(p3, t3)
}
start()
不知道async/await的使用方法的同学,可以百度看看相关资料,网上已经说得很清楚,我这里就不再赘述了。结果:
可以看到结果仍然是正确的,但是里面的代码要比原来的要清爽很多,看起来也没有那么刺眼了……
接下来是本文的重点了,看代码:
/*
* 模拟网络请求的函数
*/
function request(param) {
return new Promise((resolve) => {
let time = Math.random() * 1000 //为了模拟每次请求的时长不同,这里每次等待随机时间0到1s
setTimeout(() => resolve({param, time}), time) //请求结束之后,调用回调函数
})
}
/*
* 合并异步方法
*/
function compose(middlewares) {
return function (...args) {
function dispatch(i) {
let fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
else {
return Promise.resolve(fn(
...args,
function next() {
return dispatch(i + 1)
}
))
}
}
return dispatch(0);
}
}
let funcs = [
async function (originalParam, next) {
let {param: p1, time: t1} = await request(1)
console.log(p1, t1, originalParam)
next();
},
async function (originalParam, next) {
let {param: p2, time: t2} = await request(2)
console.log(p2, t2, originalParam)
next();
},
async function (originalParam, next) {
let {param: p3, time: t3} = await request(3)
console.log(p3, t3, originalParam)
next();
},
]
compose(funcs)('start')
结果:
可以看到结果仍然是正确的,而且我们已经把需要同步的方法统一起来,放在了funcs这个数组中,这个数组中所有的异步函数通过compose会生成一个函数,执行这个函数会依次执行funcs数组中的异步函数,每个异步函数中都可以拿到一个next函数参数,执行该函数,就会执行下一个异步函数,如果在某一次异步函数中发现数据不对,就不执行next,中断了异步函数的同步执行,这样管理起来,所有的异步函数操作起来就方便多了。
这里还实现了koa2中间件的功能,比如看下面代码(其他没变,只变了funcs数组):
let funcs = [
async function (originalParam, next) {
console.log('1 start')
next();
console.log('1 end')
},
async function (originalParam, next) {
console.log('2 start')
next();
console.log('2 end')
},
async function (originalParam, next) {
console.log('3 start')
next();
console.log('3 end')
},
]
结果
通过这样的拦截操作,我们可以实现对任意对象任意函数进行拦截功能,详情说明,请见我的博客:基于koa2中间件原理实现拦截任意对象方法的调用
上一篇: Kafka源码系列教程之删除topic