generator
Generator 函数是 ES6 提供的一种异步编程解决方案。
function* Hello(){
yield 1;
var res = yield 2; // undefined 因为yield表达式本身没有返回值,或者说总是返回undefined。
}
var hello = Hello();
var r2 = hello.next() // { value:1, done:false }
console.log(hello.next()); // { value:1, done:false }
console.log(hello.next()); // { value:2, done:false }
console.log(hello.next()); // { value:undefined, done:true }
执行generator函数会返回一个遍历器对象,一个指向内部状态的指针,可以依次遍历generator对象内部的每个状态。必须手动调用遍历器的next方法,函数才能从函数头部或者上一次停止的地方继续执行直到遇到下一个yield(或者return语句)。遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。就是调用next()返回的对象的value的值。
generator自执行
function delay(time){
setTimeout(function(){
// some code..
},time);
}
function* YieldDelay(){
yield delay(3200);
console.log('3200ms done!');
yield delay(4400);
console.log('4400ms done!');
yield delay(5500);
console.log('5500ms done!');
}
上面这段代码我们必须手动的一直调用 YieldDelay的next才
能执行完成。怎么可以自执行呢?
var g = YieldDelay()
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
上面这段代码。可以使得YieldDelay
函数自动执行完。但是这不适合异步操作如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。
那么如果我们把g.next()
当做参数传递到delay异步操作里当做回调函数的最后一步执行。是不是就可以了。
function delay(time,fn){
setTimeout(function(){
// some code..
fn()
},time);
}
function cl(){
yieldDelay.next();
}
function* YieldDelay(){
yield delay(3200,cl);
console.log('3200ms done!');
yield delay(4400,cl);
console.log('4400ms done!');
yield delay(5500,cl);
console.log('5500ms done!');
}
var yieldDelay = YieldDelay();
yieldDelay.next();
但我们现在还需要手动的进行第一次next
的调用。如何再进行优化。是的第一次next调用也可以自执行呢?上面代码delay函数有两个参数time和fn。time每个yield后面跟的时间都不一样。但是fn却都是相同的就是调用next()
函数。那么我们再进行一层封装。使得delay
可以不传递fn。
Thunk函数
thunk函数是一个偏函数,执行它会得到一个新的只带一个回调参数的函数。
function delay(time){
return function(fn){
setTimeout(function(){
fn();
},time)
}
}
delay
就是一个典型的Thunk
函数。返回一个只有一个回调的新函数。co库就是基于Thunk
函数的。以下是简易的实现。
function co(GenFunc) {
return function(cb) {
var gen = GenFunc(); // 第一次执行的时候构造出对象
next() // 调用自定义的next方法
function next(err, res) {
var ret = gen.next(res);
// 在generator函数中走一步,delay函数返回一个函数赋给ret.value
if (ret.done) {
// 判断ret.done是否为真,如果为真,说明generator函数执行完了,该调用回调函数了
cb && cb();
} else {
// 如果ret.done为假,那么调用上一个返回的函数,并且把next函数传递给它作为回调函数
ret.value(next); // 如果是promise这里可以是 ret.value.then(next);
}
}
}
}
co库的使用
co(function* (){
yield delay(4200);
yield delay(4000);
yield delay(3000);
})(function(){
// 回调函数
console.log('all done!');
})
yield跟array或对象
现在我们的co库要求yield
后必须是thunk
。我们进行拓展。如果yield
跟的是array或者对象(但目前实现的是数组和对象的每个值都好说thunk)
我们就在it.value(next)
;前增加一句it.value = toThunk(it.value)
;
我们来看toThunk
的实现
function isObject(obj){
return obj && Object == obj.constructor;
}
function isArray(obj){
return Array.isArray(obj); // Array.isArray这个方法是ES5的,但并不支持所有的浏览器
}
function toThunk(obj,ctx){
if (isObject(obj) || isArray(obj)) {
return objectToThunk(obj);
}
return obj;
}
function objectToThunk(obj){
return function(done){
var keys = Object.keys(obj);
var results = new obj.constructor();
var length = keys.length;
var _run = function(fn,key){
fn(function(err,res){// 这个参数就是thunk的回调。就是delay里的fn异步执行完执行的回调
results[key] = res;// 数组或对象中的thunk执行结果
--length || done(null, results); // 执行完一个数组长度减一
})
}
foreach(var i in keys){ //执行数组或对象的每个thunk
_run(Object[keys[i]],keys[i]);
}
}
}
Object.keys([1,2,3,4]) //[ '0', '1', '2', '3' ]
Object.keys({"one":1,"two":2,"three":3}) //[ 'one', 'two', 'three' ]
new obj.constructor()
这句,会根据obj的类型生成一个相关的空数组或者空对象。便于下面的赋值。objectToThunk
也是一个thunk
。因为最终objectToThunk
才是it.value
。这里的done
就相当于delay
里的fn,也就是co库中的next函数。这样yield后跟的数组或对象里的thunk
是并行执行的。直到所有的执行完成。
数组里面还是数组怎么办这可以使用递归嘛。跟深度遍历遇见数组里还是数组一样。
改变_run
函数,增加三行代码
var _run = function(fn,key){
//新增加判断是否数组里还是数组
if(Array.isArray(fn)) {
fn = toThunk(fn);
}
fn(function(err,res){
results[key] = res;
--length || done(null, results);
})
}
yield跟promise
首先改写toThunk
函数。增加promise
的判断
function isPromise(obj) {
return obj && 'function' == typeof obj.then;
}
function toThunk(obj){
if (isObject(obj) || isArray(obj)) {
return objectToThunk( obj);
}
if (isPromise(obj)) {
return promiseToThunk(obj);
}
return obj;
}
objectToThunk
函数的实现。同理还是把promise
转换成thunk
function promiseToThunk(promise){
return function(done){
promise.then(function(err,res){
done(err,res);
},done)
}
}
co的实现
co4.0完全抛弃了thunk风格的函数。全部转用promise。
4.0的用法
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
co的返回值不再是thunk函数了而是一个promise。并且也推荐yield后跟promise而不是thunk函数了。
function co(gen) {
//如果是generatorFunction,就执行 获得对应的generator对象
if (typeof gen === 'function') gen = gen.call(this);
//返回一个promise
return new Promise(function(resolve, reject) {
//初始化入口函数,第一次调用
onFulfilled();
//成功状态下的回调
function onFulfilled(res) {
var ret;
try {
//拿到第一个yield返回的对象值ret
ret = gen.next(res);
} catch (e) {
//出错直接调用reject把promise置为失败状态
return reject(e);
}
//开启调用链
next(ret);
}
function onRejected(err) {
var ret;
try {
//抛出错误,这边使用generator对象throw。这个的好处是可以在co的generatorFunction里面使用try捕获到这个异常。
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
//如果执行完成,直接调用resolve把promise置为成功状态
if (ret.done) return resolve(ret.value);
//把yield的值转换成promise
//支持 promise,generator,generatorFunction,array,object
//toPromise的实现可以先不管,只要知道是转换成promise就行了
var value = toPromise(ret.value);
//成功转换就可以直接给新的promise添加onFulfilled, onRejected。当新的promise状态变成结束态(成功或失败)。就会调用对应的回调。整个next链路就执行下去了。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//否则说明有错误,调用onRejected给出错误提示
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
function isPromise(obj) {
return 'function' == typeof obj.then;
}
thunk是给异步成功回调里传参执行next。promise是把next在then的成功回调里执行。
promise版的co库