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