美文网首页
JavaScript 生成器详解

JavaScript 生成器详解

作者: 涅槃快乐是金 | 来源:发表于2024-09-04 08:59 被阅读0次

    生成器最常见于 sagas 中,但它们有更多的使用场景。你将在本文中看到其中的一些。

    对于“什么是生成器?”这个问题的简短回答是:

    生成器是 JavaScript 中的拉取流(pull stream)。

    现在我们来剖析这个定义,然后跳入一些示例中。

    首先,你需要理解两个术语:“拉取(pull)”和“流(stream)”。

    什么是流?

    流是随时间传递的数据。流有两种类型:推送流(push stream)和拉取流(pull stream)。

    什么是推送流?

    推送流是一种机制,你无法控制数据何时到达。

    推送流的例子包括:

    • websocket,
    • 从磁盘读取文件,
    • 服务器发送事件。

    你可以在下面看到一个使用 Node.js 从磁盘读取大文件的推送流 JavaScript 示例:

    const fs = require('fs');
    const readStream = fs.createReadStream('./largeFile.txt');
    
    readStream.on('data', chunk => {
      console.log('data received', chunk.length);
    });
    
    readStream.on('end', () => {
      console.log('finished reading file');
    });
    
    readStream.on('error', error => {
      console.log('an error occurred while reading the file', error);
    });
    

    什么是拉取流?

    拉取流是指你可以控制何时请求数据。

    你很快会在生成器代码中看到 JavaScript 的拉取流示例,但首先你需要理解另一个概念。

    惰性 vs. 饥渴

    在编程中,数据处理可以通过两种基本方式进行:饥渴(eager)或惰性(lazy)。

    饥渴

    饥渴意味着数据会立即被评估,无论当下是否需要结果。推送流就是饥渴的。(其他例子:数组方法、Promise)

    // 使用数组方法的饥渴求值:
    const numbers = [1, 2, 3, 4, 5];
    
    // Map 方法会立即处理数组中的所有元素。
    const squares = numbers.map(num => {
      console.log(`Squaring ${num}`);
      return num * num;
    });
    
    console.log('squares:', squares); // [1, 4, 9, 16, 25]
    console.log('squares:', squares); // [1, 4, 9, 16, 25]
    

    你可能会想:“好吧,但为什么 Promise 是饥渴的呢?它们的结果是延迟到达的。”

    JavaScript 中的 Promise 表现出饥渴求值有以下几个原因。

    • 立即执行:传递给新 Promise 的函数(称为执行器函数)在 Promise 构造时立即执行。
    • 不可逆操作:一旦执行器函数开始执行,无法通过消费代码停止或暂停。它执行的操作(无论是成功解析还是拒绝)会被排队到 JavaScript 事件循环中尽快处理。
    • 没有惰性选项:Promise 没有内置机制来推迟或取消执行器的执行,直到需要值时才开始。
    • 副作用:Promise 的饥渴特性意味着,任何包含在执行器中的副作用(如 API 调用、超时或 I/O 操作)都会立即发生。

    下面的示例演示了 Promise 的立即执行。

    // 使用 Promise 和数组方法的饥渴求值
    
    console.log("Before promise");
    
    let promise = new Promise((resolve, reject) => {
      console.log("Inside promise executor");
      resolve("Resolved data");
    });
    
    console.log("After promise");
    
    promise.then(result => {
      console.log(result);
    });
    

    输出结果如下:

    $ node eager-promise-example.js
    Before promise
    Inside promise executor
    After promise
    Resolved data
    

    惰性

    惰性意味着只有在需要值时才进行求值(而不是提前)。拉取流就是惰性的。

    一个同步的例子是操作数选择运算符。

    // 使用逻辑运算符进行惰性求值
    
    function processData(data) {
        console.log(`Processing ${data}`); // 这行代码永远不会被执行 🚫
        return data * data;
    }
    
    console.log('惰性求值开始');
    const data = 5;
    const isDataProcessed = false;
    
    // 使用逻辑与运算符进行惰性求值。
    const result = isDataProcessed && processData(data);
    console.log('结果:', result); // false
    

    运行这段代码时,你会看到如下输出:

    $ node lazy-evaluation-example.js
    惰性求值开始
    结果: false
    

    由于 isDataProcessedfalse,因此 processData 函数从未执行,你也不会在控制台看到 “Processing 5”。这表明表达式只会计算得到结果所需的部分。

    什么是生成器?

    生成器是 JavaScript 中的拉取流。这意味着它是一种特殊的函数,你可以暂停执行并稍后恢复。

    生成器函数返回的 Generator 对象符合可迭代协议和迭代器协议。

    function* myGenerator() {
      yield "Hire senior";
      yield "React engineers";
      yield "at ReactSquad.io";
    }
    
    const iterator = myGenerator();
    
    // 将生成器用作迭代器
    console.log(iterator.next()); // { done: false, value: "Hire senior" }
    console.log(iterator.next()); // { done: false, value: "React engineers" }
    console.log(iterator.next()); // { done: false, value: "at ReactSquad.io" }
    console.log(iterator.next()); // { done: true, value: undefined }
    
    // 将生成器用作可迭代对象
    for (let string of myGenerator()) {
      console.log(string); // "Hire senior", "React engineers", "at ReactSquad.io"
    }
    

    除了 .next() 方法,生成器还有 .return().throw() 方法。

    • .return() - .return() 方法终止生成器的执行并返回指定的值,还会触发任何 finally 代码块。
    • .throw() - .throw() 方法允许在生成器的最后一个 yield 处抛出一个错误,该错误可以被捕获和处理,或者允许生成器通过 finally 代码块进行清理。如果未捕获,它会停止生成器并将其标记为完成。
    function* numberGenerator() {
      try {
        yield 1;
        yield 2;
        yield 3;
      } finally {
        console.log("清理完成");
      }
    }
    
    const generator = numberGenerator();
    
    // 正常使用生成器
    console.log(generator.next()); // { done: false, value: 1 }
    console.log(generator.next()); // { done: false, value: 2 }
    
    // 使用 return() 提前结束生成器
    console.log(generator.return(10)); // { done: true, value: 10 }
    // 在 return() 之后,不再生成新的值
    console.log(generator.next()); // { done: true, value: undefined }
    
    // 为 throw 示例重置生成器
    const newGenerator = numberGenerator();
    console.log(newGenerator.next()); // { done: false, value: 1 }
    
    // 使用 throw() 抛出错误信号
    try {
      newGenerator.throw(new Error("出错了"));
    } catch (e) {
      console.log(e.message); // "出错了"
    }
    // 在 throw() 之后,生成器关闭
    console.log(newGenerator.next()); // { done: true, value: undefined }
    

    你也可以在调用 .next() 时传入数字或其他值。

    试着预测以下示例中的日志输出。

    function* moreNumbers(x) {
        console.log('x', x);
        const y = yield x + 2;
        console.log('y', y);
        const z = yield x + y;
        console.log('z', z);
    }
    
    const it2 = moreNumbers(40);
    
    console.log(it2.next());
    console.log(it2.next(2012));
    console.log(it2.next());
    

    此示例演示了生成器函数 moreNumbers 如何根据每次调用 .next() 时收到的输入操作和生成值。

    看看输出,并检查你的预测。

    const it2 = moreNumbers(40);
    
    // x: 40
    console.log(it2.next()); // { value: 42, done: false }
    
    // y: 2012
    console.log(it2.next(2012)); // { value: 2052, done: false }
    
    // z: undefined
    console.log(it2.next()); // { value: undefined, done: true }
    

    我们逐步分析 moreNumbers 生成器函数的每一步,以便你充分理解它。

    步骤 代码行 控制台输出 解释
    1 const it2 = moreNumbers(40) 初始化生成器,x 设置为 40。
    2 console.log(it2.next()); { value: 42, done: false } 生成器启动并将 x 记录为 40,然后生成 42 (x + 2)。
    3 console.log(it2.next(2012)); { value: 2052, done: false } 继续执行,y 为 2012,记录 y,生成 2052 (x + y)。
    4 console.log(it2.next()); { value: undefined, done: true } 继续执行,zundefined(没有新的输入),完成执行。

    生成器的使用场景

    生成器有三大主要使用场景:

    1. 惰性求值 - 按需生成数据或处理大型或无限的数据集。
    2. 异步编程 - 处理异步操作。
    3. 迭代器 - 允许在复杂流程的各个步骤之间暂停执行。

    之前你看到了将从磁盘读取文件作为推送流的示例。下面是如何使用生成器将其转换为拉取流的代码:

    const fs = require('fs');
    
    function getChunkFromStream(stream) {
        return new Promise((resolve, reject) => {
            stream.once('data', (chunk) => {
                stream.pause();
                resolve(chunk);
            });
    
            stream.once('end', () => {
                resolve(null);
            });
    
            stream.once('error', (err) => {
                reject(err);
            });
    
            stream.resume();
        });
    }
    
    async function* readFileChunkByChunk(filePath) {
        const stream = fs.createReadStream(filePath);
        let chunk;
    
        while (chunk = await getChunkFromStream(stream)) {
            yield chunk;
        }
    }
    
    const generator = readFileChunkByChunk('./largeFile.txt');
    
    (async () => {
        for await (const chunk of generator) {
            console.log("数据接收", chunk.length);
        }
    })();
    

    实际案例

    Sagas 是处理异步 I/O 操作的一个典型例子。但你将在 Redux 系列文章中学习如何使用 Sagas

    通常,当你想控制何时获取一个值时,会使用生成器。

    来看下面这个测试示例:

    test('当用户为已加入的组织所有者时:显示邀请链接创建 UI 以及组织成员,并允许用户更改其角色', async ({ page }) => {
      // 组织中角色的生成器。
      function* roleGenerator() {
        const allRoles = Object.values(ORGANIZATION_MEMBERSHIP_ROLES);
        for (const role of allRoles) {
          yield role;
        }
      }
      
      const roleIterator = roleGenerator();
      const data = await setup({
        page,
        role: ORGANIZATION_MEMBERSHIP_ROLES.OWNER,
        numberOfOtherTeamMembers: allRoles.length,
      });
      
      const { organization, sortedUsers, user } = data;
    
      // 导航到团队成员设置页面。
      await page.goto(`/organizations/${organization.slug}/settings/team-members`);
    
      // 通过生成器为每个团队成员分配角色。
      for (let index = 0; index < sortedUsers.length; index++) {
        const memberListItem = page.getByRole('list', { name: /team members/i }).getByRole('listitem').nth(index);
        const otherUser = sortedUsers[index];
    
        // 为每个团队成员更改角色,除了当前用户。
        if (otherUser.id !== user.id) {
          await memberListItem.getByRole('button', { name: /member/i }).click();
          const role = roleIterator.next().value!;
          await page.getByRole('option', { name: role }).getByRole('button').click();
          await page.keyboard.press('Escape');
        }
      }
    
      await teardown(data);
    });
    

    在此测试中,定义了一个 roleGenerator 来顺序提供组织用户的角色列表。该方法允许测试在 UI 中的角色管理功能中为每个用户动态分配预定义列表中的唯一角色。

    在这个例子中使用生成器而不是数组的原因是,测试中主用户在 sortedUsers 数组中的位置是未知的,而生成器是一个拉取流,因此你可以按需获取角色值。

    相关文章

      网友评论

          本文标题:JavaScript 生成器详解

          本文链接:https://www.haomeiwen.com/subject/xeylljtx.html