目的在于提升性能。
Node利用JavaScript及其内部异步库,将异步直接提升到业务层面,这是一种创新。
函数式编程
高阶函数
普通函数只能接受基本数据类型作为输入或输出的函数,而高阶函数是可以接受另一个函数作为输入或输出的函数。
// 普通函数function foo (x) {return x}// 高阶函数function foo(x) {return function(x) {return x}}function foo(x, bar) {return bar(x)}
例如:数组排序sort
const arr = [1,3,5,6,2,0,8];arr.sort(function(a, d) {return a - b})// 这里的sort即是一个高阶函数
高阶函数在JavaScript中比比皆是,其中ECMAScript5中提供的一些数组方法(forEach()、map()、reduce()、reduceRight()、filter()、every()、some())十分典型。
偏函数的用法
偏函数的用法是指创建一个调用另外一个部分—参数或变量已经预置的函数—的函数的用法。
例如:
const isType = function (type) {return function (obj) {return toString.call(obj) === `[object ${type}]`}}const isString = isType('String');const isFunc = ifType('Function');// 这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数
偏函数在异步编程中是十分常见的。
优势与难点
优势
Node最大的优势莫过于是基于事件驱动,非阻塞I/O模型。非阻塞I/O可以使cpu与I/O不相互依赖等待执行,让资源能够得到更好的利用。
难点
- 异常处理 ```javascript const async = function (callback) { process.nextTick(callback) }
try { async(callbakc) }catch(e) { // TODO } //调用async()方法后,callback被存放起来,直到下一个事件循环(Tick)才会取出来执行, 此时try…catch对于异常处理也无能为力。
node在异常处理上形成一种约定,将异常作为回调函数的第一个参数返回,若为空值则表示没有异常抛出。```javascriptasync(function (err, results) {//TODO})/* 我们自行编写的异步方法上应遵循这种一些原则1. 必须执行调用者传入的回调函数2. 正确传递回异常供调用者判断*/// 例子const async = function(callback){process.nextTick(function () {const results = something;if (error) {return callback(error)}return callback(null, results)})}
在编写异步方法时,只要将异常正确地传递给用户的回调方法即可,无须过多处理。
- 函数嵌套过深
- 阻塞代码
javascript并没有sleep()这样的线程沉睡的功能,仅仅只有setTimeout,setInterval这两个延迟函数,但是这两个函数并不会阻塞后面代码的运行。
遇见这样的需求时,在统一规划业务逻辑之后,调用setTimeout()的效果会更好。
- 多线程编程
Web Workers是一个利用消息机制合理使用多核CPU的理想模型。
- 异步转同步
异步编程解决方案
异步编程的解决方案主要有三种
- 事件发布/订阅模式
- Promise/Deferred模式
- 流程控制库
事件发布/订阅模式
事件监听模式是一直广泛的异步编程的模式,是回调函数的事件化,又称为发布/订阅模式。
NodeJs自身提供的events模块是这种模式的简单实现。它具有addListener/on()、once()、removeListener()、removeAllListeners()和emit()等基本的事件监听模式的方法实现。
const events = require('events')const emmiter = new events.Emitter()// 订阅emmiter.on('event1',(msg) => {console.log('触发事件', msg)})// 发布emmiter.emit('event1')
订阅/发布自身并无同步异步调用的问题,但在Node中,emit()调用多半是伴随事件循环而异步触发的,所以我们说事件发布/订阅广泛应用于异步编程。
订阅/发布模式通常用来解耦业务逻辑,发布者无需关注订阅的侦听器如何实现逻辑,更无需关注有多少个订阅侦听器,数据通过消息的方式可以很灵活地传递。
例如:
const http = reuqire('http')const option = {host: 'www.google.com',port: 80,path: '/update',method: 'post'}const req = http.request(option, function(res) {console.log("statusCode", res.statusCode);res.setEncoding('utf-8')res.on('data', function(chunk) {console.log('data chunk', chunk)})res.on('end', function() {console.log('end')})})req.on('error', (e) => {console.log('error', e)})req.write('data')req.end()
在这段HTTP请求的代码中,程序员只需要将视线放在error、data、end这些业务事件点上即可,至于内部的流程如何,无需过于关注
多类型异步协作
可以使用到eventproxy。
npm i --save eventproxy // 安装// 使用const EventProxy = require('eventproxy')const proxy = new EventProxy()proxy.all('tpl', 'data', function (tpl, data) {// TODO// 在所有指定事件触发后将会被执行// 参数对应各自的时间名})fs.readFile(file_path, 'utf-8', function(err, template) {proxy.emit('tpl', template)})db.query(sql, function(err, data) {proxy.emit('data', data)})
EventProxy
all():订阅多个事件,当所有的事件都被触发后,侦听器才会被执行。
tail(): 订阅多个事件,当侦听器满足条件时执行一次之后,如果组合事件中的某个事件被再次触发,侦听器会用最新的数据继续执行。
异步场景下,我们常常需要用一个接口多次获取数据,此时触发的事件名或许是相同的。
after(): 实现侦听器在执行多少次之后执行侦听器的单一事件组合订阅方式。
例如
const proxy = new EventProxy();proxy.after(data, 10, function(data) {// 表示执行10次data事件之后执行})
EventProxy模块除了可以应用于Node中外,还可以用在前端浏览器中。
EventProxy异常处理
**
fail()
const proxy = new EventProxy();proxy.fail(callback)// 等价于proxy.fail(function(err) {callback(err)})proxy.bind('error', function(err) {proxy.unbind() // 卸载所有处理函数callback(err)})
done()
const proxy = new EventProxy();proxy.done('data')// 等价于function (err, content) {if (err) return proxy.emit('error', err);proxy.emit('data', content)}// done 可以接受函数作为一个参数proxy.done(function(content) {// TODO// 这里无需考虑异常proxy.emit('data', content)})// 等价于function(err, content) {if (err) return proxy.emit('error', err);(function(content) {proxy.emit('data', content)}(content))}
当只传入一个回调函数时,需要手工调用emit()触发事件
Promise/Deferred模式
使用事件的方式时,执行流程需要被预先设定。即便是分支,也是需要预先设定,这是由于发布/订阅的运行机制决定的。
而Promise/Deferred模式是一种先执行异步调用,延迟传递处理的方式。
// 普通ajax调用$.get('/api', {success: onSuccess,error: onError,complete: onComplete})// 使用了Promise/Deferred模式的ajax$.get('api').success(onSuccess).error(onError).complete(onComplete)// 这使得即使不调用success, error等方法,ajax依然会被执行。
在原始的API中,一个事件只能处理一个回调,而通过Deferred对象,可以对事件加入任意的业务处理逻辑。
$.get('api').success(onSuccess_1).success(onSuccess_2)
Promise/Deferred模式在2009年时被Kris Zyp抽象为一个提议草案,发布在CommonJS规范中。随着使用Promise/Deferred模式的应用逐渐增多,CommonJS草案目前已经抽象出了Promises/A、Promises/B、Promises/D这样典型的异步Promise/Deferred模型,这使得异步操作可以以一种优雅的方式出现
PromiseA
Promise/Deferred模式其实包含两部分,即Promise和Deferred。
Promise:
PromiseA对异步操作做出了这样的抽象定义
- Promise操作只会处在3种状态的一种:未完成态、完成态、失败态。
- Promise的状态只会由未完成态->完成态或未完成态->失败态。不能逆反,完成态和失败态不能相互转换。
- Promise状态一旦改变就不能再被更改。

Api定义上,Promises/A提议是比较简单的。一个Promise只需要具备一个then()方法即可。
- 接受完成态、错误态的回调方法。在操作完成或出现错误时,将会调用对应方法。
- 可选地支持progress事件回调作为第三个方法。
- then发放只接受function对象,其余对象将被忽略。
- then方法返回Promise对象,可以继续链式调用。
.then(fulfillHandler, errorHandler, progressHandler)
_
Deferred:
then()方法所做的事情是将回调函数存放起来。为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象通常被称为Deferred,即延迟对象。
Promise/Deferred模型简单实现
// Promiseconst Promise = function() {EventEmitter.call(this)}util.inherits(Promise, EventEmitter)Promise.prototype.then = function(fulfillHandler, errorHandler, progressHandler) {if (type fulfillHandler === 'function') {// 利用once,保证函数只执行一次this.once('success', fulfillHandler)}if (type errorHandler === 'function') {// 利用once,保证函数只执行一次this.once('error', errorHandler)}if (type progressHandler === 'function') {// 利用once,保证函数只执行一次this.on('progress', progressHandler)}return this}// Deferredconst Deferred = function () {this.state = 'unfulfilled';this.promise = new Promise();}Deferred.prototype.resolve = function(obj) {this.state = 'unfulfilled';this.promise.emit('success', obj)}Deferred.prototype.reject = function(err) {this.state = 'failed';this.promise.emit('error', err)}Deferred.prototype.progress = function(data) {this.promise.progress('progress', data)}
在实际的业务中若想使用Promise/Deferred模型可以不用封装,可以使用Q或when模块来实现。
// q安装npm i --save q//q模块将数据封装成promise/***************************************************************************************/// 利用q实现一个readFileconst q = require('q');const readFile = function(path, encoding) {const deferred = q.defer();fs.readFile(path, encoding, deferred.makeNodeResolves());return deferred.promise}// 调用readFilereadFile('./index.txt', 'utf-8').then((data) => {// TODO}, (err) => {console.log(err)})//Promise通过封装异步调用,实现了正向用例和反向用例的分离以及逻辑处理延迟,这使得回调函数相对优雅
Promise中的多异步协作
// 简单实现all方式Deferred.prototype.all = function(promises) {let count = promises.lenth;const that = this;const results = [];promises.forEach((promise, i) => {promise.then((data) => {count--;results[i] = data;if (count === 0) {that.resolve(results)}},(err) => {that.reject(err)})})return this.promise}// 调用const promise_1 = readFile('a.txt', 'utf-8');const promise_2 = readFile('b.txt', 'utf-8');const deferred = new Deferred();deferred.all([promise_1, promise_2], (data) => {// TODO}, (err) => {// TODO error})
流程控制库
尾触发与next
尾触发目前应用最多的地方是Connect的中间件。
在Connect中,尾触发十分适合处理网络请求的场景。将复杂的处理逻辑拆解为简洁、单一的处理单元,逐层次地处理请求对象和响应对象。
const connect = require('connect');const http = require('http');const app = connect();// gzip/deflate outgoing responsesconst compression = require('compression');app.use(compression());// store session state in browser cookieconst cookieSession = require('cookie-session');app.use(cookieSession({keys: ['secret1', 'secret2']}));// parse urlencoded request bodies into req.bodyconst bodyParser = require('body-parser');app.use(bodyParser.urlencoded({extended: false}));// respond to all requestsapp.use(function(req, res){res.end('Hello from Connect!\n');});//create node.js http server and listen on porthttp.createServer(app).listen(3000);
async
async模块提供了20多个方法用于处理异步的各种协作模式。
https://caolan.github.io/async/v3/
// 安装yarn add async// 例子async.map(['file1','file2','file3'], fs.stat, function(err, results) {// results is now an array of stats for each file});async.filter(['file1','file2','file3'], function(filePath, callback) {fs.access(filePath, function(err) {callback(null, !err)});}, function(err, results) {// results now equals an array of the existing files});// 异步的并行执行async.parallel([function(callback) { ... },function(callback) { ... }], function(err, results) {// optional callback});// 异步的串行执行async.series([function(callback) { ... },function(callback) { ... }]);
setup
https://www.npmjs.com/package/setup
// 安装yarn add setup
异步并发控制
bagpipe
https://github.com/JacksonTian/bagpipe/blob/master/README_CN.md
async
async也提供了一个方法用于处理异步调用的限制:parallelLimit()。
parallelLimit()方法的缺陷在于无法动态地增加并行任务,为此,async提供了queue()方法来满足该需求。
async.parallelLimit([function(callback) {fs.readFile('a.txt', 'utf-8', callback)},function(callback) {fs.readFile('b.txt', 'utf-8', callback)}], 1, function (err, results) {// TODO})// queueconst q = async.queue(function(file, callback) {fs.readFile('a.txt', 'utf-8', callback)}, 2)q.drain = function() {// 完成了队列中的所有任务之后执行}fs.readdirSync('.').forEach(function(file) {q.push(file, function(err, data) {// TODO})})
