目的在于提升性能。
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在异常处理上形成一种约定,将异常作为回调函数的第一个参数返回,若为空值则表示没有异常抛出。
```javascript
async(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模型简单实现
// Promise
const 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
}
// Deferred
const 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实现一个readFile
const q = require('q');
const readFile = function(path, encoding) {
const deferred = q.defer();
fs.readFile(path, encoding, deferred.makeNodeResolves());
return deferred.promise
}
// 调用readFile
readFile('./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 responses
const compression = require('compression');
app.use(compression());
// store session state in browser cookie
const cookieSession = require('cookie-session');
app.use(cookieSession({
keys: ['secret1', 'secret2']
}));
// parse urlencoded request bodies into req.body
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));
// respond to all requests
app.use(function(req, res){
res.end('Hello from Connect!\n');
});
//create node.js http server and listen on port
http.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
})
// queue
const 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
})
})