Generator异步化同步

前言

Generator这个异步化同步的说法我真的理解了很久,结合js的事件队列来联想,我一直想不懂。。

JS的运行规则

首先,我们先简单回顾一下JS的运行规则:

  1. JS是单线程的,只有一个主线程
  2. 函数内的代码从上到下顺序执行,遇到被调用的函数先进入被调用函数执行,待完成后继续执行
  3. 遇到异步事件,浏览器另开一个线程,主线程继续执行,待结果返回后,执行回调函数。
那么,Generator函数是如何进行异步化为同步操作的呢?

实质上很简单,* 和 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


猜你喜欢

转载自blog.csdn.net/chiuwingyan/article/details/80550101