前言
对于异步编程,从最初的回调函数Callback
再到Promise、generator、async/await
JavaScript也在慢慢填补这个坑。但异步编程到底可以做什么还是值得探讨的。
这篇不再讲述如何去使用语法实现异步,重在探讨异步编程思想和方案。
为什么会有异步编程
js是单线程的,我们平常写代码的逻辑就是一行执行完了再执行下一行,一个程序就是由一组命令序列组成。但现在问题来了,如果我们遇到异步I/O或者是非I/O的异步API时(定时器,延时器...)如果不使用异步编程的话就会发生阻塞,那么带来的问题就是页面可能会失去响应。所以为了解决多线程并行的问题,JavaScript就使用异步事件模型。
异步编程的不足
我们先来聊聊使用异步编程会给我们带来什么难点,以便我们更加了解异步编程。
异常处理
通常我们在处理异常的时候会立马想到try/catch/final
这样的语法进行异常的捕获,但是这样的语法在处理异步的时候并不一定全部适用。
例如下面的代码:
//片段一
try{
setTimeout(()=>{
console.log(a)
},500)
}catch(e){
console.log(e,'err')
}
在执行到定时器API后,回调函数被保存起来直到下一个事件循环才会取出来执行,尝试对异步方法进行异常捕获只能捕获当前事件循环内的异常。
再次改造代码,那么异步的异常就可捕获:
//片段二
try{
setTimeout(()=>{
try{
console.log(a)
}catch(e){
console.log(e,'err2')
}
},500)
}catch(e){
console.log(e,'err')
}
阻塞代码
javaScript这门语言并没有一个类似sleep()沉睡的功能,我们想实现一个延时操作可通过setTimeout,setInterVal
这两个函数,但这两个函数并不会阻塞后续的代码执行。
一起来探讨通过如下的代码实现sleep()函数是否可行?
let start=new Date();
while(new Date()- start<1000){
console.log(123);
}
//需要阻塞的代码
console.log('代码');
这段代码看似没什么问题,利用循环实现代码阻塞。但从性能上来看这段代码会持续占用CPU进行判断。所以如果遇到这样的需求时,可以整理规划业务逻辑之后使用setTimeout
来实现。
上面的代码可改造为:
setTimeout(()=>{
//需要阻塞的代码
console.log('代码');
},1000);
console.log(123)
函数嵌套过深
我们知道如果业务场景存在多层的依赖,那么会出现函数的多层嵌套,这样的代码不仅不易于维护而且阅读起来真的很费劲。那么我们该如何去解决这样的问题。
异步编程方案
事件发布/订阅模式
事件发布/订阅模式无论是在前端框架还是Node应用中,这种设计模式被广泛的使用。这种模式可以实现一个事件和多个回调函数的关联,这些回调函数又称为事件监听器。
事件最大的特点是它的解耦能力,事件发布者无需关注订阅的侦听器是如何实现业务逻辑。比如我们在封装组件的时候,将不变的部分封装在内部,将容易改变或者自定义的部分通过事件暴露给外部处理。
思考多对一的业务场景
在异步编程中,我们可能会出现多对一的情况,也就是说一个业务逻辑可能依赖多层级的数据结果,就如上面所说的多层嵌套函数问题,那么通过事件发布/订阅模式也是可以解决该问题的。
从本质上来看就是可以并行调用但实际上只能串行执行的问题。多个异步场景中回调函数的执行并不能保证它们的执行顺序,那么我们可以通过一个第三方函数来处理。
如下伪代码就是实现多对一的目的:
参数time是需要几个异步参与,cb是最后要执行的函数。
const after=function(time,cb){
let result=Object.create(null),count=0;
return function(key,value){
result[key]=value;
count++;
if(count === time) cb(result);
}
}
使用上的话可参考如下的代码:
const events=require('events');
const done=after(3,cb);
//发布者
events.emit('done','dataA',dataA);
events.emit('done','dataB',dataB);
//订阅者
events.on('done',done);
Node自身也提供了一个events模块,在服务端我们也可以利用事件队列来解决那些高访问量,大并发的情况下缓存失效时的sql数据查询请求。通过定义状态开关来控制sql查询的次数,并且使用once()方法将请求压入任务队列中,等待sql查询结束发布数据。
之前自己也写过一个简易的订阅发布库,可供大家参考:简易的订阅发布
当然一个健壮的订阅发布库,还需要暴露一些处理异常的事件。这样通过订阅事件来,将异常统一交给库来处理,这样我们只需专注自己业务逻辑即可。
Promise、 Generator、async/await
Promise简单说就是一个容器,里面保存着未来某个时刻才会结束的事件结果,一经决议就无法改变其状态,如同君子一言驷马难追。一个Pomise就是一个代表了异步操作最终完成或失败的对象。具体Promise内部原理之前总结了,可参考文章:Promise
Generator最大的特点是可以控制函数的执行,生成器可以在函数运行中被暂停一次或多次并且后面再恢复执行,在暂停期间允许其他代码语句被执行,这样子就可以用来封装异步任务。
如何去实现一个Generator函数,最核心的2步骤是: 一是要保存函数的上下文信息,二是实现一个完善的迭代方法,使得多个 yield 表达式按序执行,从而实现生成器的特性。
// 迭代器
function gen$(_context){
while(1){
switch(_context.prev = _context.next){
case 0:
_context.next=1;
return 1;
case 1:
_context.next =2;
return 2;
case 2:
_context.next=3;
return 3;
case 3:
case 'end':
return _context.stop();
}
}
}
// 上下文
var context ={
next:0,
prev:0,
done:false,
stop:function stop(){
this.done =true;
}
}
let gen =function(){
return {
next:function(){
value =context.done ?undefined:gen$(context);
done =context.done;
return{
value,
done
}
}
}
}
async/await的出现使得异步编程如何写同步代码一样,不再需要像Promise那样链式调用了,async/await是Generator的语法糖,它的具体原理实现:async/await
流程控制库
async是一个异步操作的工具库,提供了多种异步的协作模式。常用的流程控制模式有串行、并行和瀑布流模式,具体的用法可参考文档,这里具体不再讲述。
流程控制核心的思想是通过操作队列来进行控制。
比如通过流程控制来实现多对一的异步结果处理,通过一个中间函数来操作控制任务队列,下面并发限流伪代码可供参考:
function httpLimit(initHttp = 6, httpUrl = [], callback) {
let ajaxNum = 0 //正在请求的数量
let arr = []
next();
function next() {
while (ajaxNum < initHttp && httpUrl.length) {
let fn = httpUrl.shift()
++ajaxNum
fn().then((res) => {
console.log(res);
arr.push(res);
--ajaxNum;
next();
if (httpUrl.length == 0 && ajaxNum === 0) {
callback(arr);
}
})
}
}
}
一个完整的并发限流还需要有超时控制和异常处理。对于超时控制,是给异步调用设置一个阈值时间,如果异步调用在规定时间没有返回结果就是超时。 可以通过如下的伪代码实现超时控制:
//方法一
function request(url,timeout){
return new Promise((resolve,reject)=>{
fetch(url).then((res)=>{
resolve(res.json())
})
setTimeout(()=>{
reject();
},timeout)
})
}
//方法二
const p1='异步请求'
const p2=new Promise((resolve,reject)=>{
setTimeout(reject,2000)
})
Promise.race([p1,p2])
流程控制也可以应用在中间件当中,每个中间件传递参数和尾触发函数,通过队列形成一个处理流。中间件机制可以像面向切面编程一样,可以不和具体的业务逻辑产生关联。比如node中可以使用中间件进行过滤、验证、日志打印等功能。
总结
如上探讨的就是异步编程的方案,在编码过程中可以根据不同的场景具体分析。
这段时间忙着工作、健身、生活,但输出些许缓慢了,后续会多多分享和学习。
最后给大家推荐一首最近超爱听的歌曲:一夜长大~