【JS】Iterator接口、Generator函数、async/await函数初探

367 阅读5分钟

参考

Iterator 接口(遍历器接口)

ArrayString都用于表示集合,而后者专门用于表示字符集合。对于Array,我们可以通过Array.prototype.forEachArray.prototype.map来对其进行遍历;但对于String,JavaScript 并没有提供其专属的遍历方法。

// 遍历Array
const arr = ["a", "b", "c"];
arr.forEach((val) => {});
arr.map((val) => {});

// 遍历String(曲线救国)
const str = "abc";
Array.prototype.forEach.call(str, (val) => {});
Array.prototype.map.call(str, (val) => {});

// 当然,Array和String都可以通过for循环来进行遍历
for (let i = 0; i < arr.length; i++) {}
for (let i = 0; i < str.length; i++) {}

在 ES6 中粉墨登场的SetMap,也用于表示集合。试想,如果能为这类表示集合的数据结构提供一种统一的遍历方法,那该有多棒。于是,Iterator应运而生了。Iterator为不同的数据结构提供了统一的访问机制,以使数据可通过for...of被遍历。在 ES6 中,ArrayStringSetMap都原生具备Iterator接口。

// Array原生具备Iterator接口
// 意味着可以通过for...of来遍历它
const arr = ["h", "e", "l", "l", "o"];
for (let val of arr) {
  console.log(val);
}
// 这等价于下面的forEach
arr.forEach((val) => {
  console.log(val);
});

// String亦原生具备Iterator接口
const str = "hello";
for (let val of str) {
  console.log(val);
}

// Set和Map同理

Object原生不具备Iterator接口,然而,我们常常有对其进行遍历的需要。这时,我们可以手动为其部署Iterator接口,以使其能通过for...of被遍历。在部署Iterator接口之前,我们先来看看在for...of遍历过程中,实际发生了什么。

// 一段for...of遍历
const arr = ["h", "e", "l", "l", "o"];
for (let val of arr) {
  console.log(val);
}
// 实际执行流程:
// 1.调用arr的[Symbol.iterator]方法,该方法返回arr的iterator
// 2.不断调用iterator的next方法,该方法返回一个对象
// 3.1 返回的对象的value值会被传给val
// 3.2 当返回的对象的done值为false时,执行才会终止

// 通过以上,我们可以知晓以下几点:
// 1.iterator接口被部署在数据的[Symbol.iterator]属性上
// 2.iterator是一个对象,其有一个next方法
// 3.该next方法会返回一个包含value和done属性的对象

// 可能有人会问,arr的[Symbol.iterator]方法是哪来的
// 答案是,arr的[Symbol.iterator]方法是在JS内部被实现的
// 所以我们才说,Array原生具备Iterator接口

据此,我们试着为Object部署Iterator接口。

// 1.首先,iterator是一个对象
// 其次,其有一个next方法
const iterator = {
  next() {},
};

// 2.next方法返回的是一个对象
// 该对象有value和done这两个属性
const iterator = {
  next() {
    return {
      value: "someValue",
      done: "hasDone",
    };
  },
};

// 3.value和done的值从哪来?
// 我们声明一个value,并改写next方法
let value = 1;
const iterator = {
  next() {
      return value <= 5
        ? { value: value++, done: false }
        : { value: undefined, done: true };
      };
};

// 4.把iterator部署到对象上
const obj = {
  [Symbol.iterator]() {
    return iterator;
  },
};

// 5.尝试使用for...of遍历该对象
// 发现依次输出了1、2、3、4、5,遍历成功
// 这说明iterator被成功部署到obj上了!
for (let val of obj) {
  console.log(val);
}

上面例子中的value是全局变量,在obj外部。如果想要遍历obj内部的元素,也很简单。

// 假设我们想要以"element_+序号"的顺序来遍历obj
const obj = {
  length: 3,
  element_1: "A",
  element_2: "B",
  element_3: "C",
  [Symbol.iterator]() {
    let index = 1;
    return {
      // 使用箭头函数,以使this指向obj
      next: () => {
        return index <= this.length
          ? {
              value: this[`element_${index++}`],
              done: false,
            }
          : {
              value: undefined,
              done: true,
            };
      },
    };
  },
};

// 依次输出A、B、C
for (let val of obj) {
  console.log(val);
}

Generator 函数(生成器函数)

Generator函数是一个用于生成Iterator的函数。比如上一节中的obj[Symbol.iterator],以及下面例子中的generator,都是Generator函数。

// 声明generator
function generator() {
  let index = 0;
  return {
    next() {
      return index < 3
        ? { value: index++, done: false }
        : { value: undefined, done: true };
    },
  };
}

// 调用generator,返回iterator
const iterator = generator();
// 调用iterator
iterator.next(); // {value:0, done:false}
iterator.next(); // {value:1, done:false}
iterator.next(); // {value:2, done:false}
iterator.next(); // {value:undefined, done:true}

Generator函数有另一种表达形式——星号函数。

// 声明generator
function* generator() {
  // 执行generator(),到这暂停
  console.log("before 0");
  yield 0;
  // 第1次执行iterator.next(),到这暂停
  console.log("after 0");
  console.log("before 1");
  yield 1;
  // 第2次执行iterator.next(),到这暂停
  console.log("after 1");
  console.log("before 2");
  yield 2;
  // 第3次执行iterator.next(),到这暂停
  console.log("after 2");
  // 函数末尾有一句隐式的 yield undefined;
  // 第4次执行iterator.next(),到这暂停
}

// 调用generator,返回iterator
const iterator = generator(); // 并不会立即输出'before 0'
// 调用iterator
iterator.next(); // 'before 0' {value:0, done:false}
iterator.next(); // 'after 0' 'before 1' {value:1, done:false}
iterator.next(); // 'after 1' 'before 2' {value:2, done:false}
iterator.next(); // 'after 2' {value:undefined, done:true}

不难看出,每次调用iterator.next,都会开始/继续执行generator中的语句,直到碰到yield语句才会暂停执行。同时,会返回一个value值为yield身后的值、表示当前状态的对象。直到generator中的代码全部执行完毕。

此外,iterator.next支持传参,参数会传给上一个yield表达式,作为其返回值;不传参时,其返回值默认为undefined(要注意的是,yield身后的值为当前状态对象的value值,而非yield表达式的返回值)

function* generator() {
  // 第1次执行iterator.next()
  // 到"const x = yield 1"右边这部分,即到"yield 1"这里暂停
  // 并返回状态对象:{value:1, done:false}
  // 第2次执行iterator.next(‘hello’)
  // 会把"hello"传给"yield 1",作为"yield 1"的返回值
  // 并执行左边剩下的这部分,即执行"const x = 'hello'"
  const x = yield 1;
  // 第2次执行中,当"yield x"这部分执行完时暂停
  // 同理,会返回状态对象:{value:'hello', done:false}
  yield x;
}
const iterator = generator();
iterator.next(); // {value:1, done:false}
iterator.next("hello"); // {value:'hello', done:false}

async/await 函数

假设ajax()为一异步操作。

最开始,我们通过回调函数来进行异步操作。

ajax(params, function (err, dataA) {
  ajax(dataA, function (err, dataB) {
    ajax(dataB, function (err, dataC) {
      console.log(dataC);
    });
  });
});

后来有了Promise,其将函数的嵌套调用,改为函数的链式调用,解决了回调地狱问题。但是,利用Promise书写的异步操作,代码冗余,看起来仍然不够简洁、直观。

ajax(params)
  .then(function (dataA) {
    return ajax(dataA);
  })
  .then(function (dataB) {
    return ajax(dataB);
  })
  .then(function (dataC) {
    console.log(dataC);
  })
  .catch(function (err) {});

联想到星号函数中yield独有的暂停执行功能,我们猜想能否将异步操作书写成下面这样的形式,即以同步形式的代码来完成异步的操作,以符合我们书写和阅读代码时的直觉。

function* service(params) {
  const dataA = yield ajax(params);
  const dataB = yield ajax(dataA);
  const dataC = yield ajax(dataB);
  console.log(dataC);
}

然而,由于其中yield表达式的返回值始终为undefined,所以dataAdataBdataC的值也均为undefined,故实际上这段代码是无效的。所幸我们可以对Generator函数进行一些巧妙地包装,使如上代码起到预想的效果。

// 1.声明函数spawn,参数为generator
// 调用spawn后,会创建并启动iterator
function spawn(genFunc) {
  const iterator = genFunc();
  iterator.next(undefined);
}

// 2.在spawn内部写一个中间触发函数step
// 以实现iterator的自动执行和值的传递
function spawn(genFunc) {
  const iterator = genFunc();
  // 中间触发函数step
  function step(nextFunc) {
    // 调用nextFunc,即调用iterator.next并返回状态对象
    let next = nextFunc();
    // 如果状态对象的done为true,则终止执行
    if (next.done) return;
    // 否则继续调用step,即调用iterator.next
    // 且将状态对象的value作为参数传递
    step(function () {
      return iterator.next(next.value);
    });
  }
  // 第1次调用step
  step(function () {
    return iterator.next(undefined);
  });
}
// 如此一来,我们调用spawn,并传入generator
// 则会自动执行generator内的代码
// 并将每个yield身后的值传递给iterator.next
function* generator(params) {
  const dataA = yield ajax(params);
  const dataB = yield ajax(dataA);
  const dataC = yield ajax(dataB);
  console.log(dataC);
}
// 如果ajax是同步操作,那么dataC的值是有效的
// 但如果ajax是异步操作,那么传递的值会是undefined
// 目前为止,这段代码还是没起到作用

// 3.使用Promise包装step
// 以使实现异步操作下的值也能被传递
function step(nextFunc) {
  let next = nextFunc();
  if (next.done) return;
  // 当前操作resolve后,才会执行then并调用step
  Promise.resolve(next.value).then(function (val) {
    step(function () {
      return iterator.next(val);
    });
  });
}
// 到这里,spawn终于起作用了

// 4.为step加一些错误检测,以完善代码
function step(nextFunc) {
  let next;
  try {
    next = nextFunc();
  } catch (err) {
    throw err;
  }
  if (next.done) return;
  Promise.resolve(next.value).then(
    function (value) {
      step(function () {
        return iterator.next(value);
      });
    },
    function (err) {
      step(function () {
        // iterator.throw用法见参考链接
        return iterator.throw(err);
      });
    }
  );
}

// 5.可以将整个spawn函数也用Promise包装起来
// 以实现多个"星号函数"的链式调用
function spawn(genFunc) {
  return new Promise(function (resolve, reject) {
    const iterator = genFunc();
    function step(nextFunc) {
      let next;
      try {
        next = nextFunc();
      } catch (err) {
        reject(err);
      }
    }
    // 如果next.done为true,则resolve
    if (next.done) resolve(next.value);
    Promise.resolve(next.value).then(
      function (value) {
        step(function () {
          return iterator.next(value);
        });
      },
      function (err) {
        step(function () {
          return iterator.throw(err);
        });
      }
    );
    step(function () {
      return iterator.next(undefined);
    });
  });
}

// 6.为spawn增加一个参数,以实现传参功能
function spawn(genFunc, params) {
  // 中间省略...
  // 第1次调用iterator.next时,传参params
  step(function () {
    return iterator.next(params);
  });
}

让我们来试用一下刚完成的spwan

// 原星号函数
function* generator(params) {
  var dataA = yield ajax(params);
  var dataB = yield ajax(dataA);
  var dataC = yield ajax(dataB);
  console.log(dataC);
}
// 通过自执行函数启动generator
const params = {};
(function (params) {
  return spawn(generator, params);
})(params);

这样一来,写异步操作变得像写同步操作一样简单。而且实际上,我们不需要自己手写spwan函数,因为 ES6 内部替我们做了包装,使用asyncawait关键字即可实现和spwan一样的功能。比如上面一段代码,用async/await函数写起来是这样的。

async function operate(params) {
  var dataA = await ajax(params);
  var dataB = await ajax(dataA);
  var dataC = await ajax(dataB);
  console.log(dataC);
}
operate();

它看起来是不是很眼熟?没错,其形式和星号函数是相同的,只不过*变成了asyncyield变成了await。实际上async/await函数内部替我们做了转换。

// 转换前
async function operate(params) {
  // codes...
}

// 转换后
function operate(params) {
  return spawn(function* (params) {
    // codes...(await变成了*)
  });
}