在之前的这篇文章中,已经讲完了从异步操作到promise的诞生,但是promise也仅仅是做到了异步操作和结果的分离,当我们有多个异步操作,且结果前后有依赖的时候,不可避免的,就会出现一系列的.then方法。还是不大优雅。
最理想的情况是,我们能把异步操作写成同步的样子。当执行异步操作时,让代码卡在那里,等异步操作完成后再执行后续的代码。【注一】
这里,需要先了解迭代器和生成器。
一,可迭代对象和迭代器
Iterator是ES6提出来的迭代器。因为ES6开始数据结构新增了Set和Map,再加上已有的Array和Object,此外用户还可以自行组合它们来形成组合数据结构,复杂的数据结构导致循环遍历难度加大,为简化和统一循环方式,ES6就给出了迭代器(Iterator)这个接口来提供统一访问机制for..of
或者…展开符。
迭代意味着遍历的同时能记录结果。
它为各种不同的数据结构提供统一的访问机制。提供了一种按序访问集合内各个元素的方法,让我们可以方便地遍历集合内所有的元素。
1.1,可迭代对象和迭代器的区别
其实就是es6新增了迭代器的概念,所有满足迭代器协议的对象,都是可迭代对象。这里需要区分可迭代对象和迭代器的概念。
可迭代对象 | 迭代器 | |
---|---|---|
可使用方法 | for...of 和...展开符 | next()方法 |
来源 | array,str,set,map等具备[Symbol.iterator]方法,且该方法返回一个迭代器的对象 | 具备next方法,能够指针移动实现遍历可迭代对象的对象 |
举例 | array,str,set,map等 | array,str,set,map等的[Symbol.iterator]方法的返回 |
迭代器协议:
具备[Symbol.iterator]方法,且该方法会返回一个迭代器对象,该对象的特征是具备next方法,能够进行迭代。
1.2,迭代器的工作原理
创建一个指针对象,指向当前数据结构的起始位置
第一次调用next方法时,指针指向数据结构的第一个成员
接下来调用next方法,指针后移,直到指向最后一个成员
不断地调用next
重复获取过程,然后每次都返回一个结果。等到没有东西可返回了,就终止。因此next
的返回对象有两个属性done
和value
。done
表示是否结束了,value
表示当前迭代的结果。当done
为true
的时候,表示迭代已结束,这时候是没有返回结果的也就是没有value
这个属性。
//迭代器生成函数
function myIterator(arr) {
let index = 0
return {
next: function() {
let done = ( index >= arr.length );
let value = ! done ? arr[index++] : undefined;
return { value, done };
}
}
}
let myIter = myIterator([1,2,3]);
console.log(myIter.next());//{ value: 1, done: false }
console.log(myIter.next());//{ value: 2, done: false }
console.log(myIter.next());//{ value: 3, done: false }
console.log(myIter.next());//{ value: undefined, done: true }
1.3,Array等数据结构的默认迭代器
ES6中原生的可迭代对象迭有Array、Set、Map和String,for..of
能够遍历它们是因为它们原型对象上具有Symbol.iterator
属性,该属性指向该数据结构的默认迭代器方法,**当使用for...of..
迭代可迭代对象时,js引擎就会调用其Symbol.iterator
方法,从而返回相应的默认迭代器。然后执行完其中的next方法。**举例:
var arr = [1, 2, 3, 4, 5]; //数组是一个迭代器
// 使用for..of时,,js引擎就会调用其`Symbol.iterator`方法,从而返回相应数据的默认迭代器
for(var v of arr){
console.log(v); // 12345
}
那么既然它的原型对象上有Symbol.iterator
方法,且返回的是对应的默认迭代器,我们就可以利用它生成对应的迭代器,然后使用next()方法访问值:
var arr = [1, 2, 3, 4, 5]; //数组是一个迭代器
//arr[Symbol.iterator]()会返回arr的默认迭代器,于是就可以使用next
var it = arr[Symbol.iterator]();
console.log(it.next())//{ value: 1, done: false }
console.log(it.next())//{ value: 2, done: false }
console.log(it.next())//{ value: 3, done: false }
console.log(...it)//4 5,只打印出剩余的没有被迭代的,所以...应该也是利用的迭代器的next
console.log(it.next())//{ value: undefined, done: true }//剩余的两个值被...迭代过了,于是这里就结束了
1.2,手写一个可迭代对象
也就是说,一个数据结构只要有Symbol.iterator
方法且Symbol.iterator
方法返回具备next方法,就可以认为它是是可迭代的(iterable)对象。
也就是说,需要满足两个条件:
1,该对象具备Symbol.iterator方法
2,Symbol.iterator方法返回一个对象,该对象具备next方法(迭代器)。
// 实现一个可迭代对象
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
}
}
};
// 遍历可迭代对象
for (const item of myIterable) {
console.log(item);
// 输出 1 2 3
}
// 通过迭代器对象遍历可迭代对象
const iterator = myIterable[Symbol.iterator]();
console.log(iterator.next()); // {done: false, value: 1}
console.log(iterator.next()); // {done: false, value: 2}
console.log(iterator.next()); // {done: false, value: 3}
console.log(iterator.next()); // {done: true, value: undefined}
能够使用for...of
遍历和...
扩展符展开。当使用这两种方法时,js引擎其实是调用这个可迭代对象的[Symbol.iterator]方法,从而得到一个迭代器,然后执行这个迭代器的next方法,从而取到其中的值。
但是如果想要使用next方法手动遍历,就需要const myIterable2=createIterator(obj)[Symbol.iterator]()
,手动执行这个可迭代对象的[Symbol.iterator]方法,从而得到迭代器(具备next方法的对象)。
1.3,将不可迭代的object处理成可迭代
object类型之所以不能迭代,就是因为它的原型对象上没有[Symbol.iterator]属性,想想看,一个对象的属性间并没有严格的顺序要求,自然不要求能迭代。
// 自定义一个可迭代对象
function createIterator(obj){
return { // 返回一个迭代器对象
//模仿原生迭代器添加Symbol.iterator方法
[Symbol.iterator]: function () {
const keys=Object.keys(obj)
let i=0
return {
next:function () { //迭代器对象一定有next()方法
const done = ( i >= keys.length );
const key=keys[i]
const value = !done ? obj[key] : undefined;
i++
return { //next()方法返回结果对象
value: value,
done: done
}
}
}
}
}
}
const obj={
name:'名字',
age:18
}
const myIterable=createIterator(obj)
// 使用 for-of 循环遍历可迭代对象
for(var v of myIterable){
console.log(v)
//名字
//18
}
console.log([...myIterable])//['名字',18]
const myIterable2=createIterator(obj)[Symbol.iterator]()
console.log(myIterable2.next())//{ value: '名字', done: false }
console.log(myIterable2.next())//{ value: 18, done: false }
console.log(myIterable2.next())//{ value: undefined, done: true }
二,生成器Generator
它存在的最大作用就是帮助我们快速生成可迭代对象/生成器
JavaScript 中的生成器(Generator)是一种特殊的函数,可以用来定义在运行时生成一系列值的迭代器。Generator 函数以 function*
开头,内部使用 yield
关键字来指定生成器迭代过程中产生的每个值,每次遇到 yield
关键字时暂停函数的执行,可以通过调用迭代器上的 next
方法来恢复函数的执行。
2.1,创建一个生成器
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iter = myGenerator(); // 创建迭代器
for(var v of iter){
console.log(v)//1,2,3
}
const iter2 = myGenerator(); // 创建迭代器
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2.next()); // { value: 2, done: false }
console.log(iter2.next()); // { value: 3, done: false }
console.log(iter2.next()); // { value: undefined, done: true }
console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象
生成器函数返回的,既是一个可迭代对象(可以使用for...of
),又是一个迭代器(可以直接使用next()
)。
每次yield就会生成一个断点。每次next就执行到这个断点过。
2.2,既是可迭代对象,又是迭代器
类似于这样,Symbol.iterator
是返回自身。自身上既有Symbol.iterator
又有next,从而可以返回既是可迭代对象,又是迭代器。
function myGenerator(arr) {
let i=0
return {
next(){
const done = i >= arr.length;
const value = !done ? arr[i++] : undefined;
return { //next()方法返回结果对象
value: value,
done: done
}
},
[Symbol.iterator]() {
return this
}
}
}
const iter = myGenerator([1,2,3,4,5,6]); // 创建迭代器
for(var v of iter){
console.log(v)//1,2,3,4,5,6
}
console.log(iter === iter[Symbol.iterator]()); // true,对象本身是可迭代对象
const iter2 = myGenerator([1,2,3,4,5,6,7]); // 创建迭代器
console.log(iter2.next()); // { value: 1, done: false }
console.log(iter2.next()); // { value: 2, done: false }
console.log(iter2.next()); // { value: 3, done: false }
console.log(iter2.next()); // { value: 4, done: false }
生成器生成的对象就有这样的特征。
2.3,next传递参数
yield 表达式本身没有返回值,或者说总是返回 undefined 。 next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。【注意这里是上一个】,也就是说,next在第一个yield处停住,第二次的next入参才是这第一个yield的返回值。
也就是说第一个next带入参没意义,因为它没有上一个断点yield。
function* foo(x) {
let y = x * (yield)
return y
console.log("111")//后续的代码不会被执行
yield 2
}
const it = foo(6)
it.next()//执行到yield过,x*这段语句还未执行
let res = it.next(7)//next传入7作为上一个yield的返回,代码接着执行,计算后赋值给y,然后return结束
console.log(res) // { value: 42, done: true }
it.next(8)
return
会强制生成器进入关闭状态,提供给 return
方法的值,就是终止迭代器对象的值,也就是说此时返回的对象状态为true
,值为传入的值。
2.4,return方法提前终止生成器
和上文中一样,return方法也会强制生成器进入关闭状态,提供给 return
方法的值,就是终止迭代器对象的值,也就是说此时返回的对象状态为true
,值为传入的值。
function* foo() {
console.log("11")
yield 1
console.log("22")
yield 2
console.log("33")
yield 3
console.log("44")
yield 4
}
const it = foo()
console.log("next",it.next())//next { value: 1, done: false }
console.log("return",it.return("test"))//return { value: 'test', done: true }
2.5,throw抛出错误终止生成器
throw()
方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。
function* foo() {
console.log("11")
yield 1
console.log("22")
yield 2
console.log("33")
yield 3
console.log("44")
yield 4
}
const it = foo()
console.log("next",it.next())//next { value: 1, done: false }
console.log("return",it.throw(new Error('出错了!')))//Error: 出错了!
简单的理解就是,next()、throw()、return()方法,都是把yield以及后面换成传入的参数。
2.6,yield* 表达式委托迭代
yield*
允许我们在 Generator
函数中调用另一个 Generator
函数或可迭代对象。
当 Generator
函数执行到一个 yield*
表达式时,它会暂停执行,并且将执行权转移到另一个 Generator
函数或可迭代对象中。直到这个函数或对象迭代结束后,执行权才会返回到原 Generator
函数中。
这也叫委托迭代。通过这样的方式,能将多个生成器连接在一起。
function * anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function * generator(i) {
yield* anotherGenerator(i);
yield "最后一个"
}
var gen = generator(1);
console.log(gen.next().value)//2
console.log(gen.next().value)//3
console.log(gen.next().value)//4
console.log(gen.next().value)//最后一个
for (let value of generator(2)) {
console.log(value); // 输出 3,4,5,'最后一个'
}
三,Generator在异步编程的应用到await的诞生
3.1,作为异步编程的解决方案
当我们知道Generator和yield配合使用,能够暂停代码的执行,就应该能敏锐地意识到它可以用来解决异步编程,尽管Promise已经极大的提升了异步编程的可读性和可维护性,避免了回调地狱的产生。但在某些情况下,代码还是会变得非常复杂,特别是在需要同时处理多个异步操作,或者需要控制这些异步操作的顺序时。使用 Promise 还是需要写大量的 then 语句来处理异步操作的结果,这可能在一定程度上增加了代码的复杂性。
而Generator函数凭借其阻塞式调用机制使得异步代码可以写得像同步代码一样,举之前读取文件的例子:
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
let res1,res2,res3
readFile('test.txt')
.then((res)=>{
res1=res
return readFile('test1.txt')
})
.then((res)=>{
res2=res
return readFile('test2.txt')
})
.then((res)=>{
res3=res
console.log("结果",res1+res2+res3)
})
使用generator函数改写:
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
function* myGenerator() {
//先按照顺序读取三个文件
const txt1= yield readFile('./text1.txt');
const txt2=yield readFile('./text2.txt');
const txt3=yield readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
const generator = myGenerator();
generator.next().value//取得第一个promise对象
.then((result1)=>{
return generator.next(result1).value
})//取得第二个promise对象
.then((result2)=>{
return generator.next(result2).value
})//取得第三个promise对象
.then((result3)=>{
generator.next(result3)//取得最后一个文件的值,继续执行代码
})
如果只看:
function* myGenerator() {
//先按照顺序读取三个文件
const txt1= yield readFile('./text1.txt');
const txt2=yield readFile('./text2.txt');
const txt3=yield readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
是不是就和同步差不多了?
但是看起来底下的generator.next()还是有一堆的then,和之前使用promise差不多呀。
为了方便理解,我们把上面的代码改写成回调地狱的形式。
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
function* myGenerator() {
//先按照顺序读取三个文件
const txt1= yield readFile('./text1.txt');
const txt2=yield readFile('./text2.txt');
const txt3=yield readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
const generator = myGenerator();
generator.next().value.then((result1)=>{
generator.next(result1).value.then((result2)=>{
generator.next(result2).value.then((result3)=>{
generator.next(result3)
})
})
})
3.2 ,封装生成器的递归传值
注意到这一坨玩意儿其实是递归传值:
generator.next().value.then((result1)=>{
generator.next(result1).value.then((result2)=>{
generator.next(result2).value.then((result3)=>{
generator.next(result3)
})
})
})
那么,我们其实可以想办法把他封装一下,于是代码就变成了这个样子:
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
function* myGenerator() {
//先按照顺序读取三个文件
const txt1= yield readFile('./text1.txt');
const txt2=yield readFile('./text2.txt');
const txt3=yield readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
//封装函数
function co(generator){
const gen=generator();
function _next(val){
var p=gen.next(val);
if(p.done) return;
//这里用Promise.resolve是为了避免p.value不是promise,将之转化一下
Promise.resolve(p.value).then(res=>{
_next(res);
})
}
_next()
}
co(myGenerator)
到这一步其实已经差不多了,网上有复杂一丢丢的实现,但是目前这样的封装,已经足够我们理解它是如何将异步转化成看起来同步的写法。
四,async和await的诞生
假如说js引擎帮我们在代码执行时引入co函数,并且执行co(myGenerator)的话,那么对于我们写代码的人而言,我们是不是只需要写:
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
function* myGenerator() {
//先按照顺序读取三个文件
const txt1= yield readFile('./text1.txt');
const txt2=yield readFile('./text2.txt');
const txt3=yield readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
这不和async/await差不多了吗。
也就是说,可以这样子理解:
当我们写下asycn的时候,js引擎就把它当做一个生成器函数处理,并且帮助我们引入了co函数,并且做好递归传值的工作。
而await就等同于yield。
于是就产生了async和await,它才让我们写异步代码像同步一样丝滑。
const fs = require('fs')
function readFile(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName, (err,data) => {
resolve(data.toString())
})
})
}
async function test() {
//先按照顺序读取三个文件
const txt1= await readFile('./text1.txt');
const txt2= await readFile('./text2.txt');
const txt3= await readFile('./text3.txt');
//这里再用结果处理其他代码
console.log(txt1,txt2,txt3)//第一个文件 第二个文件 第三个文件
}
test()