【async/await 原理】之 Thunk 函数和 co 函数原理

Hi我是蜗牛,用直白的大白话来解释清楚复杂的问题,让新手朋友也能很好的接受。

async/await 原理网上一大堆,然后我发现很多朋友盲目的记住了 Promise + Generator = async/await,却忽略一些很重要的点,Thunk 函数和 co 模块,写这篇文章的目的是为了从0到1的将这两个函数剖析清楚,看完你对 async/await 的认识会再深刻一点

1. Generator 函数

要搞清楚 async/await,Generator 函数是基础,我们先看一个小demo

函数 A 用定时器模拟一个异步请求,我们希望 A 执行完毕之后 B 再执行

回调我们就不说了,你肯定知道

当然你不仅要知道 Promise 的用法,现在你还得知道 Generator 的语法:

Generator函数的特点:

  1. 函数声明成 function* 的样子就是 ES6 定义的 Generator 函数,Generator函数具有一个关键字 yeild,用来封印代码,任何代码前面有 yeild 关键字的,js引擎便不再自动按顺序执行它。
  2. Generator函数执行会得到一个 Generator 对象 ,该对象(g)中具有一个 next 方法,当我们人为的调用 g.next() 时,js引擎才会开始执行 Generator 函数中被封印的代码。
  3. g.next() 返回一个对象 { value: xxx, done: false },其中 value 代表的是当前某个 yeild 的执行结果,done: false 代表 Generator函数还没有执行完毕
  4. 不断地调用 g.next() 会让 js 引擎不断地执行 Generator 函数中的 yeild 封印 (一个next解开一个 yeild 封印)

为什么第一次之后再调用 g.next() 得到的 value 都是 undefined ?这里就要说到 Generator 函数的另一个特点

首先,执行顺序是这样的:

  1. 第一个 g.next() 执行,带来了 A() 的调用,但是 A 执行需要 1000 毫秒才能执行完毕
  2. 第二个 g.next() 执行是不会等待这 1000 毫秒的,所以 js引擎从 let a = yield A() 这一行之后继续执行,所以 a 变量 被打印,此时因为 A 还没执行完,所有 a 的值是 undefined。
  3. 第二个 g.next() 导致js引擎异步执行完了 第二个 yeild, 所以 B 函数被调用,因为 B 函数是同步代码,所以 打印的顺序是 undefined > 同步b > 异步A

好的那我用 2000 毫秒的定时器来执行第二个 g.next(),是不是变量 a 就有值了呢

你会发现,执行顺序确实是捋顺了,异步A先执行,同步B再执行,可是变量 a 的值还是 undefined呀?2000毫秒之后 yeild A() 早已有了结果,为什么变量 a 还是没有值呢?那你再看下面这份代码

你发现了,next函数是完全可以指定上一个 yeild 的执行结果的,这里我们在第二个 g.next() 的执行中传递了实参,它直接当做了 yeild A() 的执行结果,给到了变量 a

所以 Generator 函数还有一个特点,就是内部的 yeild A() 被执行的时候,哪怕 A() 有自己的返回结果,Generator 函数依然不采用,Generator 函数只认自己下一个 next 传入的值

举一个形象一点例子,我是 Generator 函数,我内部有一个 yeild A(),和一个 yeild B(),当我通过自己的 next 让 yeild A() 生效了之后,A有返回值,可是我不认可,我不理会,我再次调用 next() 如果没有接到参数,任谁来问我 yeild A() 的执行结果,我都说不知道,但如果第二次 调用 next('hello') 接受了参数 'hello',那么谁在问我 yeild A() 的执行结果,我都会告诉他是 'hello'

总结就一句话:yeild 的执行结果不是靠 yeild 后面接的函数来决定的,而是自己的下一个 next 决定的

明白了 Generator 函数以上的特点之后,这个 yeild 我就可以拿来做文章了,既然不调用next,yeild 就无法被执行,那我只要在合适的时机调用 next,整个 Generator 函数中的哪一行代码什么时候执行岂不是我们人为可控的了!

所以如何保证第一个 g.next() 执行完毕也就是它放开的那个 yeild 后续逻辑执行完毕后,我再去执行第二个 g.next() 就变成了我们现在要解决的首要问题了

而且,我猜你也行到了解决方案:

这样不就行了吗!.then 让一个 yeild 后面的 A() 先执行,且必须等到的 Promise 的状态要变更成 fulfilled 状态,才让第二个 g.next() 被调用,这样不就能保证 一定先执行 异步A,在执行同步B了嘛!

2. 当前面临的问题

你已经能理解,我们借助 Promise + Generator 可以手动控制异步代码和同步代码的执行顺序了,可是这么写目前依然存在一个很严峻的问题,看下面代码

执行顺序是我们理想中的顺序: 异步a > 异步b > 异步c

可是试想一下,如果我有 5个 这样的函数呢?10个呢?100个呢?

scss 复制代码
g.next().value.then(() => {
  g.next().value.then(() => {
    g.next()
    // ......
  })
})

这里的层级岂不是无穷尽了,所以我们得优化此处的代码,也叫,如何让 Generator 函数自动的执行下去

3. Thunk 函数

对于我们目前面对的问题,Thunk 函数这个概念就被引入用来解决这一问题。Thunk 函数是一种用于延迟执行的函数包装器,通常用于处理异步操作或生成器函数的执行。Thunk 函数的目标是将一个函数的执行推迟到稍后的时间,以便在需要时再执行。在 JavaScript 中,Thunk 函数通常是一个带有回调函数的函数包装器。

代码理解最方便:

在这个示例中,simpleThunk 函数接受一个回调函数作为参数,模拟了一个异步操作并在操作完成后调用回调函数。

现在,让我们将 Thunk 函数与生成器函数结合使用,以实现生成器函数的自动执行:

解释:

  1. 函数 simpleThunk 用于将其他函数包装成 Thunk 函数,我们将传入的异步函数 ABC 包装成一个 Thunk 函数,以便在生成器函数中使用。
  2. 异步函数 ABC,现在它们接受一个回调函数作为参数,并在异步操作完成后调用回调函数。
  3. 在生成器函数 exampleGenerator 中,我们使用 simpleThunk 包装了异步操作,然后使用 yield 关键字等待 Thunk 函数的执行。
  4. run 函数执行 Thunk 函数,并在异步操作完成后继续迭代生成器函数。

整理一下执行逻辑:

  1. run(exampleGenerator); run 函数中调用 exampleGenerator 的到对象(g)
  2. iterate(g); 执行了第一个 g.next(),这就开启了 yeild simpleThunk(A); 的执行,得到的仍然是一个函数体,所以 const { value, done } = g.next(); 中value还是一个函数
  3. 调用 value() 带来了 函数A 的执行,A在1秒钟之后才会调用自己内部的 callback,这就导致了 A函数没有执行完毕的话,iterate(g) 这一处的递归就无法开始
scss 复制代码
value(() => {
  iterate(g);
});

// () => { iterate(g) }  value接受到的这个回调函数 就是 A函数中的 callback
  1. A函数执行完毕,递归进去执行 B函数,B函数执行完毕,递归进去执行 C函数

这样一来,我们就通过打造一个 Thunk 函数和一个 Thunk 函数执行器(run)来实现了让 Generator 函数自动执行下一次层的 next(),解决了上述我们遇到的问题

4. co 模块

除了 Thunk 函数的方式,还有一个方法可以实现 Generator 的自动执行,就是 co 模块。co 模块是大佬 TJ Holowaychuk 封装的一个库,一个用于控制生成器函数执行的库,它允许你以同步的方式编写异步代码,使得生成器函数内部的异步操作看起来像同步代码一样。co 模块的实现基于 Promise 和生成器函数的特性,它自动迭代生成器并处理 Promise 对象的返回值。

它的用法非常简单:

那么它的实现原理呢?其实也非常简单,我们可以这样来实现它:

同样是使用递归,不过 co 借助了 Promise中的 then 方法,所以需要使用者注意 yeild 后面的内容一定要返回一个 Promise 对象,当上一个 yeild 执行完毕且状态变更为 fulfilled,then才能执行,也就才能走进下一层的递归。

5. 回顾 async/await

async/await 的语法:

对比上述我们通过 Generator + Thunk 和 Generator + Promise + co的手段 来实现将异步捋成同步后,可以很明确的看出 async/await 的实现就是由 Generator + Promise + co的手段 来封装的

6. Thunk和co的区别

co 函数和 Thunk 函数都是用于处理异步操作的工具,但它们之间存在一些关键区别。

  1. 用途:

    • co 函数通常用于协调和管理异步操作的流程,使得异步操作看起来像同步代码一样执行。它通常与生成器函数结合使用。
    • Thunk 函数主要用于封装异步操作,将其包装成一个函数,以便在需要时延迟执行。Thunk 函数通常用于构建异步流程控制工具。
  2. 执行方式:

    • co 函数是一个库或工具,它使用 Promise 和生成器函数的协作来实现异步控制。co 内部会自动执行生成器函数,并管理异步操作的执行流程。
    • Thunk 函数是一个函数包装器,它接受回调函数作为参数,并通常需要手动调用来执行。Thunk 函数的执行需要显式地调用,而不像 co 那样自动进行异步流程控制。
  3. 使用场景:

    • co 函数适用于较复杂的异步流程控制,例如需要按顺序执行多个异步操作、处理错误等情况。它在管理多个异步任务时非常有用。
    • Thunk 函数通常用于构建异步库或处理单一异步操作的情况,它更侧重于将异步操作封装成可延迟执行的函数,以便在需要时执行。

简而言之,co 和 Thunk 函数都有各自的用途和优势,选择使用哪个取决于你的具体需求和代码结构。 co 更适合复杂的异步控制流程,而 Thunk 函数更适合将异步操作封装成可延迟执行的函数。当然,实际开发过程中,肯定是直接 async/await呀,香喷喷。

相关推荐
uhakadotcom3 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰3 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪3 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy4 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom5 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom5 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom5 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom5 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试