【源码共读】第4期 | co 源码

1. 前言

2. 基础知识

co 是用于将 Generator 函数转换为 Promise 并执行,让其能够像 async、await函数一样自动执行。

2.1 生成器

生成器传递参数

2.2 生成器使用案例

参考若川大佬的文章中的co源码解析,模仿写一个。

  1. generator的执行
js 复制代码
// 模拟请求
function request(ms = 1000) {
        return new Promise(resolve => {
                setTimeout(() => {
                        resolve(111)
                }, ms)
        })
}
// 生成器函数
function* generatorFunc() {
        const res = yield request()
        console.log('打印***res', res)
}

generatorFunc()

会得到一个迭代器对象,只有调用next方法才会得到结果,不执行不会得到结果。那么能不能写一个自动执行的工具完成这个功能。

  1. 第一版-单个yield自动执行
js 复制代码
function request(ms = 1000) {
        return new Promise(resolve => {
                setTimeout(() => {
                        resolve(111)
                }, ms)
        })
}

function* generatorFunc() {
        const res = yield request()
        console.log('打印***res', res)
}

function coSimple(gen) {
        gen = gen()
        console.log('打印***gen', gen)

        const ret = gen.next()
        const promise = ret.value

        promise.then(res => {
                gen.next(res)
        })
}

coSimple(generatorFunc)

使用next进行调用,将结果包装成promise 执行结果使用gen.next(res)返回,不调用next,则生成器函数yield后的res为undefined。

  1. 第二版-考虑多个yield和传参
js 复制代码
function* generatorFunc2(suffix=''){
        const res = yield request()
        console.log('打印***res-1'+suffix,res)

        const res2 = yield request()
        console.log('打印***res-2'+suffix,res2)
}

function coSimple2(gen){
        const ctx = this
        const args = Array.prototype.slice.call(arguments,1)// 转成数组
        gen = gen.apply(ctx,args)
        console.log('打印***gen',gen)

// 第一个调用
        const ret = gen.next()
        console.log('打印***ret',ret)

        const promise = ret.value
        promise.then(res=>{
                console.log('打印***res',res)
                const ret = gen.next(res)// 此处不传入,则yield后面读取不到res,111
                const promise = ret.value
                promise.then(res=>{
                        gen.next(res)
                })
        })
}

coSimple2(generatorFunc2,'888')

此处多个yield调用使用嵌套方式,每次手动获取value。那么能不能自动手动,知道结束

  1. 第三版-无限制yield
js 复制代码
let index = 0
function request(ms = 1000) {
        return new Promise(resolve => {
                setTimeout(() => {
                        resolve(index++)
                }, ms)
        })
}


function* generatorFunc3(suffix=''){
        const res = yield request()
        console.log('打印***res-1'+suffix,res)

        const res2 = yield request()
        console.log('打印***res-2'+suffix,res2)

        const res3 = yield request()
        console.log('打印***res-3'+suffix,res3)

        const res4 = yield request()
        console.log('打印***res-4'+suffix,res4)
}

function coSimple3(gen){
        const ctx = this
        const args = Array.prototype.slice.call(arguments,1)
        gen = gen.apply(ctx,args)

        console.log('打印***gen',gen)

        return new Promise(()=>{
                function onFulfilled(res){
                        const ret = gen.next(res)
                        console.log('打印***ret',ret)
                        next(ret)
                }

                function next(ret){
                        const promise = ret.value
                        promise&&promise.then(onFulfilled)
                }
                onFulfilled()
        })
}

coSimple3(generatorFunc3)

修改了请求,方便看出每次参数的传递。

返回promise,定义onFulfilled,去手动调用next执行request,使用next获取上一次的结果,并调用自身,直到所有yield执行完毕。

3. 源码解析

3.1 函数方法

  1. slice:用于创建一个对 Array.prototype.slice 的引用。
  2. co:主要函数,将 Generator 函数或 Generator 对象转换为 Promise 并执行。
  3. co.wrap:将给定的 Generator 函数包装成一个返回 Promise 的函数。
  4. toPromise:将 yield 值转换为 Promise。
  5. thunkToPromise:将 thunk(接受回调的函数)转换为 Promise。
  6. arrayToPromise:将包含多个 "yieldables" 的数组转换为 Promise。
  7. objectToPromise:将包含多个 "yieldables" 的对象转换为 Promise。
  8. isPromise:检查一个对象是否为 Promise。
  9. isGenerator:检查一个对象是否为 Generator(具有 nextthrow 方法)。
  10. isGeneratorFunction:检查一个函数是否为 Generator 函数。
  11. isObject:检查一个值是否为普通对象。

3.2 co函数

js 复制代码
function co(gen) {
  var ctx = this; // 保存当前上下文
  var args = slice.call(arguments, 1); // 获取除了第一个参数(gen)之外的其他参数

  // 我们将所有内容包装在一个 Promise 中,以避免 Promise 链式调用导致的错误。
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args); // 如果 gen 是函数,则执行函数并将结果赋值给 gen
    if (!gen || typeof gen.next !== 'function') return resolve(gen); // 如果 gen 不是函数,或者不具有 next 方法,则直接返回结果为 gen 的 Promise

    onFulfilled();

    // 当前步骤执行成功时的处理函数
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res); // 执行生成器的 next 方法,并将结果赋值给 ret
      } catch (e) {
        return reject(e); // 如果出现异常则将异常作为 Promise 的拒绝理由
      }
      next(ret); // 继续执行下一步
      return null;
    }

    // 当前步骤执行失败时的处理函数
    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err); // 执行生成器的 throw 方法,并将结果赋值给 ret
      } catch (e) {
        return reject(e); // 如果出现异常则将异常作为 Promise 的拒绝理由
      }
      next(ret); // 继续执行下一步
    }

    // 获取生成器的下一个值,并返回一个 Promise
    function next(ret) {
      if (ret.done) return resolve(ret.value); // 如果生成器完成,则将生成器的结果作为 Promise 的解决值
      var value = toPromise.call(ctx, ret.value); // 将生成器的返回值转换为 Promise
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // 如果返回值是 Promise,则等待 Promise 的状态并执行相应的处理函数
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"')); // 如果生成器的返回值不是函数、Promise、Generator、数组或对象,则拒绝 Promise
    }
  });
}

co 函数中,它会首先判断传入的参数 gen 是否为 Generator 函数,如果是则执行该函数得到 Generator 对象。然后,在一个新的 Promise 中执行 Generator 对象的 next 方法,根据返回的结果进行相应的处理,直到 Generator 完成(done 为 true),最终返回 Promise 的结果。

4. 总结

通过学习 co 源码,学到了:

  1. 如何使用生成器函数和 yield 关键字来编写异步代码。
  2. 如何使用生成器函数的 next()throw() 方法来控制生成器的执行流程。
  3. 如何处理异步操作的返回值。co 函数会将异步操作的返回值作为参数传递给生成器函数的 yield 表达式,并将生成器函数的执行流程暂停,直到下一次调用 next() 方法。

一起学习,共同成长。O^O


ES6 系列之 Generator 的自动执行

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

相关推荐
请叫我欧皇i15 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_18 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun24 分钟前
空间数据存储格式GeoJSON
前端
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
猫爪笔记1 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
brief of gali1 小时前
记录一个奇怪的前端布局现象
前端
Json_181790144802 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网3 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02043 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing3 小时前
React核心功能详解(一)
前端·react.js·前端框架