写点什么

一步一步理解 Generator 函数的原理

  • 2022 年 1 月 11 日
  • 本文字数:3681 字

    阅读完需:约 12 分钟

作者:麦乐

来源:恒生LIGHT云社区

Generator 函数基本用法

function* helloWorldGenerator() {  yield 'hello';  yield 'world';  return 'ending';} var hw = helloWorldGenerator();
复制代码


Generator 函数调用后生成的就是一个迭代器对象,可以通过调用迭代器的 next 方法,控制函数内部代码的执行。


hw.next()// { value: 'hello', done: false } hw.next()// { value: 'world', done: false } hw.next()// { value: 'ending', done: true } hw.next()// { value: undefined, done: true }
复制代码


Generator 函数遇到 yield,可以在生成器函数内部暂停代码的执行使其挂起。在可迭代对象上调用 next()方法可以使代码从暂停的位置开始继续往下执行。


先来了解一下什么是可迭代对象?

可迭代对象

要成为可迭代 对象, 一个对象必须实现 <strong>@@iterator</strong> 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:


[ Symbol.iterator] 一个无参数的函数,其返回值为一个符合迭代器协议的对象。


let someString = "hi";typeof someString[Symbol.iterator];   let iterator = someString[Symbol.iterator]();iterator + "";                               // "[object String Iterator]" iterator.next();         // { value: "h", done: false }iterator.next();         // { value: "i", done: false }iterator.next();         // { value: undefined, done: true }
复制代码


再来看一下,挂起是怎么回?有一个新的名词“协程”。

什么是协程?

协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。


协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。


协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。

Generator 函数原理

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。


总结下来就是:


  • 一个线程存在多个协程

  • Generator 函数是协程在 ES6 的实现

  • Yield 挂起协程(交给其它协程),next 唤起协程


讲到这里,应该会对 Generator 函数有一个重新的认识吧。在实际的开发中,直接使用 Generator 函数的场景并不常见,因为它只能通过手动调用 next 方法实现函数内部代码的顺序执行。如果想象很好是使用它,可以为 Generator 函数实现一个自动执行神器。

自动执行的 Generator 函数

可以根据 g.next()的返回值{value: '', done: false}中 done 的值让 Generator 函数递归自执行:


 function run(generator) {            var g = generator();
var next = function() { var obj = g.next() console.log(obj) if(!obj.done) { next() } } next() } run(helloWorldGenerator)
复制代码


这样写能实现自执行功能,但是 不能保证执行顺序。模拟两个异步请求:


    function sleep1() {            return new Promise((resolve) => {                setTimeout(() => {                    console.log('sleep1')                    resolve(1)                }, 1000)            })        }        function sleep2() {            return new Promise((resolve) => {                setTimeout(() => {                    console.log('sleep2')                    resolve(2)                }, 1000)            })        }
复制代码


修改函数


function* helloWorldGenerator() {            yield sleep1();            console.log(1);            yield sleep2();            console.log(2);        }
复制代码


执行 run(helloWorldGenerator)看一下打印顺序:


4720


异步函数还是在同步代码执行完以后执行的,如果想要实现异步代码也能按照顺序执行,可以对代码进一步优化:


        function run(generator) {            var g = generator();
var next = function() { var obj = g.next(); console.log(obj) if(obj.done) return; // 如果yield后面返回值不是promise,可以使用Promise.resolve包裹一下,防止报错 Promise.resolve(obj.value).then(() => {next()}) } next() }
复制代码


4721


如果说 sleep1 是一个网络请求的话,在 yield 后面就可以拿到请求返回的数据,看起来像是一种更优雅的异步问题解决方案。


如果想要拿到 sleep 函数 resolve 的值,也是可以实现的。


// 修改函数 变量接收yield语句返回结果function* helloWorldGenerator() {            var a = yield sleep1();            console.log(a);            var b = yield sleep2();            console.log(b);        }// g.next(v); 传递结果值      function run(generator) {            var g = generator();
var next = function(v) { var obj = g.next(v); console.log(obj) if(obj.done) return; // 如果yield后面返回值不是promise,可以使用Promise.resolve包裹一下,防止报错 Promise.resolve(obj.value).then((v) => {next(v)}) } next() }
复制代码


你会看到和上面一样的打印结果。


仔细看上面实现方式跟 async await 很相似,实际上这就是 async await 的原理。

async await

async await 本质上就是结合 promise 实现的一个自执行 Generator 函数。将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已


更详细的代码如下,感兴趣的同学可以深入了解一下:


 // async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已            // async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句            // Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器和普通函数一样执行            // async函数对 Generator 函数的改进:            /*            (1)内置执行器            (2)更好的语义            (3)更广的适用性            (4)返回值是 Promise            */               // async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。            async function fn(args) {                // ...            }            // 等同于 里面的await 换成 yield            function fn(args) {                return spawn(function*() {                    // ...                });            }              function spawn(genF) {                return new Promise(function(resolve, reject) {                    const gen = genF();                    function step(nextF) {                        let next;                        try {                            next = nextF();                        } catch (e) {                            return reject(e);                        }                        if (next.done) {                            return resolve(next.value); // 这也是为什么 不使用try catch的话,异常异步请求后面的代码不再执行的原因,从这里不再继续调用nex()方法了。                        }                        Promise.resolve(next.value).then(function(v) {                            step(function() {                                return gen.next(v);                            });                        }, function(e) {                            step(function() {                                return gen.throw(e); // 这里可以解释为什么async 函数需要使用try catch来捕获异常,生成器函数的throw,会让代码到catch里面                            });                        });                    }                    step(function() {                        return gen.next(undefined);                    });                });            }
复制代码




想向技术大佬们多多取经?开发中遇到的问题何处探讨?如何获取金融科技海量资源?


恒生LIGHT云社区,由恒生电子搭建的金融科技专业社区平台,分享实用技术干货、资源数据、金融科技行业趋势,拥抱所有金融开发者。


扫描下方小程序二维码,加入我们!



发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2018.11.07 加入

还未添加个人简介

评论

发布
暂无评论
一步一步理解Generator函数的原理