写在最前面:这是我写的一个关于JS的系列,一篇文章也不会光光只讲一个知识点,主要希望你不要觉得突然讲那么多东西,看的会很烦躁,因为我一般会将有关联的知识点串联起来,所以,学起来会让你感觉知识很连贯,我也想通过这个系列一边记录自己的学习,一边分享自己的学习,互勉!如果可以的话,也请给我点个赞,你的点赞也能让我更加努力地更新!
概览
-
食用时间: 15-20分钟
-
难度: 简单,别跑,看完再走
-
食用价值: 了解如何使用
Generator
函数的方式通过同步书写方式来解决Promise
回调地狱所带来的问题,以及通过学习Generator
函数的声明方式和调用方式,了解它在声明以及调用方式上存在的诸多不便,最终,了解如何通过基于它的语法糖,也就是async / await
,来更加优雅地解决Promise
回调地狱所带来的的问题。 -
铺垫知识:
① 一文搞懂JS系列(八)之轻松搞懂Promise, 学习
Promise
的基本概念,着重了解它所带来的 回调地狱 的痛点。
Generator
-
基本概念
Generator
函数是 ES6 提供的一种异步编程解决方案,主要原因是它可以在执行中被中断、然后等待一段时间再被我们唤醒。
-
函数定义
首先,先看名字, Generator 函数
,也不难看出来,这是一个函数,那么为了和普通函数加以区分,它的 function
后面,有一个特殊的 *
作为区分的标记。
此外,它内部有一个 yield
关键字,而它的主要作用,是将函数进行分割,你可以理解为将函数分为多个步骤,而每次函数的调用只执行一个步骤。这也就是上面所说的被中断,等下次继续唤醒。
function* helloWorldGenerator() {
yield 'result1';
yield 'result2';
return 'result3';
}
复制代码
-
异步封装
上面我们已经学习了一下 Generator
函数的基本书写方式,那么,接下来,我们就来进行一个Promise链式的封装
getData1().then(res1=>{
getData2().then(res2=>{
getData3().then(res3=>{
})
})
})
复制代码
上面是我们常用的一个链式调用,因为下一个接口的请求参数与第一个接口的返回信息有关联关系,所以一般要写 Promise
的嵌套,才能保证正确的代码执行顺序。经常使用 Promise
嵌套的同学也应该很了解它所带来的痛点,那就是会让代码看起来和面团一样,在后期维护的时候,带来一定的问题。
接下来,我们可以用新学到的 Generator
函数进行接口的封装,通过代码的分割,将面团切块,让它看起来和同步的代码一样,并且按功能模块进行划分,一个方法只做一件事情,更加利于后期维护。既然不需要写嵌套,基于设计模式的思想,我们可以先将三个接口调用进行代码抽离,为了效果的明显,我们在里面写上一个输出语句。
function getResult1(){
return getData1().then(res=>{
console.log('step1');
// ....
})
}
function getResult2(){
return getData1().then(res=>{
console.log('step2');
// ....
})
}
function getResult3(){
return getData1().then(res=>{
console.log('step3');
// ....
})
}
复制代码
然后,再来写 Generator
函数的封装,现在的代码,就看起来和同步代码一模一样了。当然,为了效果的明显,我们在一开始添加一句输出语句。
function* getGenerator(){
console.log('start');
yield getResult1();
yield getResult2();
yield getResult3();
}
复制代码
通过 yield
关键字,将 getGenerator
函数分成了三个部分,分别是调用三个接口。
-
函数调用
通过上面的学习,不难发现,这个 Generator
函数的声明和普通函数还是存在区别的,不过不仅如此,在函数的调用上也会有所不一样。在我们调用 Generator
函数之后,函数并不会执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,通过调用这个指针对象的 next()
方法就会从头开始执行,直到遇到第一个 yield
才会停止。等到下一次 next()
方法,又会从上一次停止的地方继续执行,再遇到下一个 yield
停止。
总结而言, yield
关键词将 Generator
函数切分成好几个步骤,而 next()
方法进行调用 Generator
函数,每次按执行顺序只执行一个步骤
那么,按照我们上面所说的,我们先来生成一个指针对象,发现函数并不执行,因为控制台毫无输出。
let hw = getGenerator(); // 仅生成指针对象,并不执行
复制代码
① 接下来,调用第一次 next()
方法,也就是说,他会从头开始执行,先输出 start ,然后执行第一个 yield
,也就是 getResult1()
,输出 step1 ,然后停止。
hw.next(); // 先输出 start ,然后输出 step1
复制代码
② 接下来,第二次调用 next()
方法,会从上次的 getResult1
之后开始执行,执行完第二个 yield
之后停止,也就是执行 getResult2
,输出 step2
hw.next(); // 输出 step2
复制代码
③ 第三次同理,输出 step3。
hw.next(); // 输出 step3
复制代码
完成整个 Generator
函数的执行。
-
自动迭代器
当然,上面的手动 next()
可能大家会觉得很繁琐,我定义一个 Generator
函数已经很累了,还要我先生成指针对象,然后一步一步地进行 next()
调用,真的很累,那么,我们现在来写一个自动的迭代器,让代码来替我们逐步执行 next()
方法。
function runGenerator(gen) {
var it = gen(), ret;
// 创造一个立即执行的递归函数
(function iterate(val){
ret = it.next(val);
if (!ret.done) {
// 如果能拿到一个 promise 实例
if ("then" in ret.value) {
// 就在它的 then 方法里递归调用 iterate
ret.value.then( iterate );
}
}
})();
}
复制代码
下面,我们再进行函数的调用,可以发现函数会自动跑完
runGenerator(getGenerator); // start step1 step2 step3
复制代码
能使用这个自动迭代器的原理就在于,Generator
函数在执行过程中有一个 done
标识,当它为 true
时,也就是执行完的时候。
如果想了解关于更多 Generator
函数的知识,可以参考阮一峰的教程
async / await
说了那么多,虽然 Promise
嵌套存在代码冗杂,后期维护不方便,以及代码看起来给人不够清晰的感觉,但是通过 Generator
函数的封装以后,代码是清晰了,而且结构分片,是相比于 Promise
嵌套更加利于维护,但是繁琐的函数声明,以及还要特意弄个迭代器进行函数的调用,利于维护的同时,不免让代码也更加繁琐了。
那么,接下来就是 async / await
荣耀登场的时候了。我们用 async await
来将 Generator
函数进行替代
async function hw(){
console.log('start');
await getResult1();
await getResult2();
await getResult3();
}
复制代码
可以看出,和 Generator
函数的声明方式差不多,只不过 *
替换成了 async
, yield
替换成了 await
。
async
的作用等同于 *
,将它与普通函数进行区分,告诉大家,这可不是个普通函数,是一个异步函数。
而 await
关键字相当于告诉大家,我现在要执行异步操作了,后面的代码给我等一等,我执行完了以后,再轮到你们。
其实 async / await
不仅声明方式和 Generator
函数差不多,他们还是有一定关系的,因为, async / await
就是基于 Generator
函数的语法糖。
当然,这可不仅仅是声明方式的语法糖,在函数调用上,依旧有着超越 Generator
函数的压倒性优势,只需要和简单函数一样的调用方式,再也不需要麻烦的迭代器来运行了。
hw(); // start step1 step2 step3
复制代码
这也就是目前解决回调地狱最优雅的解决方案,不仅如此,它还支持 try / catch
的错误捕捉方式。当然, Generator
函数也支持。
不过,由于现在接口调用都使用 axios
,接口调用上的错误捕捉一般直接用 axios
拦截器中解决。