Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Generator:化异步为同步

Generator:化异步为同步

原创
作者头像
挖掘大数据
发布于 2018-01-15 11:35:36
发布于 2018-01-15 11:35:36
1.5K0
举报
文章被收录于专栏:挖掘大数据挖掘大数据

一、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……

function* gen() { let count = 0; while(true) { let msg = yield ++count; console.log(msg); } }

let iter = gen(); console.log(iter.next().value); // 1 console.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的这种特性对于异步来说,意味着什么呢? 意味着,我们终于获得了一种在不阻塞主线程的前提下实现“同步等待”的方法! 为便于说明,先上一段直接使用回调的代码:

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一起上阵。代码如下:

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返回的Promise p.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:

async funtion fetch() { try { let response = await request(); // request定义同上一端段示例代码 console.log(response.text); } catch (error) { console.log(‘Ooops, ‘, error.message); } }

fetch();

这这这。。。就靠拢同步风格的程度而言,我觉得async/await已经到了登峰造极的地步~ 顺便说一句,著名Node.js框架Koa2正是要求中间件使用这种写法,足见其强大和可爱。 前端们,擦亮手中的新锐武器,准备迎接来自异步的高难度挑战吧! 写在最后 距离发表第二话(Promise)已经过去大半年了,原本设想的终章——第三话(Generator),却迟迟未能动笔,因为笔者一直没能弄懂Generator这个行为怪异的家伙究竟是如何存在于JS世界的,又如何成为“回调地狱”的终极解决方案?直到回头弥补了一些计算机基础知识,才最终突破了理解上的障碍,把Generator的来龙去脉想清楚,从而敢应用到实际工作中。所以说,基础是很重要的,这是永不过时的真理。前端发展非常迅速,框架、工具日新月异,只有基础扎实,才能从容应对,任他风起云涌,我自稳坐钓鱼台。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
从Generator到Async function
为什么说Async function是从Promise,Generator一路走来的?
ayqy贾杰
2019/06/12
5480
从Generator入手读懂 co 模块源码(干货)
本文主要会讲 Generator 的运用和实现原理,然后我们会去读一下 co 模块的源码,最后还会提一下 async/await。
coder_koala
2020/09/08
6960
从Generator入手读懂 co 模块源码(干货)
异步遍历器
《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法返回的对象的结构是{value, done},其中value表示当前的数据的值,done是一个布尔值,表示遍历是否结束。
小小杰啊
2022/12/21
3250
Generator 函数的语法
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。 封装了多个内部状态。
小小杰啊
2022/12/21
8320
Generator函数
JavaScript是单线程的,异步编程对于 JavaScript语言非常重要。如果没有异步编程,根本没法用,得卡死不可。
木子星兮
2020/07/16
1.1K0
异步解决方案补充
接着昨天讲,四种异步解决方案前两种回调函数和promise昨天讲过了,今天只是补充说明另外两种解决方案。那讲之前肯定少不了我们要明确一下同步和异步的概念,这里咱找了一个更官方的解释来让我们更加的通俗易懂。
qiangzai
2022/09/16
4400
异步解决方案补充
【ECMAScript6】es6 要点(二)Promise | 自个写一个Promise | Generator | Async/Await
但是,我们不能无限制地调用next从Generator实例中获取值。否则最后会返回undefined。原因:Generator犹如一种序列,一旦序列中的值被消费,你就不能再次消费它。即,序列为空后,再次调用就会返回undefined!。
前端修罗场
2023/10/07
3750
【ECMAScript6】es6 要点(二)Promise | 自个写一个Promise | Generator | Async/Await
ES6中的Promise和Generator详解
ES6中除了上篇文章讲过的语法新特性和一些新的API之外,还有两个非常重要的新特性就是Promise和Generator,今天我们将会详细讲解一下这两个新特性。
程序那些事
2020/12/31
1.4K0
ES6读书笔记(三)
前段时间整理了ES6的读书笔记:《ES6读书笔记(一)》,《ES6读书笔记(二)》,现在为第三篇,本篇内容包括:
全栈程序员站长
2021/07/06
1.2K0
JS--异步的日常用法
这两个名词确实是很多人都常会混淆的知识点。其实混淆的原因可能只是两个名词在中文上的相似,在英文上来说完全是不同的单词。
江拥羡橙
2023/12/09
3652
JS--异步的日常用法
generator的作用_对服从与执行的理解
1. 在GeneratorFunction内,当遇到yield关键字的时候,先将执行上下文设置为yield之后的表达式进行执行,并且将该表达式返回值作为当前迭代的结果;
全栈程序员站长
2022/08/04
3860
js异步编程面试题你能答上来几道
在上一节中我们了解了常见的es6语法的一些知识点。这一章节我们将会学习异步编程这一块内容,鉴于异步编程是js中至关重要的内容,所以我们将会用三个章节来学习异步编程涉及到的重点和难点,同时这一块内容也是面试常考范围。
loveX001
2022/10/10
5500
js中异步方案比较完整版(callback,promise,generator,async)
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。)
IT人一直在路上
2019/09/18
2K0
js中异步方案比较完整版(callback,promise,generator,async)
JavaScript 异步编程指南 — 了解下 Generator 更好的掌握异步编程
Generator 是 ES6 对协程的实现,提供了一种异步编程的解决方案,和 Promise 一样都是线性的模式,相比 Promise 在复杂的业务场景下避免了 .then().then() 这样的代码冗余。
五月君
2021/07/15
6670
Promise、Generator、Async 合集
我们知道Promise与Async/await函数都是用来解决JavaScript中的异步问题的,从最开始的回调函数处理异步,到Promise处理异步,到Generator处理异步,再到Async/await处理异步,每一次的技术更新都使得JavaScript处理异步的方式更加优雅,从目前来看,Async/await被认为是异步处理的终极解决方案,让JS的异步处理越来越像同步任务。异步编程的最高境界,就是根本不用关心它是不是异步。
泯泷、
2024/03/09
1970
【深扒】深入理解 JavaScript 中的异步编程
虽然整个思路看起来没什么毛病,对吧。但是它就是不行的,获取数据是异步的,也就是说请求数据的时候,输出已经执行了,这时候必然是 undefined
小丞同学
2021/08/18
8070
Generator 函数的异步应用
异步编程对 JavaScript 语言太重要。JavaScript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。本章主要介绍 Generator 函数如何完成异步操作。
小小杰啊
2022/12/21
1.6K0
JavaScript中的Generator(生成器)
众所周知,传统的JavaScript异步的实现是通过回调函数来实现的,但是这种方式有两个明显的缺陷:
刘亦枫
2020/03/19
1.5K0
JavaScript异步编程:Promise、async&await与Generator
Promise 是 JavaScript 中用于处理异步操作的一种解决方案,它提供了一种更简洁、更清晰的方式来处理异步操作的结果。Promise 有三种状态:pending(进行中)、fulfilled(已完成,成功)和 rejected(已完成,失败)。Promise 的核心概念是链式调用,通过 .then() 方法处理成功(fulfilled)的结果,或者通过 .catch() 方法处理失败(rejected)的结果。
iwhao
2024/07/04
3601
Promise/async/Generator实现原理解析
笔者刚接触async/await时,就被其暂停执行的特性吸引了,心想在没有原生API支持的情况下,await居然能挂起当前方法,实现暂停执行,我感到十分好奇。好奇心驱使我一层一层剥开有关JS异步编程的一切。阅读完本文,读者应该能够了解:
Nealyang
2020/03/25
1.9K0
相关推荐
从Generator到Async function
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档