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

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()方法可以使代码从暂停的位置开始继续往下执行。

如果Generator函数带有return 了一个值,将会在done为ture的结果值中拿到,也就是value。

function* helloWorldGenerator() {
  const res1 = yield 'hello';
  console.log(res1, 'res1')
  const res2 = yield 'world';
  console.log(res2, 'res2')
  return 'ending';
}
 
var hw = helloWorldGenerator();

console.log(hw.next());
console.log(hw.next());
console.log(hw.next());

问题: 为什么上面的res都是undefined?这是构造器函数的规范,如果想要res1有值,可以在下一个next函数传递进去:

function a() {return 3}

function *g() {
  const res = yield a()
  console.log(res) // 6
}
const gen = g()

const g1 = gen.next();
console.log(g1);
const g2 = gen.next(6);
console.log(g2);
{value: '3', done: false}
6
{value: undefined, done: true}

问题: 如果yield后面是一个promise,会怎么?

function *g() {
  const res = yield Promise.resolve(2);
  console.log(res) 
}
const gen = g()

const g1 = gen.next();
console.log(g1);
const g2 = gen.next();
console.log(g2);

如果想要拿到 Promise.resolve(2)执行结果2这个值,需要这样写:

function *g() {
  const res = yield Promise.resolve(2);
  console.log(res) // 6
}
const gen = g()

const g1 = gen.next();
console.log(g1);
Promsie.resolve(g1.value).then((v) => {
  const g2 = gen.next(v);
  console.log(g2);
})

可迭代对象

上面提到生成器函数执行后会返回一个可迭代对象。这里讲一下什么是可迭代对象?

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

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

目前所有的内置可迭代对象如下:StringArrayTypedArrayMap 和 Set,它们的原型对象都实现了 @@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 }

而Generator函数调用后的返回值,就是具有这样特性的一个可迭代对象。

当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。

所以返回值也是支持for in或者for of循环的。

let someString = "hi";
typeof someString[Symbol.iterator] === 'function';   // true
let iterator = someString[Symbol.iterator]();

for(let value of iterator) {
  console.log(value) // h ,i
}
function *g() {
  const res = yield Promise.resolve(2);
  const res1 = yield Promise.resolve(3);
}
const gen = g()

for(let key in  gen) {
  console.log(key);
}

for(let value of  gen) {
  console.log(value);
}

 

什么是协程和挂起?

协程(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)
            })
        }

generator函数

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

正常的输出顺序应该为:

sleep1,1,  sleep2, 2

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

41D1643C-3546-4CDD-84E6-8FAFD86B99B9.png

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


        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()
        }

1.png

如果说sleep1是一个网络请求的话,在yield后面就想要拿到返回的数据,就可以这样实现:

// 修改函数 变量接收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本质上就是结合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);
                    });
                });
            }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值