前言
Generator这个异步化同步的说法我真的理解了很久,结合js的事件队列来联想,我一直想不懂。。
JS的运行规则
首先,我们先简单回顾一下JS的运行规则:
- JS是单线程的,只有一个主线程
- 函数内的代码从上到下顺序执行,遇到被调用的函数先进入被调用函数执行,待完成后继续执行
- 遇到异步事件,浏览器另开一个线程,主线程继续执行,待结果返回后,执行回调函数。
实质上很简单,* 和 yield 是一个标识符,在浏览器进行软编译的时候,遇到这两个符号,自动进行了代码转换:
// 异步函数 function asy() { $.ajax({ url: 'test.txt', dataType: 'text', success() { console.log("我是异步代码"); } }) } function* gener() { let asy = yield asy(); yield console.log("我是同步代码"); } let it = gener().next(); it.then(function() { it.next(); }) // 我是异步代码 // 我是同步代码
浏览器编译过后:
function gener() { // let asy = yield asy(); 替换为 $.ajax({ url: 'test.txt', dataType: 'text', success() { console.log("我是异步代码"); // next 之后执行以下 console.log("我是同步代码"); } }) // yield console.log("我是同步代码"); }
这里,我们首先要对函数在执行时,内存内发生了什么。参考:https://juejin.im/entry/5a5971cf6fb9a01ca3253af9
栈与堆
一个JS程序的内存分为代码区、栈区、堆区和队列区(图中没有画出代码区):
队列(Queue)就是FEer所熟知的事件循环队列。
代码区保存着全部JS源代码被引擎编译成的机器码(以V8为例)。
栈(stack)保存着每个函数执行所需的上下文,一个栈元素被称为一个栈帧,一个栈帧对应一个函数。
对于引用类型的数据,在栈帧里只保存引用,而真正的数据存放在堆(Heap)里。堆与栈不同的是,栈内存由JS引擎自动管理,入栈时分配空间,出栈时回收,非常清楚明了;而堆是程序员通过new操作符手动向操作系统申请的内存空间(当然,用字面量语法创建对象也算),何时该回收没那么明晰,所以需要一套垃圾收集(GC)算法来专门做这件事。
扯了一堆预备知识,终于可以回到Generator的正题了:
普通函数在被调用时,JS引擎会创建一个栈帧,在里面准备好局部变量、函数参数、临时值、代码执行的位置(也就是说这个函数的第一行对应到代码区里的第几行机器码),在当前栈帧里设置好返回位置,然后将新帧压入栈顶。待函数执行结束后,这个栈帧将被弹出栈然后销毁,返回值会被传给上一个栈帧。
当执行到yield语句时,Generator的栈帧同样会被弹出栈外,但Generator在这里耍了个花招——它在堆里保存了栈帧的引用(或拷贝)!这样当iter.next方法被调用时,JS引擎便不会重新创建一个栈帧,而是把堆里的栈帧直接入栈。因为栈帧里保存了函数执行所需的全部上下文以及当前执行的位置,所以当这一切都被恢复如初之时,就好像程序从原本暂停的地方继续向前执行了。
而因为每次yield和iter.next都对应一次出栈和入栈,所以可以直接利用已有的栈机制,实现值的传出和传入。
这就是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的返回值。
个人理解:
这里的化异步为同步,其实是对于gen函数里面的代码而言,因为要等到ajax请求成功执行回调函数,才会继续执行next方法,也才会继续执行gen函数里面接下来的代码,直到遇到下一个next(),next()可以对generator函数进行代码控制。
例子:
function time(){ setTimeout(() => { console.log(456) it.next(123) }, 2000); } function* gen() { let timer=yield time(); console.log(timer) } let it=gen(); it.next()
这个例子,运行的结果是2s后先输出 456 ,再输出123.
这里,gen函数运行到yield处时,由于暂停了,它会执行右边的time函数,gen函数会被推出栈。由于time函数里面是一个定时器,因此,2s后才会把事件添加到事件队列当中,此时js线程一直是空闲状态,直到定时器回调函数加入到事件队列当中,js线程才会把这个事件取出放到js线程中执行,继而输出=》456,然后执行it.next(),在堆当中把gen函数直接入栈,再输出=》123,这样,仿佛化异步为同步。
再有个例子:
function time(){ setTimeout(() => { console.log(456) it.next(123) }, 2000); } function* gen() { let timer=yield time(); console.log(timer) } function test() { console.log('test') } let it=gen(); it.next() test();
这里,运行的结果是先输出=>test,=>456,=>123
和上面的类似,这里gen函数运行到yield时,先去执行time函数,gen()被推出栈,由于time里面是定时器,浏览器定时器线程此时会几时,到了时间,浏览器触发事件线程就会把回调函数放到事件队列。由于time是异步函数,因此yield以后,就会执行接下来的test方法,等到主线程空闲了,并且事件队列当中存在定时器函数,才会把回调函数执行。才会继续执行gen函数yeild下面的代码。
因此,我们可以看出,Generator函数化异步为同步,其实只是对于gen函数里面而已,也可以理解成,浏览器在遇到generator的时候,会进行编译,其实还是依据JavaScript的运行规则来执行的。
参考文献:https://www.cnblogs.com/liangyin/p/8299313.html