co的源码简读

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)。之后的调用需要接下的封装完成

  1. 通过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。这一处在最后会继续谈到。

  1. promise自身是不用处理的
  2. generator无非递归co函数就好了,并且co自己就返回一个promise
  3. 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)
        })
    })
}
  1. 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不变。

猜你喜欢

转载自blog.csdn.net/weixin_38801333/article/details/80748910
今日推荐