【源码共读】第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原理

相关推荐
梨子同志7 分钟前
qiankun
前端
梨子同志16 分钟前
Nginx
前端
梨子同志18 分钟前
NestJS
前端
梨子同志33 分钟前
Next.js
前端
谭光志43 分钟前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
Hilaku1 小时前
前端架构师的克制:什么时候该坚决对新技术说不?
前端·javascript·程序员
陈随易1 小时前
前端项目部署只要30秒
前端·后端·程序员
本末倒置1831 小时前
从 Vue CLI 迁移到 Rsbuild 后,JSX 突然全挂了?一次排查记录
前端
古夕1 小时前
多应用同域部署下 Favicon 异常问题排查与治理
前端·javascript
YIAN1 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp