总结常用的异步编程案例

2,293 阅读7分钟

前言

对于异步编程,从最初的回调函数Callback再到Promise、generator、async/awaitJavaScript也在慢慢填补这个坑。但异步编程到底可以做什么还是值得探讨的。

这篇不再讲述如何去使用语法实现异步,重在探讨异步编程思想和方案。

为什么会有异步编程

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中可以使用中间件进行过滤、验证、日志打印等功能。

总结

如上探讨的就是异步编程的方案,在编码过程中可以根据不同的场景具体分析。

这段时间忙着工作、健身、生活,但输出些许缓慢了,后续会多多分享和学习。

最后给大家推荐一首最近超爱听的歌曲:一夜长大~