从 iterator 讲到 async/await

Iterator,即我们常说的迭代器。在许多编程语言中都有它的身影。而 JavaScript 在 ES6 规范中正式定义了迭代器的标准化接口。

为什么需要迭代器

这个问题嘛?需要从设计模式讲起了。

我们知道设计模式中就有迭代器模式。迭代器模式要解决的问题是这样的:在遍历不同集合的时候(数组、Map、Set等),不同的集合有不同的遍历方式,每次都要针对集合的不同来重新编写代码。麻烦!懒惰的程序员们就想啊:是否可以有一种通用的遍历集合元素的方式呢?

于是,迭代器模式诞生了。它是实现对不同集合进行统一遍历操作的一种机制。也可以理解为是对集合遍历行为的一个抽象。

在 happyEnding 的世界里,只要你实现了迭代器接口,就相当于加入了迭代器大家庭了。而函数 next(),就是一个通行证。

ES6 中的迭代器

在 ES6 中,怎样才算可以被认定为是一个 可以提供迭代器的对象 呢? 两个必须满足的条件:

  • 一个实现了 Interable 接口的函数,该函数必须能够生成迭代器对象;
  • 迭代器对象中包含有 next() 方法,next() 函数的返回格式是:{ value | Object , done | Boolean }。

我们来看个栗子:

class Users {
	constructor(users){
		this.users = users;
	}

  // 实现 Interable 接口
	[Symbol.iterator]: function(){
		let i =0;
		let users = this.users;

    // 返回一个迭代器对象
		return {

      // 必须包含 next() 方法
      // 返回值格式符合规范:{ value | Object , done | Boolean }
			next(){
				if( i < users.length){
					return {done:false, value:users[i++]};
				}
				return {done : true};
			}
		}
	}
}

这个栗子是符合 ES6 的规范的。这里值得注意的是:我们使用了ES6 中预定义的特殊Symbol值 Symbol.iterator,任何 Object 都可以通过添加这个属性来自定义迭代器的实现。 关于 Symbol ,不是本文的重点哈。

我们来看看如何使用这个可以提供迭代器的对象:

const allUsers = new Users([
	{name: 'frank'},
	{name: 'niuniu'},
	{name: 'niuniu2'}
]);

// 验证方式1:ES6 的 for...of 它会主动调用 Symbol.iterator
for( let v of allUsers){
	console.log( v );
}
//output:
$:{ name: 'frank' }
	{ name: 'niuniu' }
	{ name: 'niuniu2' }

// 验证方式2:自己调用

// 主动返回一个迭代器对象
const allUsersIterator = allUsers[Symbol.iterator]();

console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
//output:
$:{ done: false, value: { name: 'frank' } }
	{ done: false, value: { name: 'niuniu' } }
	{ done: false, value: { name: 'niuniu2' } }
	{ done: true }

再啰嗦一下下:ES6 中的 Array、Map、Set 这些内置对象都已实现了 Symbol.iterator,简言之它们已经实现了迭代器属性。但,想要显式地使用对应对象的迭代器特性,还需要自己去调用:

let bar = [1,2,3,4];
//显式调用生成迭代器
let barIterator = bar[Symbol.iterator]();
//使用迭代器特性
console.log(barIterator.next().value); // output : 1

从迭代器到生成器

讲完了迭代器 Iterator,我们来讲讲生成器 Generator。为什么 JavaScript 中需要用到生成器 Generator 呢?

有两个解释点:
来看看上面的 User 对象在生成器下的表现方式:

class Users {
	constructor(users){
		this.users = users;
		this.length = users.length;
	}

	*getIterator(){
		for( let i=0; i< this.length; i++ ){
			yield this.users[i];
		}
	}
}

const allUsers = new Users([
	{name: 'frank'},
	{name: 'niuniu'},
	{name: 'niuniu2'}
]);

//验证
let allUsersIterator = allUsers.getIterator();
console.log(allUsersIterator.next()); //{ value: { name: 'frank' }, done: false }

是不是看起来简单了一点呢?如果仅仅是这个让迭代器看起来更加优雅,ES6 根本不需要生成器这种新的函数形式。生成器的语法更加复杂。而这些复杂性之所以存在,是为了应对更多的应用场景的。

且先来看生成器的语法:

声明

通过以下语法来生成生成器函数:

function *foo(){
	//...
}
foo();

尽管生成器使用了 * 来声明,但是执行起来还是和普通函数是一样:
也可以传参给它:

function *foo(x,y){
	//...
}

foo(24,2);

主要区别是:执行生成器,比如 foo(24,2) ,并不实际在生成器中执行代码。相反,它会产生一个迭代器控制这个生成器执行其代码。(这个执行生成器函数生成迭代器的过程,和文章前面显示调用生成迭代器的过程类似哦)

要让代码生效,需要调用迭代器方法 next() 。

function *foo(){
	//...
}
//生成迭代器
let fooIterator = foo();

//执行
fooIterator.next();

可能你会好奇,既然调用了迭代器方法 next() ,函数怎么知道返回什么呢?在生成器函数中没有 return 啊。别急,关键字 yield 就是在扮演这个角色的。

yield

yield 关键字在生成器中,用来表示 暂停点 。看下面代码:

function *foo(){
	let x=10;
	let y=20;
	yield;
	let z = 10 + 20;
}

在这个生成器中,首次运行前两行,遇到 yield 会暂停这个生成器。如果恢复的话,会从 yield 处执行。就这样,只要遇到 yield 就会暂停。生成器中 yield 可以出现任意多次,你甚至可以将它放在循环中。

yield 不仅仅是一个暂停点,它还是一个表达式。 yield 的右边,是暂停时候的返回值(就是迭代器被调用 next() 后的返回值)。而 yield 在语句的位置,还可以插入 next() 方法中的输入参数(替换掉 yield 及其右侧表达式):

function *foo(){
	let x=10;
	let y=20;
	let z = yield x+y;
	return x + y +z;
}
//生成迭代器
let fooIterator = foo();
//第一次执行迭代器的next(),遇到 yield 返回,返回值是 yield 右侧的运行结果
console.log(fooIterator.next().value); // 30
//第二次执行迭代器的next(100), yield 及其右侧表达式的位置会替换为参数 100
console.log(fooIterator.next(100).value); // 130

融合 Promise 来控制异步操作

ES5 的 Promise 中包含了异步操作,待操作完成时,会返回一个决议。但是它的写法 then() 会让代码在复杂情况下变得很难看,众多的 then 嵌套并没有比回调地狱好看多少。于是我们就想,是否可以通过生成器来更好地控制 Promise 的异步操作呢?

将 Promise 放到 yield 后面,然后迭代器侦听这个 promise 的决议(完成或拒绝),然后要么使用完成消息恢复生成器的允许(调用 next()),要么向生成器抛出一个带有拒绝原因的错误。

这是最为重要的一点: yield 一个 Promise,然后通过这个 Promise 来控制生成器的迭代过程 。

import 'axios';

//步骤一:定义生成器
function *main(){
    try {
        var text = yield myPromise();
        console.log("generator result :", text);
    }catch(err){
        console.error("generator err :",err);
    }
}
//步骤二:定义 promise 函数
function myPromise(){
    return axios.get("/api/info");
}

//步骤三:创建出迭代器
let mainIterator = main();

//步骤四:使用 promise 来控制迭代器的工作过程
let p = mainIterator.next().value;

p.then(
    function(res){
            let data = res.data;
        console.log(" resolved : ", data);
        //! promise 决议(完成)来控制迭代器
        mainIterator.next(data);
    },
    function(error){
        console.log(" rejected : ", error);
        //! promise 决议(拒绝)来控制迭代器
        mainIterator.throw(error);
    }
);


//output
$ resolved : {name : frank}
	generator result : {name : frank}

这样,我们了解到了在生成器当中如何使用 Promise。并且能够很好工作。只是,你会觉得,这个代码甚至比之前的 Promise 写法还要啰嗦。

假如有一个库,它封装好了所有与生成器、迭代器、Promise 结合的细节,你只需要简单的调用(只需要写上面代码的步骤一与步骤二),就能够将异步的写法转变为同步的写法。你会想要么?

ES7 的 async/await

上面的描述就是 ES7 中 async/await 语法的原理雏形。它的实现与考虑的情况远比我们上面的这个 demo 版本更加复杂。它的写法如下:

function myPromise(){
    return axios.get("/api/info");
}

async main(){
	try {
	    var text = await myPromise();
	    console.log(text);
	}catch(err){
	    console.log(err);
	}
}

如果你将和async/await 语法与生成器做一个对比,可以简单地将 async 类比为 * ,而将 await 类比为 yield 。它就是那个在生成器与迭代器中融合了 Promise的一个官方版的实现。

自动执行器

function* gen() {
  const res1 = yield Promise.resolve(1);
  const res2 = yield Promise.resolve(1 + res1);
  return res2;
}

/**
 * 自动执行器
 * @param {Function} fn 生成器函数
 */
function asyncFn(fn) {
  const iterator = fn();
  next();
  function next(data) {
    const { value, done } = iterator.next(data);
    console.log(value, done);
    if (!done) {
      value.then((val) => next(val));
    }
  }
}

asyncFn(gen);

这就是文章的主要内容了。我们从迭代器讲到了生成器,并且最终结合 Promise 引出了ES7 中 async/await 语法。

原文出处

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值