Generator简单了解

Generator是一个生成器,它生成的到底是什么呢?

Ta生成的就是一个 Iterator对象

function *gen() {
    yield 1;
    yield 2;
    return 3;
}

const it = gen();

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
console.log(it.next()); // { value: undefined, done: true }

Generator有什么意义呢?

普通函数的执行会形成一个调用栈,入栈和出栈是一口气完成的。而Generator必须得手动调用next()才能往下执行,相当于把执行的控制权从引擎交给了开发者。
所以Generator解决的是流程控制的问题。

它可以在执行过程暂时中断,先执行别的程序,但是它的执行上下文并没有销毁,仍然可以在需要的时候切换回来,继续往下执行。

最重要的优势在于,它看起来是同步的语法,但是却可以异步执行。


yield的作用

对于一个Generator函数来说,什么时候该暂停呢?就是在碰到yield关键字的时候。

function *gen() {
    console.log('a');
    yield 13 * 15;
    console.log('b');
    yield 15 - 13;
    console.log('c');
    return 3;
}

const it = gen();

运行结果:
在这里插入图片描述
看上面的例子,第一次调用it.next()的时候,碰到了第一个yield关键字,然后开始计算yield后面表达式的值,然后这个值就成了it.next()返回值中value的值,然后停在这。这一步会打印a,但不会打印b。
以此类推。return的值作为最后一个状态传递出去,然后返回值的done属性就变成true,一旦它变成true,之后继续执行的返回值都是没有意义的。
这里面有一个状态传递的过程。yield把它暂停之前获得的状态传递给执行器。

执行器传递状态给状态机内部
在这里插入图片描述

这有什么用?如果能在执行过程中给状态机传值,我们就可以改变状态机的执行条件。你可以发现,Generator是可以实现值的双向传递的。
为什么要作为上一个yield的返回值?你想啊,作为上一个yield的返回值,才能改变当前代码的执行条件,这样才有价值不是嘛。这地方有点绕,仔细想一想。


Generator自动执行

好吧,既然引擎把Generator的控制权交给了开发者,那我们就要探索出一种方法,让Generator的遍历器对象可以自动执行。

function* gen() {
    yield 1;
    yield 2;
    return 3;
}

function run(gen) {
    const it = gen();
    let state = { done: false };
    while (!state.done) {
        state = it.next();
        console.log(state);
    }
}

run(gen);

但想想我们是来干什么的,我们是来探讨JavaScript异步的呀。这个简陋的run函数能够执行异步操作吗?

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    fetch(url).then(res => res.json()).then(res => console.log(res));
}

function *gen() {
    yield fetchByName('veedrin');
    yield fetchByName('tj');
}

function run(gen) {
    const it = gen();
    let state = { done: false };
    while (!state.done) {
        state = it.next();
    }
}

run(gen);

事实证明,Generator会把fetchByName当做一个同步函数来执行,没等请求触发回调,它已经将指针指向了下一个yield。我们的目的是让上一个异步任务完成以后才开始下一个异步任务,显然这种方式做不到。
我们已经让Generator自动化了,但是在面对异步任务的时候,交还控制权的时机依然不对。

Generator异步自动执行 ---- (在回调中交还控制权)

哪个时间点表明某个异步任务已经完成?当然是在回调中咯。

我们来拆解一下思路。

  • 首先我们要把异步任务的其他参数和回调参数拆分开来,因为我们需要单独在回调中扣一下扳机。

然后yield asyncTask()的返回值得是一个函数,它接受异步任务的回调作为参数。因为Generator只有yield的返回值是暴露在外面的,方便我们控制。
最后在回调中移动指针。

function thunkify(fn) {
    return (...args) => {
        return (done) => {
            args.push(done);
            fn(...args);
        }
    }
}

这就是把异步任务的其他参数和回调参数拆分开来的法宝。是不是很简单?它通过两层闭包将原过程变成三次函数调用,第一次传入原函数,第二次传入回调之前的参数,第三次传入回调,并在最里一层闭包中又把参数整合起来传入原函数。

是的,这就是大名鼎鼎的thunkify。

const fs = require('fs');
const thunkify = require('./thunkify');

const readFileThunk = thunkify(fs.readFile);

function *gen() {
    const valueA = yield readFileThunk('/Users/veedrin/a.md');
    console.log('a.md 的内容是:\n', valueA.toString());
    const valueB = yield readFileThunk('/Users/veedrin/b.md');
    console.log('b.md 的内容是:\n', valueB.toString());
}

function run(gen) {
    const it = gen();
    function next(err, data) {
        const state = it.next(data);
        if (state.done) return;
        state.value(next);
    }
    next();
}

run(gen);

我们完全可以把回调函数抽象出来,每移动一次指针就递归一次,然后在回调函数内部加一个停止递归的逻辑,一个通用版的run函数就写好啦。上例中的next()其实就是callback()呢。

使用co

co是一个真正的异步解决方案,因为它暴露的接口足够简单

import co from './co';

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    return fetch(url).then(res => res.json());
}

function *gen() {
    const value1 = yield fetchByName('veedrin');
    console.log(value1);
    const value2 = yield fetchByName('tj');
    console.log(value2);
}

co(gen);

也许是终极异步解决方案

我知道你想说什么,写一个异步调用还得引入一个npm包(虽然是大神TJ写的包)

妈卖批的npm!

当然是不存在的。如果一个特性足够重要,社区的呼声足够高,它就一定会被纳入标准的。马上我们要介绍的就是血统纯正的异步编程家族终极继承人——爱新觉罗·async。

封装原理:

import co from 'co';

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    return fetch(url).then(res => res.json());
}

co(function *gen() {
    const value1 = yield fetchByName('veedrin');
    console.log(value1);
    const value2 = yield fetchByName('tj');
    console.log(value2);
});

使用:

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    return fetch(url).then(res => res.json());
}

async function fetchData() {
    const value1 = await fetchByName('veedrin');
    console.log(value1);
    const value2 = await fetchByName('tj');
    console.log(value2);
}

fetchData();

看看这无缝升级的体验,啧啧

灵活使用async

别被新的关键字吓到了,它其实非常灵活。

async function noop() {
    console.log('Easy, nothing happened.');
}

复制代码这家伙能执行吗?当然能,老伙计还是你的老伙计。

async function noop() {
    const msg = await 'Easy, nothing happened.';
    console.log(msg);
}

复制代码同样别慌,还是预期的表现。

只有当await关键字后面是一个Promise的时候,它才会显现它异步控制的威力,其余时候人畜无害。

function fetchByName(name) {
    const url = `https://api.github.com/users/${name}/repos`;
    return fetch(url).then(res => res.json());
}

async function fetchData() {
    const name = await 'veedrin';
    const repos = await fetchByName(name);
    console.log(repos);
}

虽然说await关键字后面跟Promise或者非Promise都可以处理,但对它们的处理方式是不一样的。非Promise表达式直接返回它的值就是了,而Promise表达式则会等待它的状态从pending变为fulfilled,然后返回resolve的参数。它隐式的做了一下处理。
注意看,fetchByName('veedrin')按道理返回的是一个Promise实例,但是我们得到的repos值却是一个数组,这里就是await关键字隐式处理的地方。

我们把它弄到一个作用域里去

import sleep from './sleep';

function work() {
    [1, 2, 3].forEach(async v => {
        const rest = await sleep(3);
        console.log(rest);
    });
    return '睡醒了';
}

work();

不好意思,return '睡醒了’没等异步操作完就执行了,这应该也不是你要的效果吧。

所以这种情况,只能用for循环来代替,async和await就能长相厮守了。

import sleep from './sleep';

async function work() {
    const things = [1, 2, 3];
    for (let thing of things) {
        const rest = await sleep(3);
        console.log(rest);
    }
    return '睡醒了';
}

work();

返回Promise实例

async可不止一颗糖哦。它是Generator、co、Promise三者的封装。如果说Generator只是一个状态机的话,那async天生就是为异步而生的。

import sleep from './sleep';

async function work() {
    const needRest = await sleep(6);
    const anotherRest = await sleep(3);
    console.log(needRest);
    console.log(anotherRest);
    return '睡醒了';
}

work().then(res => console.log('?', res), res => console.error('?', res));

因为async函数返回一个Promise实例,那它本身return的值跑哪去了呢?它成了返回的Promise实例resolve时传递的参数。也就是说return '睡醒了’在内部会转成resolve(‘睡醒了’)。

我可以保证,返回的是一个真正的Promise实例,所以其他特性向Promise看齐就好了。

并发

也许你发现了,上一节的例子大概要等9秒多才能最终结束执行。可是两个sleep之间并没有依赖关系,你跟我说说我凭什么要等9秒多?

之前跟老子说要异步流程控制是不是!现在又跟老子说要并发是不是!

我…满足你。

方法一:

import sleep from './sleep';

async function work() {
    const needRest = await Promise.all([sleep(6), sleep(3)]);
    console.log(needRest);
    return '睡醒了';
}

work().then(res => console.log('?', res), res => console.error('?', res));
import sleep from './sleep';

async function work() {
    const onePromise = sleep(6);
    const anotherPromise = sleep(3);
    const needRest = await onePromise;
    const anotherRest = await anotherPromise;
    console.log(needRest);
    console.log(anotherRest);
    return '睡醒了';
}

work().then(res => console.log('?', res), res => console.error('?', res));

办法也是有的,还不止一种。手段都差不多,就是把await往后挪,这样既能搂的住,又能实现并发。

大总结

在这里插入图片描述

原文地址: https://juejin.im/post/5cda1492e51d453a8f348c02

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值