迭代器和生成器

WARNING

大量 code 警告

迭代器

背景知识

  1. 什么是迭代?

从一个数据集合中按照一定的顺序,不断取出数据的过程

  1. 迭代和遍历的区别?

迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完 遍历强调的是要把整个数据依次全部取出

  1. 迭代器

对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象

  1. 迭代模式

一种设计模式,用于统一迭代过程,并规范了迭代器规格:

  • 迭代器应该具有得到下一个数据的能力
  • 迭代器应该具有判断是否还有后续数据的能力

JS中的迭代器

JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:

1{value: 值, done: 是否迭代完成}

模板

1const obj = {
2  next() {
3    return {
4      value:xxx,
5      done:xx
6    }
7  }
8}

则认为该对象obj是一个迭代器 含义:

  • next方法:用于得到下一个数据
  • 返回的对象
    • value:下一个数据的值
    • done:boolean,是否迭代完成

演示1

1const arr = [1, 2, 3, 4, 5];
2//迭代数组arr
3const iterator = {
4  i: 0, //当前的数组下标
5  next() {
6    var result = {
7      value: arr[this.i],
8      done: this.i >= arr.length//下标越界了
9    }
10    this.i++;
11    return result;
12  }
13}
14//让迭代器不断的取出下一个数据,直到没有数据为止
15let data = iterator.next();
16while (!data.done) { //只要没有迭代完成,则取出数据
17  console.log(data.value)
18  //进行下一次迭代
19  data = iterator.next();
20}
21
22console.log("迭代完成")

演示2

1const arr1 = [1, 2, 3, 4, 5];
2const arr2 = [6, 7, 8, 9];
3// 迭代器创建函数  iterator creator
4function createIterator(arr) {
5  let i = 0;//当前的数组下标
6  return {
7    next() {
8      var result = {
9        value: arr[i],
10        done: i >= arr.length
11      }
12      i++;
13      return result;
14    }
15  }
16}
17const iter1 = createIterator(arr1);
18iter1.next()
19const iter2 = createIterator(arr2);
20iter2.next()

演示3:无限延伸功能

1// 依次得到斐波拉契数列前面n位的值
2// 1 1 2 3 5 8 13 .....
3//创建一个斐波拉契数列的迭代器
4function createFeiboIterator() {
5  let prev1 = 1,
6      prev2 = 1, //当前位置的前1位和前2位
7      n = 1; //当前是第几位
8  return {
9    next() {
10      let value;
11      if (n <= 2) {
12        value = 1;
13      } else {
14        value = prev1 + prev2;
15      }
16      const result = {
17        value,
18        done: false//这个数列无限延伸
19      };
20      prev2 = prev1;
21      prev1 = result.value;
22      n++;
23      return result;
24    }
25  }
26}
27const iterator = createFeiboIterator();
28iterator.next()

可迭代协议与for-of循环

可迭代协议

  • 迭代器(iterator):一个具有next方法的对象,next方法返回下一个数据并且能指示是否迭代完成
  • 迭代器创建函数(iterator creator):一个返回迭代器的函数

可迭代协议

ES6规定,如果一个对象具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable) 数组就是个可迭代对象

思考:如何知晓一个对象是否是可迭代的?答案:数组、类数组、自己写的对象都可以做成可迭代对象

思考:如何遍历一个可迭代对象?答案:for-of循环

for-of 循环

for-of 循环用于遍历可迭代对象,格式如下

1//迭代完成后循环结束
2for(const item in iterable){
3    //iterable:可迭代对象
4    //item:每次迭代得到的数据
5}

for-of循环的原理

调用对象的[Symbol.iterator]方法,得到一个迭代器。不断调用next方法,只有返回的done为false,则将返回的value传递给变量,然后进入循环体执行一次。

演示:数组就是个可迭代对象

1const arr = [5, 7, 2, 3, 6];
2// const iterator = arr[Symbol.iterator]();
3// let result = iterator.next();
4// while (!result.done) {
5//     const item = result.value; //取出数据
6//     console.log(item);
7//     //下一次迭代
8//     result = iterator.next();
9// }
10// 等价于
11for (const item of arr) {
12  console.log(item)
13}

演示3

1<div>1</div>
2<div>2</div>
3<div>3</div>
4<script>
5  const divs = document.querySelectorAll("div");
6  // const iterator = divs[Symbol.iterator]()
7  // let result = iterator.next();
8  // while (!result.done) {
9  //     const item = result.value; //取出数据
10  //     console.log(item);
11  //     //下一次迭代
12  //     result = iterator.next();
13  // }
14  // 等价于
15  for (const item of divs) {
16    console.log(item);
17  }
18</script>

演示:自定义可迭代对象

1//可迭代对象
2var obj = {
3  a: 1,
4  b: 2,
5  [Symbol.iterator]() {
6    const keys = Object.keys(this);//得到对象this里面所有属性名的一个数组
7    // console.log(keys)
8    let i = 0;
9    return {
10      next: () => {
11        const propName = keys[i];
12        const propValue = this[propName];
13        const result = {
14          value: {
15            propName,
16            propValue
17          },
18          done: i >= keys.length
19        }
20        i++;
21        return result;
22      }
23    }
24  }
25} 
26for (const item of obj) {
27  console.log(item); // {propName:"a", propValue:1}
28}

展开运算符与可迭代对象

展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。 演示

1var obj = {
2  a: 1,
3  b: 2,
4  [Symbol.iterator]() {
5    const keys = Object.keys(this);
6    let i = 0;
7    return {
8      next: () => {
9        const propName = keys[i];
10        const propValue = this[propName];
11        const result = {
12          value: {
13            propName,
14            propValue
15          },
16          done: i >= keys.length
17        }
18        i++;
19        return result;
20      }
21    }
22  }
23}
24
25const arr = [...obj];
26console.log(arr);
27
28function test(a, b) {
29  console.log(a, b)
30}
31
32test(...obj);

迭代器和可迭代协议,解决实际问题

解决副作用的 redux 中间件

  • redux-thunk:需要改动action,可接收action是一个函数

  • redux-promise:需要改动action,可接收action是一个promise对象,或action的payload是一个promise对象

以上两个中间件,会导致action或action创建函数不再纯净。

  • redux-saga将解决这样的问题,它不仅可以保持action、action创建函数、reducer的纯净,而且可以用模块化的方式解决副作用,并且功能非常强大。

redux-saga是建立在ES6的生成器基础上的,要熟练的使用saga,必须理解生成器。

要理解生成器,必须先理解迭代器和可迭代协议。

生成器

生成器 (Generator)

  1. 什么是生成器?

生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器(有next方法),同时又是一个可迭代对象(有知名符号),可以用于for of循环

1//伪代码
2
3var generator = new Generator();
4generator.next();//它具有next方法
5var iterator = generator[Symbol.iterator];//它也是一个可迭代对象
6for(const item of generator){
7    //由于它是一个可迭代对象,因此也可以使用for of循环
8}

注意:Generator构造函数,不提供给开发者使用,仅作为JS引擎内部使用

  1. 如何创建生成器

生成器的创建,必须使用生成器函数(Generator Function)

  1. 如何书写一个生成器函数呢?
1//这是一个生成器函数,该函数一定返回一个生成器
2function* method(){
3}
  1. 生成器函数内部是如何执行的?

第一次调用只是生成了生成器,并没有调用里面 生成器函数内部是为了给生成器的每次迭代提供的数据 每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置 yield是一个关键字,该关键字只能在生成器函数内部使用,表达“产生”一个迭代数据。

  1. 有哪些需要注意的细节?

1). 生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中 2). 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值 3). 第一次调用next方法时,传参没有任何意义 4). 在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

  1. 生成器的其他API
  • return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
1generator.return()//结束了,done为true
2generator.return(3)//value=3
  • throw方法:调用该方法,可以在生成器中产生一个错误
1generator.throw(new Error("sdbjjas"))

演示

1function* test() {
2  console.log("第1次运行")
3  yield 1;
4  console.log("第2次运行")
5  yield 2;
6  console.log("第3次运行")
7}
8
9const generator = test();

优化演示:数组的迭代

1const arr1 = [1, 2, 3, 4, 5];
2const arr2 = [6, 7, 8, 9];
3
4// 迭代器创建函数  iterator creator
5function* createIterator(arr) {
6  for (const item of arr) {
7    yield item;
8  }
9}
10
11const iter1 = createIterator(arr1);
12const iter2 = createIterator(arr2);

创建一个斐波拉契数列的迭代器

1function* createFeiboIterator() {
2  let prev1 = 1,
3      prev2 = 1, //当前位置的前1位和前2位
4      n = 1; //当前是第几位
5  while (true) {
6    if (n <= 2) {
7      yield 1;
8    } else {
9      const newValue = prev1 + prev2
10      yield newValue;
11      prev2 = prev1;
12      prev1 = newValue;
13    }
14    n++;
15  }
16}
17
18const iterator = createFeiboIterator();

演示:生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中

1function* test() {
2  console.log("第1次运行")
3  yield 1;
4  console.log("第2次运行")
5  yield 2;
6  console.log("第3次运行");
7  return 10;
8}
9
10const generator = test();

演示:调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值

1function* test() {
2  console.log("函数开始")
3  let info = yield 1;//这里info不是1,这里根据参数动态变化
4  console.log(info)
5  info = yield 2 + info;
6  console.log(info)
7}
8const generator = test();
9控制台
10generator.next()
11demo.js:2 函数开始
12{value: 1, done: false}
13generator.next(5)
14{value: 7, done: false}

演示:在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

1function* t1(){
2  yield "a"
3  yield "b"
4}
5function* test() {
6  两种错误的调用方式:
7  1. t1()// 调用生成器函数不会导致里面的代码执行
8  2. yield t1();// 将生成器对象给了yield执行
9  yield* t1();//相当于把t1里面代码直接copy过来了
10  yield 1;
11  yield 2;
12  yield 3;
13}
14const generator = test();

生成器

1// generator.next === generator[Symbol.iterator]().next()
2//下面的函数是一个生成器函数,用于创建生成器
3function* createGenerator() {
4  console.log("生成器函数的函数体 - 开始");
5  yield 1; // {value:1, done:false}
6  console.log("生成器函数的函数体 - 运行1");
7  yield 2;
8  console.log("生成器函数的函数体 - 运行2");
9  yield 3;
10  console.log("生成器函数的函数体 - 运行3");
11  return "结束";
12}
13
14var generator = createGenerator(); //调用后,一定得到一个生成器
1function* createArrayIterator(arr) {
2  for (let i = 0; i < arr.length; i++) {
3    const item = arr[i];
4    console.log(`第${i}次迭代`);
5    yield item;
6  }
7  console.log("函数结束");//迭代完成后运行
8}
9
10var generator = createArrayIterator([1, 2, 3, 4, 5, 6]);
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  yield 1; // {value:1, done:false}
5  return;// 后面不会运行了
6  console.log("生成器函数的函数体 - 运行1");
7  yield 2;
8  console.log("生成器函数的函数体 - 运行2");
9  yield 3;
10  console.log("生成器函数的函数体 - 运行3");
11  return "结束";
12}
13
14var generator = createGenerator(); //调用后,一定得到一个生成器
1function asyncGetData() {
2  return new Promise(resolve => {
3    setTimeout(() => {
4      resolve("成哥")
5    }, 2000);
6  })
7}
8
9function* task() {
10  console.log("开始获取数据....");
11  const data = yield asyncGetData()
12  console.log("获取到数据:", data);
13  const data2 = yield asyncGetData();
14  console.log("又获取到了数据:", data2);
15  const data3 = yield 1;
16  console.log("又获取到了数据:", data3)
17}
18
19/**
20         * 通用函数:运行一个生成器任务
21         */
22function run(generatorFunction) {
23  const generator = generatorFunction(); //得到一个生成器
24  next();
25
26  /**
27             * 封装了generator的next方法,进行下一次迭代
28             */
29  function next(nextValue) {
30    const result = generator.next(nextValue);
31    if (result.done) {
32      //迭代结束了
33      return; 
34    }
35    const value = result.value; //拿到迭代的数据
36    if (typeof value.then === "function") {
37      //迭代的数据是一个Promise
38      value.then(data => next(data));
39    } else {
40      next(result.value);
41    }
42  }
43}
44
45run(task);
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  let result = yield 1; //将1作为第一次的迭代的值,还没有完成赋值 绝不是把1赋值给result,result是外部给他的
5  console.log("生成器函数的函数体 - 运行1", result);
6  result = yield 2; //将2作为第二次迭代的值
7  console.log("生成器函数的函数体 - 运行2", result);
8  result = yield 3;
9  console.log("生成器函数的函数体 - 运行3", result);
10  return "结束";
11}
12
13var generator = createGenerator(); //调用后,一定得到一个生成器
14
15var result = generator.next(); //{value:1, done:false}
16while (!result.done) {
17  //有迭代的值
18  result = generator.next(result.value);//如果想把yield返回的值交给result
19}
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  try {
4    console.log("生成器函数的函数体 - 开始");
5    let result = yield 1; //将1作为第一次的迭代的值.
6    console.log("生成器函数的函数体 - 运行1", result);
7    result = yield 2; //将2作为第二次迭代的值
8    console.log("生成器函数的函数体 - 运行2", result);
9    result = yield 3;
10    console.log("生成器函数的函数体 - 运行3", result);
11    return "结束";
12  } catch (err) {
13    console.log("报错了");
14    yield "Abc";
15  }
16
17}
18
19var generator = createGenerator(); //调用后,一定得到一个生成器
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  let result = yield 1; //将1作为第一次的迭代的值
5  console.log("生成器函数的函数体 - 运行1", result);
6  result = yield 2; //将2作为第二次迭代的值
7  console.log("生成器函数的函数体 - 运行2", result);
8  result = yield 3;
9  console.log("生成器函数的函数体 - 运行3", result);
10  return "结束";
11
12}
13
14var generator = createGenerator(); //调用后,一定得到一个生成器
1function* g2() {
2  console.log("g2-开始");
3  let result = yield "g1";
4  console.log("g2-运行1");
5  result = yield "g2";
6  return 123;
7}
8//下面的函数是一个生成器函数,用于创建生成器
9function* createGenerator() {
10  console.log("生成器函数的函数体 - 开始");
11  let result = yield 1; //将1作为第一次的迭代的值
12  result = yield* g2(); //result为g2函数的返回值
13  console.log("生成器函数的函数体 - 运行1", result);
14  result = yield 2; //将2作为第二次迭代的值
15  console.log("生成器函数的函数体 - 运行2", result);
16  result = yield 3;
17  console.log("生成器函数的函数体 - 运行3", result);
18  return "结束";
19
20}
21
22var generator = createGenerator(); //调用后,一定得到一个生成器

生成器函数特点

  1. 调用生成器函数,会返回一个生成器,而不是执行函数体(因为,生成器函数的函数体执行,收到生成器控制)
  2. 每当调用了生成器的next方法,生成器的函数体会从上一次yield的位置(或开始位置)运行到下一个yield
    • yield关键字只能在生成器内部使用,不可以在普通函数内部使用
    • 它表示暂停,并返回一个当前迭代的数据
    • 如果没有下一个yield,到了函数结束,则生成器的next方法得到的结果中的done为true
  3. yield关键字后面的表达式返回的数据,会作为当前迭代的数据
  4. 生成器函数的返回值,会作为迭代结束时的value
    • 但是,如果在结束过后,仍然反复调用next,则value为undefined
  5. 生成器调用next的时候,可以传递参数,该参数会作为生成器函数体上一次yield表达式的值。
    • 生成器第一次调用next函数时,传递参数没有任何意义
  6. 生成器带有一个throw方法,该方法与next的效果相同,唯一的区别在于:
    • next方法传递的参数会被返回成一个正常值
    • throw方法传递的参数是一个错误对象,会导致生成器函数内部发生一个错误。
  7. 生成器带有一个return方法,该方法会直接结束生成器函数
  8. 若需要在生成器内部调用其他生成器,注意:如果直接调用,得到的是一个生成器,如果加入*号调用,则进入其生成器内部执行。如果是yield* 函数()调用生成器函数,则该函数的返回结果,为该表达式的结果
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  yield 1; // {value:1, done:false}
5  console.log("生成器函数的函数体 - 运行1");
6  yield 2;
7  console.log("生成器函数的函数体 - 运行2");
8  yield 3;
9  console.log("生成器函数的函数体 - 运行3");
10  return "结束";
11}
12
13var generator = createGenerator(); //调用后,一定得到一个生成器
1function* createArrayIterator(arr) {
2  for (let i = 0; i < arr.length; i++) {
3    const item = arr[i];
4    console.log(`第${i}次迭代`)
5    yield item;
6  }
7  console.log("函数结束")
8}
9
10var generator = createArrayIterator([1, 2, 3, 4, 5, 6])
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  yield 1; // {value:1, done:false}
5  return;
6  console.log("生成器函数的函数体 - 运行1");
7  yield 2;
8  console.log("生成器函数的函数体 - 运行2");
9  yield 3;
10  console.log("生成器函数的函数体 - 运行3");
11  return "结束";
12}
13
14var generator = createGenerator(); //调用后,一定得到一个生成器
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  let result = yield 1; //将1作为第一次的迭代的值
5  console.log("生成器函数的函数体 - 运行1", result);
6  result = yield 2; //将2作为第二次迭代的值
7  console.log("生成器函数的函数体 - 运行2", result);
8  result = yield 3;
9  console.log("生成器函数的函数体 - 运行3", result);
10  return "结束";
11}
12
13var generator = createGenerator(); //调用后,一定得到一个生成器
14
15var result = generator.next(); //{value:1, done:false}
16while (!result.done) {
17  //有迭代的值
18  result = generator.next(result.value);
19}
1function asyncGetData() {
2  return new Promise(resolve => {
3    setTimeout(() => {
4      resolve("成哥")
5    }, 2000);
6  })
7}
8
9function* task() {
10  console.log("开始获取数据....");
11  const data = yield asyncGetData()
12  console.log("获取到数据:", data);
13  const data2 = yield asyncGetData();
14  console.log("又获取到了数据:", data2);
15  const data3 = yield 1;
16  console.log("又获取到了数据:", data3)
17}
18
19/**
20         * 通用函数:运行一个生成器任务
21         */
22function run(generatorFunction) {
23  const generator = generatorFunction(); //得到一个生成器
24  next();
25
26  /**
27             * 封装了generator的next方法,进行下一次迭代
28             */
29  function next(nextValue) {
30    const result = generator.next(nextValue);
31    if (result.done) {
32      //迭代结束了
33      return; 
34    }
35    const value = result.value; //拿到迭代的数据
36    if (typeof value.then === "function") {
37      //迭代的数据是一个Promise
38      value.then(data => next(data));
39    } else {
40      next(result.value);
41    }
42  }
43}
44
45run(task);
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  try {
4    console.log("生成器函数的函数体 - 开始");
5    let result = yield 1; //将1作为第一次的迭代的值
6    console.log("生成器函数的函数体 - 运行1", result);
7    result = yield 2; //将2作为第二次迭代的值
8    console.log("生成器函数的函数体 - 运行2", result);
9    result = yield 3;
10    console.log("生成器函数的函数体 - 运行3", result);
11    return "结束";
12  } catch (err) {
13    console.log("报错了");
14    yield "Abc";
15  }
16}
17
18var generator = createGenerator(); //调用后,一定得到一个生成器
1//下面的函数是一个生成器函数,用于创建生成器
2function* createGenerator() {
3  console.log("生成器函数的函数体 - 开始");
4  let result = yield 1; //将1作为第一次的迭代的值
5  console.log("生成器函数的函数体 - 运行1", result);
6  result = yield 2; //将2作为第二次迭代的值
7  console.log("生成器函数的函数体 - 运行2", result);
8  result = yield 3;
9  console.log("生成器函数的函数体 - 运行3", result);
10  return "结束";
11}
12
13var generator = createGenerator(); //调用后,一定得到一个生成器
1function* g2() {
2  console.log("g2-开始");
3  let result = yield "g1";
4  console.log("g2-运行1");
5  result = yield "g2";
6  return 123;
7}
8//下面的函数是一个生成器函数,用于创建生成器
9function* createGenerator() {
10  console.log("生成器函数的函数体 - 开始");
11  let result = yield 1; //将1作为第一次的迭代的值
12  result = yield* g2(); //result为g2函数的返回值
13  console.log("生成器函数的函数体 - 运行1", result);
14  result = yield 2; //将2作为第二次迭代的值
15  console.log("生成器函数的函数体 - 运行2", result);
16  result = yield 3;
17  console.log("生成器函数的函数体 - 运行3", result);
18  return "结束";
19
20}
21
22var generator = createGenerator(); //调用后,一定得到一个生成器

生成器应用-异步任务控制

高仿await

1function* task() {
2  const d = yield 1;
3  console.log(d)
4  // //d : 1
5  const resp = yield fetch("http://101.132.72.36:5100/api/local")
6  const result = yield resp.json();
7  console.log(result);
8}
9run(task)
10function run(generatorFunc) {
11  const generator = generatorFunc();
12  let result = generator.next(); //启动任务(开始迭代), 得到迭代数据
13  handleResult();
14  //对result进行处理
15  function handleResult() {
16    if (result.done) {
17      return; //迭代完成,不处理
18    }
19    //迭代没有完成,分为两种情况
20    //1. 迭代的数据是一个Promise
21    //2. 迭代的数据是其他数据
22    if (typeof result.value.then === "function") {//promise里面有个then方法
23      //1. 迭代的数据是一个Promise
24      //等待Promise完成后,再进行下一次迭代
25      result.value.then(data => {
26        result = generator.next(data)
27        handleResult();
28      })
29    } else {
30      //2. 迭代的数据是其他数据,直接进行下一次迭代
31      result = generator.next(result.value)
32      handleResult();
33    }
34  }
35}