异步编程
一.函数式编程
函数式编程(回调函数 深层嵌套)是异步编程的基础。
函数式编程的本质是:++函数是一等公民,可以调用,也可以作为 参数/返回值++
1.高阶函数
任何将函数作为参数/返回值的函数都可以称为高阶函数。
有回调的高阶函数形成了一种后续传递的风格,将业务重点放在了回调函数中。
典型的回调有:
- Array.sort(callback)
不同的callback可以得到不同的顺序结果。
var points = [40, 100, 1, 5, 25, 10];
points.sort(function(a, b) {
return a - b;
});
// [ 1, 5, 10, 25, 40, 100 ]
同样的还有reduce、some、every等等。
- 事件注册
var emitter = new events.EventEmitter();
emitter.on('event_foo', function () {
// TODO
});
同一个事件event_foo,可以执行不同的业务逻辑。
2.偏函数用法
偏函数指一个由参数/变量已经预置的函数返回生成的函数。
- 案例一
var toString = Object.prototype.toString;
var isString = function (obj) {
return toString.call(obj) == '[object String]';
};
var isFunction = function (obj) {
return toString.call(obj) == '[object Function]';
};
仅仅一个判断类型的功能,不同的类型却需要不同的函数定义,较为冗余
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object ' + type + ']';
};
};
var isString = isType('String');
var isFunction = isType('Function');
isType就是那个包装函数,执行时预置了type变量,结果返回一个偏函数。
- 案例二
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
after包装函数返回的偏函数,会虚假执行times次后,才会真正执行。
二.异步编程优势与难点
1.优势
基于事件驱动的非阻塞IO模型
2.难点
异常捕获
- 作为调用者,异步方法执行,结果立即返回,回调会存放起来,直到下一轮事件循环来调用,所以在当前事件循环捕获错误没有意义。
// 这个捕获无意义
try {
async(callback);
} catch (e) {
// TODO
}
- 作为异步方法提供者,需要:
- 确保调用者的回调会被执行
- 提供异常结果给调用者自行判断
var async = function (callback) {
process.nextTick(function() {
var results = something;
if (error) {
return callback(error);
}
callback(null, results);
});
};
嵌套过深
多个异步嵌套调用的场景
fs.readdir(path.join(__dirname, '..'), function (err, files) {
files.forEach(function (filename, index) {
fs.readFile(filename, 'utf8', function (err, file) {
// TODO
});
});
});
阻塞代码
缺少线程沉睡能力,而setTimeout()并不能阻止后续代码执行,同步的阻塞会阻塞CPU,而非线程沉睡。
多线程编程
充分利用多核CPU:
-
浏览器前端有
web workers- 充分利用多核CPU,减少阻塞UI渲染
- 浏览器支持程度不够
- 无法提高UI渲染效率
-
Node后端
child_process基础APIcluster模块深层次应用
异步转同步
偶尔的同步应用场景因缺少API而尴尬
三.异步编程解决方案
1.事件发布订阅模式
Node的events模块提供了一个发布订阅的简单实现,并且大部分模块都继承了该模块。
事件的回调函数称为侦听器,侦听器很方便的实现业务逻辑和事件之间的关联和耦合。
案例
var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};
var req = http.request(options, function (res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
res.on('end', function () {
// TODO
});
});
req.on('error', function (e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write('data\n');
req.write('data\n');
req.end();
发布订阅多应用于异步编程,这是因为事件的发布(触发),往往是++由事件循环异步触发++
data end事件都是在异步请求得到响应时触发的。
- 解耦业务逻辑
调用方只需要关心data end error的侦听器函数
- 钩子(hook)机制,钩子导出内部状态和数据
调用方通过data end error事件侦听器函数来获取异步请求的中间状态数据
继承events模块
var events = require('events');
function Stream() {
events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);
构造类Stream实现对events模块的继承
利用事件队列解决雪崩问题
雪崩问题指高访问量,大并发量情况下缓存失效的场景。
var select = function (callback) {
db.select("SQL", function (results) {
callback(results);
});
};
大并发量情况下,一条SQL语句将被发到数据库中反复查询。
var proxy = new events.EventEmitter();
var status = "ready";
var select = function (callback) {
proxy.once("selected", callback);
if (status === "ready") {
status = "pending";
db.select("SQL", function (results) {
proxy.emit("selected", results);
status = "ready";
});
}
};
使用once(侦听器完成后解耦关联)和事件队列使并发有序执行。
多异步之间的协作方案
一般一个事件对应多个侦听器,但也有一个侦听器对应多个事件的场景,比如多个异步协作:
其实面对的就是异步嵌套的难点:
var count = 0;
var results = {};
var done = function (key, value) {
results[key] = value;
count++;
if (count === 3) {
// 渲染页面
render(results);
}
};
fs.readFile(template_path, "utf8", function (err, template) {
done("template", template);
});
db.query(sql, function (err, data) {
done("data", data);
});
l10n.get(function (err, resources) {
done("resources", resources);
});
通过第三方函数和第三方变量处理异步协作,这个变量count称为哨兵变量。
可以使用偏函数改造:
var after = function (times, callback) {
var count = 0, results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
};
};
var done = after(times, render);
这样就实现了 异步:回调=多:1的效果。
var emitter = new events.Emitter();
var done = after(times, render);
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) {
emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
emitter.emit("done", "data", data);
});
l10n.get(function (err, resources) {
emitter.emit("done", "resources", resources);
});
实现多对多可以加入事件订阅发布
2.Promise/Deferred模式
- 普通的
事件触发方式(发布订阅模式的运行机制决定需要预先设定分支)
$.get('/api', {
success: onSuccess,
error: onError,
complete: onComplete
});
在AJAX异步请求发出之前,必须先将不同事件的侦听器设置好。
- Promise/Deferred模式
$.get('/api')
.success(onSuccess)
.error(onError)
.complete(onComplete);
即使分支尚未定义,异步请求已经发出。
$.get('/api')
.success(onSuccess1)
.success(onSuccess2);
通过Deferred对象,不同于以前一个事件只能有一个回调,可以一个事件分别使用不同业务处理逻辑。
Promise/A
Promise/A规范包含了Promise和Deferred两部分。
Promise
Promise/A规范关于单个异步操作定义如下:
- Promise操作只有三个状态:
未完成态完成态失败 - 状态转化只能由
未完成态向完成态/失败转化,完成态与失败不能互相转化 - 状态转化一旦完成,不可再被修改
一个Promise对象很简单,只需要提供then()方法即可,then()有些小要求:
- 接收两个函数作为参数
- 一个作为状态转化为成功态的回调,一个作为状态转化为失败态的回调
- 返回Promise对象实现链式调用
var Promise = function () {
EventEmitter.call(this);
};
util.inherits(Promise, EventEmitter);
Promise.prototype.then = function (fulfilledHandler, errorHandler) {
if (typeof fulfilledHandler === 'function') {
// 利用once()方法,保证成功回调只执行一次
this.once('success', fulfilledHandler);
}
if (typeof errorHandler === 'function') {
// 利用once()方法,保证异常回调只执行一次
this.once('error', errorHandler);
}
return this;
};
then方法中订阅了不同的事件(保存了函数参数),还需要一个状态变化来触发这些事件,这就是Deferred对象。
Deferred
var Deferred = function () {
this.state = 'unfulfilled';
this.promise = new Promise();
};
Deferred.prototype.resolve = function (obj) {
this.state = 'fulfilled';
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.emit('progress', data);
};
通过原型链我们关联了Deferred和Promise,并且在Deferred对象中实现态变的同时,去触发Promise的事件,执行回调。
promisify
var promisify = function (res) {
var deferred = new Deferred();
var result = '';
res.on('end', function () {
deferred.resolve(result);
});
res.on('error', function (err) {
deferred.reject(err);
});
return deferred.promise;
};
这里专门为网络请求定制了一个promisify。
promisify(res).then(function () {
// Done
}, function (err) {
// Error
}, function (chunk) {
// progress
console.log('BODY: ' + chunk);
});
这里使用promisify包装了请求结果。
promise和deferred的差别:
promise主要作用于外部,通过then()方法暴露给外部自定义逻辑deferred主要作用于内部,维护异步模型的状态
Promise中的多异步协作
Deferred.prototype.all = function (promises) {
var count = promises.length;
var that = this;
var results = [];
promises.forEach(function (promise, i) {
promise.then(function (data) {
count--;
results[i] = data;
if (count === 0) {
that.resolve(results);
}
}, function (err) {
that.reject(err);
});
});
return this.promise;
};
Promise进阶
解决异步的顺序调用问题,Promise的秘诀是对队列的操作(链式调用)。
回调函数形式的解决方案
obj.api1(function (value1) {
obj.api2(value1, function (value2) {
obj.api3(value2, function (value3) {
obj.api4(value3, function (value4) {
callback(value4);
});
});
});
});
回调地狱
普通函数回调形式的解决方案
var handler1 = function (value1) {
obj.api2(value1, handler2);
};
var handler2 = function (value2) {
obj.api3(value2, handler3);
};
var handler3 = function (value3) {
obj.api4(value3, hander4);
};
var handler4 = function (value4) {
callback(value4);
});
obj.api1(handler1);
额。。比较难懂
事件订阅(事件侦听)和发布形式的解决方案
var emitter = new event.Emitter();
emitter.on("step1", function () {
obj.api1(function (value1) {
emitter.emit("step2", value1);
});
});
emitter.on("step2", function (value1) {
obj.api2(value1, function (value2) {
emitter.emit("step3", value2);
});
});
emitter.on("step3", function (value2) {
obj.api3(value2, function (value3) {
emitter.emit("step4", value3);
});
});
emitter.on("step4", function (value3) {
obj.api4(value3, function (value4) {
callback(value4);
});
});
emitter.emit("step1");
是不是变得更糟了
Promise解决方案
promise()
.then(obj.api1)
.then(obj.api2)
.then(obj.api3)
.then(obj.api4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
}).done();
Promise链式调用的实现原理
var Promise = function () {
// 队列用于存储待执行的回调函数
this.queue = [];
this.isPromise = true;
};
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
var handler = {};
if (typeof fulfilledHandler === 'function') {
handler.fulfilled = fulfilledHandler;
}
if (typeof errorHandler === 'function') {
handler.error = errorHandler;
}
this.queue.push(handler);
return this;
};
上面是Promise的实现,主要用来收集回调。
var Deferred = function () {
this.promise = new Promise();
};
// 完成态
Deferred.prototype.resolve = function (obj) {
var promise = this.promise;
var handler;
while ((handler = promise.queue.shift())) {
if (handler && handler.fulfilled) {
var ret = handler.fulfilled(obj);
if (ret && ret.isPromise) {
// 注意这里有promise.queue队列的继承。如果then回调也是一个promise,则Defferd的promise要切换,队列要继承。
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
};
// 失败态
Deferred.prototype.reject = function (err) {
var promise = this.promise;
var handler;
while ((handler = promise.queue.shift())) {
if (handler && handler.error) {
var ret = handler.error(err);
if (ret && ret.isPromise) {
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
};
// 生成回调函数
Deferred.prototype.callback = function () {
var that = this;
return function (err, file) {
if (err) {
return that.reject(err);
}
that.resolve(file);
};
};
上述是Deferred实现,主要实现内部状态的切换
var readFile1 = function (file, encoding) {
var deferred = new Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
};
var readFile2 = function (file, encoding) {
var deferred = new Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
};
readFile1('file1.txt', 'utf8').then(function (file1) {
return readFile2(file1.trim(), 'utf8');
}).then(function (file2) {
console.log(file2);
});
使用过程如上。代码有包装的操作,可以用偏函数将APIpromise化
// smooth(fs.readFile);
var smooth = function (method) {
return function () {
var deferred = new Deferred();
var args = Array.prototype.slice.call(arguments, 0);
args.push(deferred.callback());
method.apply(null, args);
return deferred.promise;
};
};
使用可以简化成:
var readFile = smooth(fs.readFile);
readFile('file1.txt', 'utf8').then(function (file1) {
return readFile(file1.trim(), 'utf8');
}).then(function (file2) {
// file2 => I am file2
console.log(file2);
});
3.流程控制库
尾触发与next
手动调用才能持续进行,称为
尾触发,关键词是next,应用最多的是Connect的中间件。
async
异步的并行执行
异步调用的依赖处理
异步的并行执行
自动依赖处理
Step
wind
四.异步并发控制
在Node中,可以方便的异步发起并发调用,未防止并发量过大,需要过载保护。
异步并发控制
规则
- 通过一个队列控制并发量
- 活跃的异步调用(异步调用了,但是回调还没有执行)小于限定值时,从队列获取异步执行
- 活跃调用达到限定值时,剩下的异步调用暂存队列
- 每个异步调用结束,从队列中获取新的异步调用执行
'use strict';
const EventEmitter = require('events');
function hasOwnProperty(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
class Bagpipe extends EventEmitter {
constructor(limit, options = {}) {
super();
this.limit = limit;
this.active = 0;
this.queue = [];
// queue length
this.queueLength = Math.round(this.limit * (this.options.ratio || 1));
}
push(method, ...args) {
this.queue.push({
method: method,
args: args
});
this.next();
return this;
}
next() {
// 没到限制,或者没有排队
if (this.active >= this.limit || !this.queue.length) {
return;
}
const {method, args} = this.queue.shift();
this.active++;
const callback = args[args.length - 1];
var called = false;
args[args.length - 1] = (err, ...rest) => {
// if timeout, don't execute
if (!called) {
this._next();
callback(err, ...rest);
}
};
method(...args);
}
_next() {
this.active--;
this.next();
}
}
module.exports = Bagpipe;
取自bagpipe类库源码,我做了一定删减(配置项导致,比如超时控制 拒绝模式等),方便理解。
五.总结
- 线性思维是习惯导致。打破思维壁垒,掌握异步,享受异步编程的愉快体验
- 阅读代码,找出最精简的描述,而后才是细枝末节
- 编写工具/平台化代码,同编写业务代码没什么不同,需求总是最重要的。