co
背景
有三个文件file1.md/file2.md/file3.md
统计这三个文件的大小信息
输出{file1: 6058, file2: 12116, file3: 18174}
传统回调
const fs = require('fs');
function error(err) {
throw new Error(err);
}
function main() {
const sizeInfo = {
'file1': 0,
'file2': 0,
'file3': 0
};
fs.stat('file1.md', function(err, stat) {
if (err) return error(err);
sizeInfo['file1'] = stat.size;
fs.stat('file2.md', function(err, stat) {
if (err) return error(err);
sizeInfo['file2'] = stat.size;
fs.stat('file3.md', function(err, stat) {
if (err) return error(error);
sizeInfo['file3'] = stat.size;
console.dir(sizeInfo);
});
})
});
}
main();
Generator
function returnTwoAsync() {
setTimeout(function() {
return 2;
}, 0);
}
function returnThreeAsync() {
setTimeout(function() {
it.next(3);
}, 0);
}
function* gen() {
console.log('Begin.....');
yield 1;
yield returnTwoAsync();
yield returnThreeAsync();
}
const it = gen();
it.next(); // 'Beign....' {data:1, done:false}
it.next(); // {data:undefined, done:false}
it.next(); // {data:3, done:false}
it.next(); // {data:undefined, done:true}
Generator
// 业务场景下
function* gen() {
try {
let data1 = yield getData1();
catch (e) {
console.error(e);
}
let data2 = yield getData2();
}
}
function getData(err, data) {
if (err) {
it.throw(err);
} else {
it.next(data);
}
}
Generator
const fs = require('fs');
// 改造size方法
function size(filename) {
fs.stat(filename, function(err, stat) {
if (err) it.throw(err);
else it.next(stat.size);
});
}
function* main() {
const sizeInfo = {
'file1': 0,
'file2': 0,
'file3': 0
};
try {
sizeInfo['file1'] = yield size('file1.md');
sizeInfo['file2'] = yield size('file2.md');
sizeInfo['file3'] = yield size('file3.md');
} catch (e) {
console.error(e);
}
console.log(sizeInfo);
}
const it = main();
it.next();
Generator
优势:
阻塞式的、同步式的代码来撰写业务逻辑
通过 try-catch 进行错误处理
问题:
强耦合迭代器
对所有的异步函数都得进行改造
Thunkify
A thunk is a subroutine used to inject an additional calculation into another subroutine.
fs.readFile(fileName, callback);
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
Thunkify
function thunkify(fn){
return function(){
var args = new Array(arguments.length);
var ctx = this;
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function(done){
var called;
args.push(function(){
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
Runner
function runGenerator(generatorFunc) {
// 获得generator迭代器
let it = generatorFunc();
next();
// 自定义一个next函数
function next(err, res) {
if (err) {
it.throw(err);
}
const {
value,
done
} = it.next();
if (done) {
return;
}
// 使得函数不再依赖迭代器的next 不需要 it.next(data) 来手动传值
if (typeof value === 'function') {
value.call(this, function(err, response) {
if (err) {
next(err, null);
} else {
next(null, res);
}
})
}
}
}
Runner
const fs = require('fs');
function size(filename) {
// 不耦合迭代器
return function(fn) {
fs.stat(filename, function(err, stat) {
if (err)
fn(err);
else
fn(null, stat.size);
});
}
}
function runGenerator(gen) {
// 先获得迭代器
const it = gen();
next();
function next(err, res) {
if (err) {
return it.throw(err);
}
const {
value,
done
} = it.next(res);
if (done) {
return;
}
if (typeof value === 'function') {
value.call(this, function(err, res) {
if (err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
}
function* main() {
const sizeInfo = {
'file1': 0,
'file2': 0,
'file3': 0
};
try {
sizeInfo['file1'] = yield size('file1.md');
sizeInfo['file2'] = yield size('file2.md');
sizeInfo['file3'] = yield size('file3.md');
} catch (error) {
console.error('error:', error);
}
console.dir(sizeInfo)
}
runGenerator(main);
并行获取数据
const fs = require('fs');
let info = {};
function print() {
keys = Object.keys(info);
if (keys.length === 3) {
console.dir(info);
}
}
fs.stat('file1.md', function(err, stat) {
if (!err) {
info['file1'] = stat.size;
print();
}
});
fs.stat('file2.md', function(err, stat) {
if (!err) {
info['file2'] = stat.size;
print();
}
});
fs.stat('file3.md', function(err, stat) {
if (!err) {
info['file3'] = stat.size;
print();
}
})
并行Generator
// 只展示修改的地方
function runGenerator(gen) {
// 先获得迭代器
const it = gen();
// 驱动generator运行
next();
function next(err, res) {
// ...
if (Array.isArray(value)) {
// 存放异步任务结果
const results = [];
// 等待完成的任务数
let pending = value.length;
// 当任务队列里任意一个任务发生错误时,终止所有任务的继续
let finished = false;
value.forEach(function(func, index) {
func.call(this, function(err, res) {
if (err) {
finished = true;
next(err);
} else {
// 保证结果的存放顺序
results[index] = res;
// 直到所有任务执行完毕
if (--pending === 0) {
next(null, results);
}
}
})
})
}
if (typeof value === 'function') {
// ...
}
}
}
function* main() {
let sizeInfo = {};
try {
let sizes = yield [
size('file1.md'),
size('file2.md'),
size('file3.md')
];
sizeInfo['file1'] = sizes[0];
sizeInfo['file2'] = sizes[1];
sizeInfo['file3'] = sizes[2];
console.log(sizeInfo);
} catch (error) {
console.error('error:', error);
}
}
runGenerator(main);
换用Promise
function size(filename) {
return new Promise((resolve, reject) => {
fs.stat(filename, (err, stat) => {
if (err)
reject(err);
else
resolve(stat.size);
});
});
}
function runGenerator(gen) {
// ...
function next(err, res) {
// ...
if (isPromise(value)) {
value.then(function(res) {
next(null, res);
}, function(err) {
next(err);
});
}
if (Array.isArray(value)) {
// ...
}
if (typeof value === 'function') {
// ...
}
}
}
function isPromise(obj) {
return obj && typeof obj.then === 'function';
}
function* main() {
const sizeInfo = {
'file1': 0,
'file2': 0,
'file3': 0
};
try {
sizes = yield Promise.all([
size('file1.md'),
size('file2.md'),
size('file3.md')
]);
sizeInfo['file1'] = sizes[0];
sizeInfo['file2'] = sizes[1];
sizeInfo['file3'] = sizes[2];
} catch (error) {
console.error('error:', error);
}
console.dir(sizeInfo);
}
runGenerator(main);
Thunkify优化
function runGenerator(gen) {
// ...
if (isPromise(value)) {
value.then(function(res) {
next(null, res);
}, function(err) {
next(err);
});
}
if (Array.isArray(value)) {
const results = [];
let pending = value.length;
value.forEach(function (func, index) {
func.call(this, function (err, res) {
if (err) {
next(err);
} else {
results[index] = res;
if (--pending === 0) {
next(null, results);
}
}
})
})
}
if (typeof value === 'function') {
value.call(this, function (err, res) {
if (err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
问题:
代码重复:
出错:next(err)
正常:next(null, res)
Thunkify优化
function toThunk(fn) {
if(Array.isArray(fn)) {
const results = [];
let pending = fn.length;
return function(cb) {
let finished = false;
fn.forEach(function(func, index) {
if(finished) {
return;
}
func = toThunk(func);
func.call(this, function(err, res) {
if(err) {
finished = true;
cb(err);
} else {
results[index] = res;
if(--pending === 0) {
cb(null, results);
}
}
} );
})
}
}else if(isPromise(fn)) {
return function(cb) {
return fn.then(function(res){
cb(null, res);
}, function(err){
cb(err);
})
}
}
}
Thunkify优化
function runGenerator(gen) {
const it = gen();
next();
function next(err, res) {
if (err) {
return it.throw(err);
}
const { value, done } = it.next(res);
if (done) {
return;
}
thunk = toThunk(value);
if (typeof thunk === 'function') {
thunk.call(this, function (err, res) {
if (err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
}
获取返回值
function *main() {
const sizeInfo = {
'file1': 0,
'file2': 0,
'file3': 0
};
sizes = yield[
size('file1.md'),
size('file2.md'),
size('file3.md')
];
sizeInfo['file1'] = sizes[0];
sizeInfo['file2'] = sizes[1];
sizeInfo['file3'] = sizes[2];
return sizeInfo;
}
let sizeInfo = runGenerator(main);
console.dir(sizeInfo); // undefined
异步获取返回值
获取返回值
function runGenerator(gen, cb) {
const it = gen();
next();
function next(err, res) {
if (err) {
try {
// 防止报错:Unhandled promise rejection
return it.throw(err);
} catch (e) {
return cb(err);
}
}
const { value, done } = it.next(res);
if (done) {
cb(null, value);
}
thunk = toThunk(value);
if (typeof thunk === 'function') {
thunk.call(this, function (err, res) {
if (err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
}
runGenerator(main, function(err, sizeInfo){
if(err) {
console.error('error', err);
} else {
console.dir(sizeInfo);
}
});
继续Thunkify
function wrap(gen) {
const it = gen();
return function (cb) {
next();
function next(err, res) {
if (err) {
try {
return it.throw(err);
} catch (e) {
return cb(err);
}
}
const { value, done } = it.next(res);
if(done) {
cb(null, value);
}
thunk = toThunk(value);
if(typeof thunk === 'function') {
thunk.call(this, function(err, res) {
if(err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
}
}
let wrapped = wrap(main);
function print(err, sizeInfo) {
if(err) {
console.error(err);
} else {
console.dir(sizeInfo);
}
}
wrapped(print);
main函数支持传参
function *main(files) {
// 初始化信息
const sizeInfo = files.reduce((info, file) => {
info[file] = 0;
return info;
}, {});
try{
const requests = files.map((file) => {
return size(file);
});
sizes = yield requests;
sizes.forEach((size, index) => {
sizeInfo[files[index]] = sizes[index];
});
return sizeInfo;
} catch(error) {
console.error('error:', error);
}
}
wrap(main)(['file1.md', 'file2.md', 'file3.md'], function(err, sizeInfo){
if(err) {
console.error(err);
} else {
console.dir(sizeInfo);
}
});
main函数支持传参
function wrap(gen) {
return function (cb) {
const args = Array.prototype.slice.call(arguments);
const length = args.length;
// 判断最后一个参数是否为回调函数
if(length && 'function' === typeof args[length-1]) {
cb = args.pop();
it = gen.apply(this, args);
} else {
return;
}
next();
function next(err, res) {
if(err) {
return it.throw(err);
}
const { value, done } = it.next(res);
if(done) {
cb(null, value);
}
thunk = toThunk(value);
if(typeof thunk === 'function') {
thunk.call(this, function(err, res) {
if(err) {
next(err, null);
} else {
next(null, res);
}
});
}
}
}
}
参考
co
By Joson Chen
co
- 234