Node 异步编程
为什么要异步编程
异步的概念之所以会火起来,是因为浏览器中Javascript在单线程执行,而且它与UI渲染共用一个进程。如果脚本执行时间超过100ms,用户就会感到卡顿。如果Javascript要从服务器上获取资源,并且以同步的方式进行,那么JavaScript要等资源完全获取后才会继续运行,那么这时UI渲染就会停滞,导致用户体验下降。通过异步消可以除阻塞的现象,JavaScript和UI渲染可以同时进行,给用户一个鲜活的页面。
函数式编程
高阶函数
在通常语言中,函数的参数只接受基本数据类型,返回值也是基本数据类型。高阶函数则是把函数作为参数或作为函数的返回值,如下面代码:
1 | function foo(x) { |
高阶函数比普通函数灵活很多,除了通常意义的函数调用外,还形成了一种后续传递风格的结果接受方式,而非单一的返回值形式。
1 | function foo(x,bar){ |
对于相同的foo函数,传入的bar参数不同,可以得到不同的结果。一个经典的例子便是数组的sort方法。
1 | var arr=[40,100,1,2,5,25,66] |
通过改动sort方法的参数,可以产生不同的排序方式。ES5中提供的一些数组方法(forEach、map、filter、every)都是高阶函数。
偏函数
偏函数是指创建一个调用另外一个部分(参数或变量)已经预置的函数,如下面代码:
1 | var toString=Object.prototype.toString |
上面的代码只有两个函数定义,但是我们需要重复定义很多类似的函数,如果有更多的isXXX,就会出现很多冗余代码。为了解决重复代码,我们引入一个新的函数,这个函数可以创建类似的函数,如下面的代码
1 | var toString = Object.prototype.toString |
这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数。
异步编程的优势与难点
优势
Node的最大特性就是非阻塞I/O模型,非阻塞的I/O可以使CPU与I/O并不相互以来等待,使资源得到更好的利用。
难点
异常处理
JavaScript处理异常,通常使用try
/catch
语句块来捕获异常,但是这对异步编程不一定适用。异步I/O的实现有两个阶段:提交请求和处理结果,这两个阶段之间有事件循环的调度,两者不关联。异步方法在第一阶段提交请求后立即返回,然而异常有可能会发生在第二阶段,因此try
/catch
的功效在此不会发挥作用。Node在异常处理上形成了约定,将异常作为回调函数的第一个实参,如果为空值,则表明异步调用没有产生异常。
在编写异步函数时,也要遵循以下原则:
- 必须执行调用者传入的回调函数
- 正确传递回异常供调用者判断
函数嵌套过深
这是Node饱受诟病的地方,在Node中事务中存在多个异步调用的场景有很多,比如一个遍历目录的操作:
1 | var fs=require("fs") |
由于两次操作存在依赖关系,函数的嵌套也情有可原。虽然在结果上是没有问题的,但是没有利用好异步IO的并行优势,这是异步编程的典型问题。
阻塞代码
Javascript没有sleep这样的线程沉睡功能,唯独有setInterval和setTimeout两个函数,这两个函数并不能阻塞后续代码的持续执行,
1 | console.log(1) |
异步编程解决方案
事件发布/订阅模式
Node自身提供的events模块是发布/订阅模式的简单实现,操作极其简单:
1 | var events = require('events'); |
事件发布/订阅模式自身没有同步和异步调用问题,常常用来解耦业务逻辑。事件监听器也是一种钩子机制,利用钩子导出内部数据或状态给外部的调用者。
继承events模块
Node中Stream对象继承EventEmitter的例子:
1 | const EventEmitter = require('events'); |
- 利用事件队列解决雪崩问题
在事件订阅/发布模式中,通常有一个once方法,通过它添加的监听器只能执行一次,在执行后就会将它与事件的关联移除。这个特性可以帮助我们过滤一些重复性的事件响应。
下面是一个数据库查询语句的调用:
1 | var select=function(callback){ |
如果服务刚好启动,这样缓存中是不存在数据的,如果访问量巨大,同一句SQL会被发送到数据库中反复查询,影响服务性能,一种改进方案是添加状态锁:
1 | var status = "ready" |
但是在多次调用select时,只有第一次是生效的,后续的select是无效的,这时可以引入事件队列:
1 | var events = require("events") |
流程控制
使用ES6的async语法。ES6-async
异步并发控制
bagpipe解决方案
通过一个队列来控制并发量,如果当前活跃的异步调用量小于限定值,从队列中取出执行。如果活跃调用达到限定值,调用存放在队列中。每个异步调用结束时,从队列中取出新的异步调用执行。
1 | var Bagpipe = require("bagpipe") |