本篇文章会按迭代器 => 生成器 => 生成器+promise解决异步 => async、await这个顺序介绍以下几点
- 迭代器和生成器是什么
- 生成器解决异步的方法以及弊端
- 如何处理这个弊端
- async、await和generator的关系
ES6的迭代器和可迭代对象
什么是迭代器?
迭代器是一个对象,迭代器对象要求实现then方法,并且then方法的返回值是{ done:boolean , value:any } 的对象。让我们看一下迭代器的使用吧
let index = 0
const it = {
next(){
//当超出数组下标的时候,done是true,value是undefined
return index < names.length?
{value:names[index++],done:false}:{value:undefined,done:true}
}
}
console.log(it.next()); //{ done:false , value: 1}
console.log(it.next()); //{ done:false , value: 2}
console.log(it.next()); //{ done:false , value: 3}
console.log(it.next()); //{ done:true , value: undefined}
什么是可迭代对象?
可迭代对象是实现了Symbol.iterator方法的对象,调用这个方法会返回迭代器对象。可迭代对象又叫iterable对象,鼠标移入的时候可以看见,常见的可迭代对象有:Array,Map,Set。
可以看到,set构造方法中可以传入一个可迭代对象来创建set实例,所以我经常会用它来方便的去重数组
let arr = [1,2,3,1]
console.log([...new Set(arr)]) //[1,2,3]
- 可迭代对象有什么用?
再次说明,可迭代对象是实现了Symbol.iterator方法的对象,并且这个方法会返回迭代器。可迭代对象的应用场景有很多,例如
for...of
const arr = [1,2,3]
//普通对象是不能用for..of的
const obj = {name:'jzsp'}
for(const item of arr){
console.log(item)
}
展开语法
const arr = [1,2,3]
const obj = {name:'jzsp'}
console.log(...arr)
//普通对象是不能直接展开的,会报错
// console.log(...obj)
//注意,这个是对象中用展开语法,是es9新增的,本质上用的不是迭代器
console.log({...obj})
解构赋值
const arr = [1,2,3]
const obj = {name:'jzsp'}
const [v1,v2,v3] = arr
//注意,这个本质上用的不是迭代器,是es9新增语法
const { name } = obj
创建可迭代对象的时候,传入可迭代对象
let arr = [1,2,3,1]
console.log([...new Set(arr)]) //[1,2,3]
其他一些可以传入可迭代对象的API
自定义类实现可迭代的案例
//案例是这样的:这是一个教室类,里面有很多的学生,要求可以用for...of遍历所有的学生
class classromm{
constructor(address,name,students) {
this.address = address
this.name = name
this.students = students
}
entry(newStudent){
this.students.push(newStudent)
}
//在对象的原型链上添加[Symbol.iterator]方法
[Symbol.iterator](){
let index = 0
//返回的是一个迭代器对象,迭代器对象要求实现next方法,next方法返回的是{done,value}对象
return {
next:()=>{
//遍历所有的学生
return index < this.students.length ?
{done:false,value:this.students[index++]}:{done:true,value:undefined}
}
}
}
}
const room = new classromm('台州','九折水瓶教室',['九折水瓶1号','九折水瓶2号'])
//
for (const stu of room) {
console.log(stu)
}
Generator生成器
可能很多人会问了,明明标题是Generator,为什么前面要花那么多时间介绍迭代器呢?这是因为Generator生成器跟Iterator迭代器有异曲同工之妙。还记得迭代器对象是什么吗?
没错,Iterator是实现了next方法,并且next方法返回值是{done,value}的对象!Generator也是如此,它内部有next方法,返回值也是{done,value}对象。
生成器是ES6新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行。生成器是特殊的迭代器。
生成器函数
我们需要通过生成器函数来获取Generator对象,生成器函数是一个函数,但是与普通的函数比有区别
- 生成器函数需要在
function后面加* - 生成器函数内部可以通过
yield关键字来控制函数的执行流程 - 生成器函数的返回值是
generator生成器对象 - 通过调用生成器对象的next方法来执行函数。yield相当于将函数内部分段了,第i个next就执行第i-1段
让我们通过一个例子来看看生成器函数吧!
function* foo(){
console.log(1)
yield
console.log(2)
yield
console.log(3)
yield 'jzsp'
console.log(4)
return '我是返回值'
}
let generator = foo()
generator.next() // {value:undefined,done:false}
generator.next()
//因为生成器generator是特殊的迭代器,有next方法,返回值的格式跟迭代器一样
//value是yield关键字后面跟着的值(或者是return的值)
//done的话,如果当前next没有被yield关键字暂停执行,也就是说后面没有yield,那么done就为true
console.log(generator.next()) //{ value: 'jzsp' done: false }
//done为true的时候,也就是函数执行结束的时候,value的值是函数return的值
console.log(generator.next()) //{ value: '我是返回值', done: true }
next传参
第i次调用next时传的参数,会作为第i-1次yield表达式的返回值。
第i次调用next的返回值的value,是第i个yield表达式后面的值
第一段(第一个yield前)代码需要传值的话,可以在调用生成器函数的时候传入
function* foo(first){
console.log('123',first)
const n = yield first
console.log(n) //第二个参数
}
let gen = foo('第一个参数')
console.log(gen.next()) //{ value: '第一个参数', done: false }
gen.next("第二个参数")
生成器代替迭代器(yield*)
生成器代替迭代器的原理是,生成器用yield可以实现暂停效果,用next可以实现迭代效果,并且next返回对象中的done属性会根据yield的迭代情况自动判断,value属性就是yield关键字后的值
//传入一个数组,返回该数组的迭代器(虽然数组有迭代器)
function createArrayIterator(arr){
let index = 0
return {
next:function(){
return index < arr.length?
{value:arr[index++],done:false}:{value:undefined,done:true}
}
}
}
//代替写法
function* createArrayIterator(arr){
//第一种写法
for(const item of arr){
yield item
}
//第二种写法:yield*后面跟着的是可迭代对象,每次调用next的时候会迭代一次可迭代对象
yield* arr
}
生成器+Promise解决异步问题
花了那么大篇幅介绍了迭代器跟生成器(其实里面还有很多内容,但是主要是为了讲解处理异步),终于到正题了!让我们看一下Generator是怎么解决异步问题的吧!
假设我们现在有一个用Promise封装的模拟异步请求,如下
function request(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
//我们默认是成功的
resolve(url);
}, 3000);
});
}
现在想模拟的是,发送一个字符串给服务器,服务器将这个返回,我们再将这个返回的字符串拼接上aaa再发送,服务器再返回,我们再拼接上bbb再发送,服务器再返回。
使用Generator+promise可以解决这个问题,代码如下
function* getData(){
const res1 = yield request("111")
const res2 = yield request(res1+'aaa')
const res3 = yield request(res2+'bbb')
return res3
}
//手动执行(回调地狱)
//第一次next获取到第一个yield返回的promise,指定then回调,在回调中获取返回值
//第二次next传入第一个promise值,这个值会作为第一个yield的返回值res1
const gen = getData()
gen.next().value.then(res=>{
gen.next(res).value.then(res=>{
gen.next(res).value.then(res=>{
console.log(res)
})
})
})
这显然不是我们想要的,因为实际生活中我们很难处理很多请求的情况(回调地狱),并且还要自己写,那么我们可以实现一个自动执行next的函数来帮我们完成这件事。
//自动执行,传入生成器函数对象,在内部通过递归自动执行
function execGenerator(fn){
//获取生成器对象
const gen = fn()
function exec(v){
//先调用next执行函数,直到yield停止并且获取yield的返回值
const res = gen.next(v)
//判断是否迭代完成,如果完成的话直接返回
if(res.done){
return res.value
}
//没迭代完成的话,就取出返回对象中的value(promise),在then中取出promise的值,递归调用
res.value.then(res=>{
exec(res)
})
}
exec()
}
execGenerator(getData)
async、await
先来介绍一下async和await吧
async函数
- 在声明函数前加上async关键字就可以变成async函数。
- async函数的返回值一定是一个promise,这个返回的promise由async函数return的内容决定。
注意,加上async关键字不代表函数就变成异步的了,在函数内部正常写的话,跟普通函数没有太大区别
async function foo(){
console.log(2)
}
console.log(1)
foo()
console.log(3)
//输出顺序是123,说明foo并没有变成异步函数
await关键字
await关键字后面经常跟上一个promise,当这个promise状态没有改变(一直是pending的时候),下面的代码都不会执行。等到promise状态改变的时候,会将promise的值取出作为await关键字的返回值,然后可以继续执行后面的代码。(如果是普通值,就直接作为await的关键字的返回值)
function request(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
//我们默认是成功的
resolve(111)
},2000)
})
}
async function foo(){
const res = await request()
console.log('----------',res)
}
foo()
如果await后的promise结果是reject失败的,那么会将reject的值作为整个async函数foo返回的promise的reject的值,并且不会向下执行
function request(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(111) //失败的promise
},2000)
})
}
async function foo(){
const res = await request()
//不会向下执行
console.log(res)
}
foo().catch(err=>{
console.log(err)
})
await关键字是语法糖:在Generator中,yield后面是Promise的话,会在promise执行结束的时候将promise封装在next()返回的对象的value属性中,想执行下一步需要调用next方法;而await后面是promise的话,会在promise状态改变时取出promise的值作为await关键字的返回值
值得注意的是,await关键字后面的代码本质上是在当前await返回的Promise的then方法中执行的,也就是说,await后面的代码是放到微任务中执行的。需要注意的是,await后如果跟着的是常数,await关键字下面的代码本质上还是会被放到微任务中。
async、await解决异步
async、await这种解决异步的方案,其实就是Generator+Promise自动执行版本的语法糖哦~(看是不是跟上面Generator的例子很像呢)感谢ES7,感谢JavaScript!
async function getData(){
const res1 = await request("111")
const res2 = await request(res1+'aaa')
const res3 = await request(res2+'bbb')
return res3
}