查看: 143|回复: 0

漫话JavaScript与异步·第三话——Generator:化异步为同步

[复制链接]

该用户从未签到

发表于 2019-11-4 10:38:42 | 显示全部楼层 |阅读模式
一、Promise并非完美

我在上一话中介绍了Promise,这种模式增强了变乱订阅机制,很好地办理了控制反转带来的信任问题、硬编码回调执行次序造成的“回调金字塔”问题,无疑大大提高了前端开辟体验。但有了Promise就能完美地办理异步问题了吗?并没有。
起首,Promise仍然需要通过then方法注册回调,虽然只有一层,但沿着Promise链一长串写下来,还是有些让人头晕。
更大的问题在于Promise的错误处置惩罚比力麻烦,由于Promise链中抛出的错误会一直传到链尾,但在链尾捕捉的错误却不一定清楚泉源。而且,链中抛出的错误会fail掉背面的整个Promise链,如果要在链中及时捕捉并处置惩罚错误,就需要给每个Promise注册一个错误处置惩罚回调。噢,又是一堆回调!
那么最理想的异步写法是怎样的呢?像同步语句那样直观地按次序执行,却又不会阻塞主线程,最好还能用try-catch直接捕捉抛出的错误。也就是说,“化异步为同步”!
痴心妄想?
我在第一话里提到,异步和同步之间的鸿沟在于:同步语句的执行时机是“现在”,而异步语句的执行时机在“未来”。为了填平鸿沟,如果一个异步操作要写成同步的形式,那么同步代码就必须有“等待”的本事,等到“未来”变成“现在”的那一刻,再继续执行背面的语句。
在不阻塞主线程的条件下,这可能吗?
听起来不太可能。幸好,Generator(生成器)为JS带来了这种超本事!

二、“暂停/继续”邪术

ES6引入的新特性中,Generator可能是其中最强大也最难理解的之一,即使看了阮一峰老师列举的大量示例代码,知道了它的全部API,也仍是不得要领,这是由于Generator的行为方式突破了我们所熟知的JS运行规则。可一旦把握了它,它就能赋予我们巨大的能量,极大地提升代码质量、开辟效率,以及FEer的幸福指数。
我们先来简朴回首一下,ES6之前的JS运行规则是怎样的呢?
1. JS是单线程执行,只有一个主线程
2. 宿主情况提供了一个变乱队列,随着变乱被触发,相应的回调函数被放入队列,排队等待执行
3. 函数内的代码从上到下次序执行;如果遇到函数调用,就先进入被调用的函数执行,待其返回后,用返回值替代函数调用语句,然后继续次序执行
对于一个FEer来说,日常开辟中理解到这个水平已经够用了,直到他尝试使用Generator……
  1. function* gen() {    let count = 0;    while(true) {        let msg = yield ++count;        console.log(msg);    }}let iter = gen();console.log(iter.next().value);// 1console.log(iter.next('magic').value);// 'magic'// 2
复制代码
等等,gen明明是个function,执行它时却不执行内里的代码,而是返回一个Iterator对象?代码执行到yield处竟然可以暂停?暂停以后,竟然可以规复继续执行?说好的单线程呢?别的,暂停/规复执行时,还可以传出/传入数据?怎么肥四?岂非ES6对JS做了什么魔改?
其实Generator并没有改变JS运行的基本规则,不过套用上面的naive JS观已经不敷以表明其实现逻辑了,是时候掏出长年在书架上吃灰的盘算机基础,重温那些考完试就忘掉的知识。
  
三、法力的秘密——栈与堆

(注:这个部门包含了大量的个人理解,未必准确,欢迎指教)
理解Generator的关键点在于理解函数执行时,内存里发生了什么
一个JS步伐的内存分为代码区、栈区、堆区和队列区,从MDN借图一张以阐明(图中没有画出代码区):

队列(Queue)就是FEer所熟知的变乱循环队列。
代码区保存着全部JS源代码被引擎编译成的机器码(以V8为例)。
栈(stack)保存着每个函数执行所需的上下文,一个栈元素被称为一个栈帧,一个栈帧对应一个函数。
对于引用类型的数据,在栈帧里只保存引用,而真正的数据存放在堆(Heap)里。堆与栈不同的是,栈内存由JS引擎自动管理,入栈时分配空间,出栈时采取,非常清楚明了;而堆是步伐员通过new操作符手动向操作体系申请的内存空间(当然,用字面量语法创建对象也算),何时该采取没那么明了,以是需要一套垃圾网络(GC)算法来专门做这件事。
扯了一堆预备知识,终于可以回到Generator的正题了:
平凡函数在被调用时,JS引擎会创建一个栈帧,在内里准备好局部变量函数参数临时值代码执行的位置(也就是说这个函数的第一行对应到代码区里的第几行机器码),在当前栈帧里设置好返回位置,然后将新帧压入栈顶。待函数执行竣事后,这个栈帧将被弹出栈然后烧毁,返回值会被传给上一个栈帧。
当执行到yield语句时,Generator的栈帧同样会被弹出栈外,但Generator在这里耍了个花招——它在堆里保存了栈帧的引用(或拷贝)!这样当iter.next方法被调用时,JS引擎便不会重新创建一个栈帧,而是把堆里的栈帧直接入栈。由于栈帧里保存了函数执行所需的全部上下文以及当前执行的位置,以是当这一切都被规复如初之时,就似乎步伐从原本暂停的地方继续向前执行了。
而由于每次yield和iter.next都对应一次出栈和入栈,以是可以直接利用已有的栈机制,实现值的传出和传入
这就是Generator邪术背后的秘密!

四、终极方案:Promise+Generator

Generator的这种特性对于异步来说,意味着什么呢?
意味着,我们终于获得了一种在不阻塞主线程的条件下实现“同步等待”的方法!
为便于阐明,先上一段直接使用回调的代码:
  1. let it = gen();  // 获得迭代器function request() {    ajax({        url: 'www.someurl.com',        onSuccess(res){            it.next(res);  // 规复Generator运行,同时向其中塞入异步返回的结果        }    });}function* gen() {    let response = yield request();    console.log(response.text);}it.next();  // 启动Generator
复制代码
留意let response = yield request()这行代码,是不是很有同步的感觉?就是这个Feel!
我们来仔细分析下这段代码是如何运行的。起首,最后一行it.next()使得Generator内部的代码从头开始执行,执行到yield语句时,暂停,此时可以把yield想象成return,Generator的栈帧需要被弹出,会先盘算yield右边的表达式,即执行request函数调用,以获得用于返回给上一级栈帧的值。当然request函数没有返回值,但它发送了一个异步ajax请求,并注册了一个onSuccess回调,表示在请求返回结果时,规复Generator的栈帧并继续运行代码,并把结果作为参数塞给Generator,准确地说是塞到yield所在的地方,这样response变量就获得了ajax的返回值。
可以看出,这里yield的功能设计得非常奇妙,似乎它可以“赋值”给response。
更妙的是,迭代器不但可以.next,还可以.throw,即把错误也抛入Generator,让后者来处置惩罚。也就是说,在Generator里使用try-catch语句捕捉异步错误,不再是梦!
先别急着激动,上面的代码还是too young too simple,要真正发挥Generator处置惩罚异步的威力,还得联合他的好兄弟——Promise一起上阵。代码如下:
  1. function request() {  // 此处的request返回的是一个Promise    return new Promise((resolve, reject) => {        ajax({            url: 'www.someurl.com',            onSuccess(res) {                resolve(res);            },            onFail(err) {                reject(err);            }         });    });}let it = gen();let p = it.next().value;  // p是yield返回的Promisep.then(res => it.next(res),    err => it.throw(err)  // 发生错误时,将错误抛入生成器);function* gen() {    try {        let response = yield request();        console.log(response.text);    } catch (error) {        console.log('Ooops, ', error.message);  // 可以捕捉Promise抛进来的错误!    }}
复制代码
这种写法完美联合了Promise和Generator的优点,可以说是FEer们梦寐以求的超级武器。
但聪明的你一定看得出来,这种写法套路非常固定,当Promise对象一多时,就需要写许多类似于p.then(res => ...., err => ...)这样的重复语句,以是人们为了偷懒,就把这种套路给提炼成了一个更加精简的语法,那就是传说中的async/await
  1. async funtion fetch() {    try {        let response = await request();  // request定义同上一端段示例代码        console.log(response.text);    } catch (error) {        console.log('Ooops, ', error.message);    }}
  2. fetch();
复制代码
这这这。。。就靠拢同步风格的水平而言,我觉得async/await已经到了至高无上的田地~
顺便说一句,著名Node.js框架Koa2正是要求中间件使用这种写法,足见其强大和可爱。
前端们,擦亮手中的新锐武器,准备欢迎来自异步的高难度挑战吧!

写在最后

距离发表第二话(Promise)已颠末去大半年了,原本设想的终章——第三话(Generator),却迟迟未能动笔,由于笔者一直没能弄懂Generator这个行为怪异的家伙究竟是如何存在于JS天下的,又如何成为“回调地狱”的终极办理方案?直到回头弥补了一些盘算机基础知识,才最终突破了理解上的障碍,把Generator的来龙去脉想清楚,从而敢应用到现实工作中。以是说,基础是很紧张的,这是永不过时的真理。前端发展非常敏捷,框架、工具日新月异,只有基础踏实,才能从容应对,任他风起云涌,我自稳坐钓鱼台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?用户注册

x

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表