IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    了解JavaScript中的生成器函数(Generator)

    Tqing发表于 2023-10-28 13:15:01
    love 0

    为什么使用Generator?

    在JavaScript使用异步操作时,在async和await还没有被JavaScript官方正式推出时,那么异步操作解决方案就只有回调函数和Promise。

    回调函数

    所谓回调函数,就是把需要执行的动作以函数的方式包装起来,再将这个函数以参数的方式传递给其他的函数,当时机到来时再进行调用。

    // 需在浏览器中运行
    function loadImage(imgUrl, callback) {
      const img = document.createElement("img");
      img.onload = function () {
        callback(this);
      };
      img.src = imgUrl;
    }
    
    loadImage(
      "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
      (img) => {
        document.body.appendChild(img);
        loadImage(
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
          (img) => {
            document.body.appendChild(img);
            loadImage(
              "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
              (img) => {
                document.body.appendChild(img);
              }
            );
          }
        );
      }
    );

    Promise

    promise是为了解决回调函数产生的回调地狱问题而产生的。

    function loadImage(imgUrl) {
      return new Promise((resolve) => {
        const img = document.createElement("img");
        img.onload = function () {
          resolve(this);
        };
        img.src = imgUrl;
      });
    }
    
    loadImage(
      "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg"
    )
      .then((img) => {
        document.body.appendChild(img);
        return loadImage(
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg"
        );
      })
      .then((img) => {
        document.body.appendChild(img);
        return loadImage(
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg"
        );
      })
      .then((img) => {
        document.body.appendChild(img);
      });

    虽然解决了回调地狱的问题,异步任务执行步骤也更加清晰了,但是相比于现在async和await的异步操作同步化的表达方式还是略逊一筹,在async和await还没有推出时,社区就已经利用生成器函数实现了社区版的async和await,当时生成器的出现就是为了服务于异步编程。

    使用方式

    创建方式

    function* getValue() {
      yield 1;
      yield 2;
      return 3;
    }
    
    const generator = getValue() // 获取到生成器
    console.log(Object.prototype.toString.call(generator)); // [object Generator]

    next方法

    获取返回值

    生成器函数和普通函数的调用方式完全不同,上面代码虽然调用生成器函数产生了生成器,但是生成器函数内部的代码并为被执行。

    那么需要怎样操作才能让生成器内部的代码执行?

    生成器有一个next方法,当这个方法被调用时,会把yield后面的值返回回来,并且生成器函数内部的代码停止执行,当再次调用next方法后,生成器函数内部代码会从上次暂停处开始执行,到下一个yield语句处停止。

    next()方法返回的对象包含两个属性:

    • value:返回的值,也就是yield关键字后面的值。
    • done:表示生成器函数是否已经完成,true表示已经完成,false表示未完成。

      function* getValue() {
      yield 1;
      yield 2;
      yield 3;
      }
      
      const generator = getValue();
      
      console.log(generator.next()); // { value: 1, done: false }
      console.log(generator.next()); // { value: 2, done: false }
      console.log(generator.next()); // { value: 3, done: false }
      console.log(generator.next()); // { value: undefined, done: true }

      当生成器内最后的yield语句也执行过后,意味着整个生成器函数执行完毕,调用next()方法产出的结果中done的值为true了。

    简单用图片演示下运行流程:

    第一次调用next()方法,生成器函数会暂停在第一个yield语句

    yield 1.png

    再次调用next()方法,生成器函数会暂停在第二个yield语句

    yield 2.png

    再次调用next()方法,生成器函数会暂停在第三个yield语句

    yield 3.png

    再次调用next()方法,生成器函数执行完毕

    return undefined.png

    这里返回的value为什么是undefined,其实可以这么理解,因为在JavaScript中,函数不主动return,那么会默认返回undefined。

    function* getValue() {
      yield 1;
      yield 2;
      yield 3;
      return undefined;
    }

    如果生成器函数中有return语句,那么在执行return语句的时候就会把生成器的状态置为已完成,后面的yield语句将不再被执行,这点和普通函数是保持一致的。

    function* getValue() {
      yield 1;
      yield 2;
      return 3;
    }
    
    const generator = getValue();
    
    console.log(generator.next()); // { value: 1, done: false }
    console.log(generator.next()); // { value: 2, done: false }
    console.log(generator.next()); // { value: 3, done: true }

    传递参数

    next函数其实也可以传递参数,这个参数将被yield语句消费。

    function* getValue() {
      const a = yield 1;
      const b = yield 2 * a;
      return 3 * b;
    }
    
    const generator = getValue();
    
    console.log(generator.next()); // { value: 1, done: false }
    console.log(generator.next(5)); // { value: 10, done: false }
    console.log(generator.next(6)); // { value: 18, done: true }

    还是使用图片来演示整个流程。

    1. 首先调用next(),语句在第一个yield语句处暂停
    2. 然后调用next(5),参数5会传递给yield语句等号左边的变量a,语句会停在第二个yield语句处
    3. 然后调用next(6),参数6会传递给yield语句等号左边的变量b,碰到return语句直接返回,整个生成器函数执行完毕。

    next-参数.png

    throw函数

    next函数可以往生成器里面传递函数,而throw方法可以往生成器函数里面抛出异常,如果没有在生成器函数里面捕获异常,那么生成器函数会向其它函数一样抛出异常。

    function* getValue() {
      yield 1;
      try {
        yield 2;
      } catch (error) {
        console.error(error); // 捕获异常, 打印 trhow error
      }
      yield 3;
    }
    
    const generator = getValue();
    
    console.log(generator.next()); // { value: 1, done: false }
    console.log(generator.next(5)); // { value: 2, done: false }
    console.log(generator.throw("throw error")); // {value: 3, done: false}
    

    return函数

    reutrn函数在被调用后,生成器函数会直接返回return函数传递的值,而且生成器函数整个函数执行完毕。

    function* getValue() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    const generator = getValue();
    
    console.log(generator.next()); // { value: 1, done: false }
    console.log(generator.return(5)); // { value: 5, done: true } // 此时生成器函数执行完毕
    console.log(generator.next()); // {value: undefined, done: true}

    使用场景

    实现Async和Await

    function getValue(n, ms = 1000) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(n);
        }, ms);
      });
    }
    
    function* generator() {
      console.log(1);
      let x = yield getValue(2);
      console.log(x);
      x = yield getValue(3, 2000);
      console.log(x);
      x = yield 4;
      console.log(x);
    }
    
    function asyncGenerator(generator) {
      return new Promise((resolve, reject) => {
        let iterable = generator();
        let generated = iterable.next();
        tick();
    
        function tick() {
          if (generated.done === false) {
            Promise.resolve(generated.value).then(
              (value) => {
                try {
                  generated = iterable.next(value);
                  tick();
                } catch (err) {
                  reject(err);
                }
              },
              (reason) => {
                try {
                  generated = iterable.throw(reason);
                  tick();
                } catch (err) {
                  reject(err);
                }
              }
            );
          } else {
            resolve(generated.value);
          }
        }
      });
    }
    
    asyncGenerator(generator);
    

    借用上面实现asyncGenerator函数,我们可以再来优化下加载图片的代码

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body></body>
      <script>
        const imgUrlList = [
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/1716878f-79a2-4db1-af8c-b9c2039f0b3c_product_W572_H370.jpg",
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/8b36f9a7-f780-4e71-b719-9300109a9ff2_product_W572_H370.jpg",
          "https://travel.12306.cn/imgs/resources/uploadfiles/images/6d77d0ea-53d0-4518-b7e9-e53795b4920c_product_W572_H370.jpg",
        ];
    
        function loadImage(imgUrl) {
          return new Promise((resolve) => {
            const img = document.createElement("img");
            img.onload = function () {
              resolve(this);
            };
            img.src = imgUrl;
          });
        }
    
        function* loadImageList() {
          const img1 = yield loadImage(imgUrlList[0]);
          document.body.appendChild(img1);
          const img2 = yield loadImage(imgUrlList[1]);
          document.body.appendChild(img2);
          const img3 = yield loadImage(imgUrlList[2]);
          document.body.appendChild(img3);
        }
    
        function asyncGenerator(generator) {
          return new Promise((resolve, reject) => {
            let iterable = generator();
            let generated = iterable.next();
            tick();
    
            function tick() {
              if (generated.done === false) {
                Promise.resolve(generated.value).then(
                  (value) => {
                    try {
                      generated = iterable.next(value);
                      tick();
                    } catch (err) {
                      reject(err);
                    }
                  },
                  (reason) => {
                    try {
                      generated = iterable.throw(reason);
                      tick();
                    } catch (err) {
                      reject(err);
                    }
                  }
                );
              } else {
                resolve(generated.value);
              }
            }
          });
        }
    
        asyncGenerator(loadImageList).then(() => {
          console.log("load success");
        });
      </script>
    </html>
    

    实现自定义迭代器

    const list = {
      head: {
        value: 1,
        next: {
          value: 2,
          next: {
            value: 3,
            next: null,
          },
        },
      },
      *[Symbol.iterator]() {
        let curNode = list.head;
        while (curNode) {
          yield curNode.value;
          curNode = curNode.next;
        }
      },
    };
    
    for (let val of list) {
      console.log(val);
    }

    上面针对链表数据结构使用for...of的进行遍历,这得益于在list内部声明了迭代器,有两点是for...of所需要的

    • 返回包含next方法的对象
    • 调用返回的next方法返回的对象包含value和done这两个属性

    而这两点生成器函数全部满足,无疑是天作之合。



沪ICP备19023445号-2号
友情链接