Skip to content

Generator函数 ​

引入资源 ​

  • 摘自

基本概念 ​

  1. 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态
  2. 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  3. 形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
  4. Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
  5. 必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行
    js
        function* helloWorldGenerator() {
          yield 'hello';
          yield 'world';
          return 'ending';
        }
        var hw = helloWorldGenerator();     
        hw.next()
        // { value: 'hello', done: false }
        hw.next()
        // { value: 'world', done: false }
        hw.next()
        // { value: 'ending', done: true }
        hw.next()
        // { value: undefined, done: true }
  6. yield表达式只能用在 Generator 函数里面,用在其他地方都会报错
  7. yield表达式如果用在另一个表达式之中,必须放在圆括号里面
  8. yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号

应用 ​

  1. 异步操作同步化
    js
          var clock = function* () {
            console.log(111);
            yield console.log(222);
            console.log(333);
          };
          let c = clock()
          c.next() // 111  222
          c.next() // 333

async和await ​

  1. async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await async会把后面的函数执行后return的结果包装成一个resolve的promise
    js
        const fs = require('fs');
        const readFile = function (fileName) {
          return new Promise(function (resolve, reject) {
            fs.readFile(fileName, function(error, data) {
              if (error) return reject(error);
              resolve(data);
            });
          });
        };
        const gen = function* () {
          const f1 = yield readFile('/etc/fstab');
          const f2 = yield readFile('/etc/shells');
          console.log(f1.toString());
          console.log(f2.toString());
        };    
        //等价于    
    
        const asyncReadFile = async function () {
        const f1 = await readFile('/etc/fstab');
        const f2 = await readFile('/etc/shells');
        console.log(f1.toString());
        console.log(f2.toString());
      };
  2. async函数对 Generator 函数的改进,体现在以下四点
    1. 内置执行器:Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器
    2. async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
    3. co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
    4. async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作
  3. 语法
    1. 返回 Promise 对象
    • async函数返回一个 Promise 对象。
    • async函数内部return语句返回的值,会成为then方法回调函数的参数
      js
          async function f() {
            return 'hello world';
          }
          f().then(v => console.log(v))
          // "hello world"
    • async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到
      js
          async function f() {
            throw new Error('出错了');
          }
          f().then(
            v => console.log(v),
            e => console.log(e)
          )
          // Error: 出错了
    1. Promise 对象的状态变化
    • async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
      js
    1. await 命令
    • 正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值
      js
          async function f() {
            // 等同于
            // return 123;
            return await 123;
          }
          f().then(v => console.log(v))
          // 123
    • await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中
    • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发
      js
          let foo = await getFoo();
          let bar = await getBar();   
          //上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。    
          
          
          // 写法一
          let [foo, bar] = await Promise.all([getFoo(), getBar()]);
          // 写法二
          let fooPromise = getFoo();
          let barPromise = getBar();
          let foo = await fooPromise;
          let bar = await barPromise;
    • await命令只能用在async函数之中,如果用在普通函数,就会报错。如果将forEach方法的参数改成async函数,也有问题

实例:按顺序完成异步操作 ​

  1. Promise 的写法如下
    js
        function logInOrder(urls) {
        // 远程读取所有URL
        const textPromises = urls.map(url => {
          return fetch(url).then(response => response.text());
        });
        // 按次序输出
        textPromises.reduce((chain, textPromise) => {
          return chain.then(() => textPromise)
            .then(text => console.log(text));
        }, Promise.resolve());
      }   
      //上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。这种写法不太直观,可读性比较差
  2. async 函数实现
    js
        async function logInOrder(urls) {
        for (const url of urls) {
          const response = await fetch(url);
          console.log(await response.text());
        }
      }   
      //上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求
  3. 并发发出远程请求
    js
        async function logInOrder(urls) {
        // 并发读取远程URL
        const textPromises = urls.map(async url => {
          const response = await fetch(url);
          return response.text();
        });
        // 按次序输出
        for (const textPromise of textPromises) {
          console.log(await textPromise);
        }
      }   
      //上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出