> 文章列表 > 从promise到await

从promise到await

从promise到await

在之前的这篇文章中,已经讲完了从异步操作到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的返回对象有两个属性donevaluedone表示是否结束了,value表示当前迭代的结果。当donetrue的时候,表示迭代已结束,这时候是没有返回结果的也就是没有value这个属性。

//迭代器生成函数
function myIterator(arr) {let index = 0return {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=0return {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=0return {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 yconsole.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 1console.log("22")yield 2console.log("33")yield 3console.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 1console.log("22")yield 2console.log("33")yield 3console.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=resreturn readFile('test1.txt')
})
.then((res)=>{res2=resreturn readFile('test2.txt')
})
.then((res)=>{res3=resconsole.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。

从promise到await

于是就产生了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()