generator
generator是es6里新定义的一种函数。声明这种函数需要在function之后加‘*’:
function* gen(){
var a = yeild 'hello'
console.log(a)
var b = yeild 'world'
return b
}
这种函数是GeneratroFunction类型的:
gen.constructor.name == "GeneratorFunction"
下面展示它的调用:
//生成一个generator,函数体不执行
var genFn = gen()
//执行到第一个yield语句,返回{done:false,value:'hello'}
var a = genFn.next()
//执行第一个yield到第二个yield的语句,包括第二个yield语句。
var b = genFn.next(a.value)
//执行 return
var res = genF.next(b.value)
//如果继续执行genF.next,返回{done:true,value:undefined}
这种函数一开始的初衷是用来引入协程的,后来变成了一种异步编程的解决方案。
它的最大的优势是:通过next控制函数体的执行流。
自然的,异步变同步的编程就可以通过generator很容易的实现。
generator的异步编程
异步函数:返回的数据通过回调函数返回。
比如
setTimeout(function(){},1)
readFile(filePath,callback)
如何改成同步的形式呢,比如直接返回数据:
var value = setTimeout(1)
var value = readFile(filePath)
通过generator就可以实现:
1. 分离异步函数的callback(thunk函数)
//分离callback,只需要外部包一个函数
function readFile(filePath){
return funtion(cb){
readFile(filePath,callback)
}
}
//更一般的就是将函数A的运行A(a,b,c,cb) = A(a,b,c)(cb)
//下面给出这一般的通用实现
function thunkFy(fn){
var ctx = this
//A的参数不确定,写成无参
//A的返回是一个单参函数
return function(){
var args = [].slice.call(arguments)
//返回单参函数
return function(done){
args.push(done)
fn.apply(this,args)
}
}
}
第一步将callback分离之后,真正给开发者调用的是A(a,b,c)。之后的调用需要接下的封装完成
- 通过yield返回
function* gen(){
var value = yield readFile(filePath)
...
}
通过yield返回的是一个需要callback的函数,并不是我们想要的异步数据。但是要明白:
value的真正值是执行gen()的next传进入的,不是yield返回的。
3. 通过运行器执行generator
var genFn = gen()
genFn.next().value(function(data){
genFn.next(data)
})
//将上面的写法包装一下
function run(fn){
var gen = fn()
//callback
function next(data){
var ret = gen.next(data)
if(ret.done) return;
ret.value(next)
}
next()
}
完成上面的步骤,我们就可以用同步方式写异步代码了:
//声明generator
fucntion* gen(){
...
var value = yield thunkFy(readFile)(filepath)
...
}
run(gen)
上面的写法中,要求yield的返回必须是thunk函数。有时候返回的是promise呢?
这个时候co就出现了,将thunk函数和promise统一做了处理。
co
co具体做了什么,相信上面已经说的很清楚了。下面主要讲解co源码,从中学习一些编程的经验。
co主要代码分两部分:
1. 递归next
2. 将所有yield的返回,Promise化
在开始这两部分之前,先把框架搭建一下。
function co(gen){
//后面很多地方函数的调用都通过call和apply,主要就是控制this对象的一致性。
//我们可以通过co(gen).bind(this)将this传给gen。
var ctx = this
//这里选择了promise作为返回值的包裹。
//异步返回promise已经越来越趋近主流
return new Promise(function(resolve,reject){
//递归
function next(res){
}
//promise化
function toPromise(obj){
}
})
}
递归
递归最难的地方应该是找到边界条件,我们这里就非常简单了就是next返回的done为true的时候。
//雏形
function next(res){
var ret = gen.next(res)
if(ret.done) return resolve(ret.value)
next(ret)
}
//value转化为promise
function next(res){
var ret = gen.next(res)
if(ret.done) return resolve(ret.value)
var value = toPromise.call(ctx,ret.value)
//开始递归,执行promise的then方法继续执行本next函数;
//本next的参数是:前一个promise的resolve的value,即前一个yield返回值
if(value && isPromise(value)) value.then(next,onReject)
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
源码将上面的next函数写成了两个函数:
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, 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) + '"'));
}
最后可以看到,yield后面不能跟基本数据类;
但是引用类型里有基本数据类型是可以的。比如:{a:1,b:”hello”,c:null}等
Promise化
co支持yield a function, promise, generator, array, or object。
为了统一处理这么多种情况,先把它们都转化成promise,最后统一用promise的then方法获取异步返回值。
function toPromise(obj){
if(!obj) return obj;
//promise
if(isPromise(obj)) return obj;
//object
if(isObject(obj)) return objectToPromise.call(this,obj)
//array
if(Array.isArray(obj)) return arrayToPromise.call(this,obj)
//function
if(isThunk(obj)) return thunkToPromise.call(this,obj)
//generator
if(isGeneratorFunction(obj)) return co.call(this,obj)
return obj
}
虽然支持5中数据类型,最终会promise化的只有function。其他4种去掉promise自身,还有3个通过递归遍历内部value,直到function,promise。
还有一点,递归过程中,对于array,object类型,内部的value为基本数据类型,统一转换到了promise。这一处在最后会继续谈到。
- promise自身是不用处理的
- generator无非递归co函数就好了,并且co自己就返回一个promise
- function,这里的function只能是thunk函数:
function thunkToPromise(obj){
return new Promise(function(resolve,reject){
obj.call(ctx,function(err,res){
if(err) return reject(err)
if(arguments.length > 2) res = [].slice.call(arguments,1)
resolve(res)
})
})
}
- array和object是一样的,都是遍历出每个value,进行promise化,array自身可以通过map遍历,写起来比较简单:
function arrayToPromise(obj){
//
return Promise.all(obj.map(toPromise,this))
}
function objectToPromise(obj){
//
var keys = Object.key(obj)
var results = {};
var promises = [];
for(var i =0; i< keys.length;i++){
var key = keys[i]
var value = obj[key]
var promise = toPromise.call(this,value)
if(promise && isPromise(promise) defer(key,promise)
else results[key] = value
}
//1.此时results并不一定包含所有的结果
//2.但是我们知道promises都执行完之后,results肯定就获取到了所有的value
return Promise.all(promises).then(function(){
return results
})
function defer(key,promise){
results[key] = undefined
promises.push(promise.then(function(value){
//此次的执行是不确定的
//但是可以确定:它会在之后的then方法的回调函数之前执行
results[key] = value
}))
}
}
最后还要强调一下,toPromise,co等函数的递归,都是通过call方法调用的。这主要也是要为了保证整个调用链里的this不变。