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魔法背后的秘密四、终极方案PromiseGeneratorGenerator的这种特性对于异步来说意味着什么呢意味着我们终于获得了一种在不阻塞主线程的前提下实现“同步等待”的方法为便于说明先上一段直接使用回调的代码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想象成returnGenerator的栈帧需要被弹出会先计算yield右边的表达式即执行request函数调用以获得用于返回给上一级栈帧的值。当然request函数没有返回值但它发送了一个异步ajax请求并注册了一个onSuccess回调表示在请求返回结果时恢复Generator的栈帧并继续运行代码并把结果作为参数塞给Generator准确地说是塞到yield所在的地方这样response变量就获得了ajax的返回值。可以看出这里yield的功能设计得非常巧妙好像它可以“赋值”给response。