一文搞懂JS系列(九)之更优雅的异步解决方案

写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞

合集地址:一文搞懂JS系列专题

概览

  • 食用时间: 15-20分钟

  • 难度: 简单,别跑,看完再走

  • 食用价值: 了解除了 Promise ,如何使用 Generator 函数的方式来解决异步操作,以及通过学习 Generator 函数的声明方式和调用方式,了解它在声明以及调用方式上存在的诸多不便,最终,了解如何通过基于它的语法糖,也就是 async / await ,来更加优雅地解决 Promise 多重调用所带来的的问题。

  • 铺垫知识:

    一文搞懂JS系列(八)之轻松搞懂Promise, 学习 Promise 的基本概念。

Promise 异步封装

先来看一个 Promise 嵌套的例子,我们要依次执行三个接口, getData1() , getData2() 以及 getData3()

getData1().then(res1=>{
  getData2().then(res2=>{
    getData3().then(res3=>{
      
    })
  })
})

这样子的写法很容易造成层层嵌套,后期造成维护困难,代码像面团一样揉在一起的感觉。

Promise 还有一种方式叫做链式调用,它的原理主要是 .then() 回调返回的也是一个 Promise ,所以才可以一直 .then() ,代码如下:

getData1().then(res=>{
  console.log(res)  // data1
  return getData2();
}).then(res=>{
  console.log(res) // data2
  return getData3();
}).then(res=>{
  console.log(res) // data3
})

链式回调的写法就比嵌套的写法来的清晰多了,毕竟,本来 Promise 的出现就是为了解决回调地狱的。下面让我们来看看另一种解决方案,那就是 Generator 函数。

Generator

  • 基本概念

Generator 函数是 ES6 提供的一种异步编程解决方案,主要原因是它可以在执行中被中断、然后等待一段时间再被我们唤醒。

  • 函数定义

首先,先看名字, Generator 函数 ,也不难看出来,这是一个函数,那么为了和普通函数加以区分,它的 function 后面,有一个特殊的 * 作为区分的标记。

此外,它内部有一个 yield 关键字,而它的主要作用,是将函数进行分割,你可以理解为将函数分为多个步骤,而每次函数的调用只执行一个步骤。这也就是上面所说的被中断,等下次继续唤醒

function* helloWorldGenerator() {
  yield 'result1';
  yield 'result2';
  return 'result3';
}
  • 异步封装

接下来,我们可以用新学到的 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

说了那么多,我们会发现Generator 函数的封装以后,代码是清晰了,而且结构分片,但是繁琐的函数声明,以及还要特意弄个迭代器进行函数的调用,不免让代码也更加繁琐了。

如此大费周章地讲述 Generator 函数,是因为接下来我们要讲的 async / await 就完全没有这种烦恼,但是,这两者之间有一定的渊源。

那么,接下来就是 async / await 荣耀登场的时候了。我们用 async await 来将 Generator 函数进行替代

async function hw(){
  console.log('start');
  await getResult1();
  await getResult2();
  await getResult3();
}

可以看出,和 Generator 函数的声明方式差不多,只不过 * 替换成了 asyncyield 替换成了 await

async 的作用等同于 * ,将它与普通函数进行区分,告诉大家,这可不是个普通函数,是一个异步函数。

await 关键字相当于告诉大家,我现在要执行异步操作了,后面的代码给我等一等,我执行完了以后,再轮到你们。

其实 async / await 不仅声明方式和 Generator 函数差不多,他们还是有一定关系的,因为, async / await 就是基于 Generator 函数的语法糖。

当然,这可不仅仅是声明方式的语法糖,在函数调用上,依旧有着超越 Generator 函数的压倒性优势,只需要和简单函数一样的调用方式,再也不需要麻烦的迭代器来运行了。

hw();    // start step1 step2 step3

这也就是目前解决异步回调最优雅的解决方案,不仅如此,它还支持 try / catch 的错误捕捉方式。当然, Generator 函数也支持。

不过,由于现在接口调用都使用 axios ,接口调用上的错误捕捉一般直接用 axios 拦截器中解决。

系列目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351