从 Interator 讲到 Async/Await
2020/2/21 11:45:34
本文主要是介绍从 Interator 讲到 Async/Await,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
初识迭代器
Interator,即我们常说的迭代器。在许多编程语言中都有它的身影。而 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 呢?
有两个解释点:
- 为了让迭代器的工作方式更加优雅,我们使用生成器来做一层包装
- 为了解决 ES5 中 Promise 的多层
then
写法不直观带来的问题
来看看上面的 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的一个官方版的实现。
更多的关于 ES7 中 async/await 语法知识,不是本文的重点。了解来龙去脉才是笔者关心的问题。所以这个章节就此打住啦!
小结
这就是文章的主要内容了。我们从迭代器讲到了生成器,并且最终结合 Promise 引出了ES7 中 async/await 语法。希望有所帮助!
这篇关于从 Interator 讲到 Async/Await的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-01使用 SVN合并操作时,怎么解决冲突的情况?-icode9专业技术文章分享
- 2025-01-01告别Anaconda?试试这些替代品吧
- 2024-12-31自学记录鸿蒙API 13:实现人脸比对Core Vision Face Comparator
- 2024-12-31自学记录鸿蒙 API 13:骨骼点检测应用Core Vision Skeleton Detection
- 2024-12-31自学记录鸿蒙 API 13:实现人脸检测 Core Vision Face Detector
- 2024-12-31在C++中的双端队列是什么意思,跟消息队列有关系吗?-icode9专业技术文章分享
- 2024-12-31内存泄漏(Memory Leak)是什么,有哪些原因和优化办法?-icode9专业技术文章分享
- 2024-12-31计算机中的内存分配方式堆和栈有什么关系和特点?-icode9专业技术文章分享
- 2024-12-31QT布局器的具体使用原理和作用是什么?-icode9专业技术文章分享
- 2024-12-30用PydanticAI和Gemini 2.0构建Airflow的AI助手