你不知道的JS系列——关于生成器

生如夏花之绚烂,死如秋叶之静美

说明

学习总结 + 个人理解,巩固 + 方便查阅,大家愿意看的简单看看就好

Promise 真的就是最好了?

在上一篇《你不知道的JS系列——深入理解Promise》,我们看到了 Promise 相对于回调表达程序异步所体现出来的优越性:顺序性和可信任性,但 Promise 就是最好了吗?
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
那么有没有一种以顺序的、同步的来表达异步的方式,因为这更符合我们大脑的思考模式,此时不简单的生成器(Generator)登场了。直接说吧,ES6 中 最完美的世界 就是生成器(看似同步的异步代码)和 Promise(可信任可组合)的结合。我们平时应用最多的 async/await 就是它们的组合实现。
Tips: 生成器与遍历器具有不可分割的关系,要了解生成器,那么首先得需要了解遍历器。

什么是 Iterator(遍历器)?

Iterator 既称迭代器也称遍历器,都表达的是一个意思。它是一种统一的接口机制,用来处理所有不同的数据结构。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作,这个接口主要供 for...of 消费。
Iterator 遍历器对象,主要具有 next方法,还可以具有 return 方法和 throw 方法,其中 return 方法和 throw 方法并不是必须的。
一个手写的 Iterator 遍历器对象大致如下:

{
    next: function() {
        return { value: '某个值', done: true};  // done 是布尔值,代表遍历是否结束
    },
    return: function() {
        file.close();       // 可以在这里清理或释放资源
        return { value: '某个值', done: true };
    },
    throw: function() {
        // 用于在函数体外调用抛出错误
    }
}
复制代码

一般我们并不会手写 Iterator 遍历器,多是用 Generator 函数来自动生成。

什么是 Generator(生成器)?

基本概念

Generator 函数(生成器)是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • 语法上,可以把它理解成一个状态机,封装了多个内部状态
  • 形式上,Generator 函数是一个普通函数,但是有两个特征:
    function 关键字与函数名之间有一个星号(*
    ② 函数体内部使用 yield 表达式,yield(翻译:产出)用来定义不同的内部状态

执行 Generator 函数,并不会像普通函数一样执行内部代码最终返回一个结果值,而是会返回一个遍历器(Iterator)对象。下面是一个 Generator 函数:

function* helloWorldGenerator() {
    yield 'hello';  // 状态 hello
    yield 'world';  // 状态 world
    return 'ending';    // 最终状态return定义  ending
}
var hw = helloWorldGenerator();  // 执行,返回遍历器对象 hw
复制代码

执行生成器函数返回的遍历器对象,我们通过它自身的 next方法来启动,以后每次调用next方法依次遍历,每次遍历会返回一个有着valuedone两个属性的对象。

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

value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

yield 表达式

yield表达式就是暂停标志。即调用next方法遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

注意:

  • return语句作为程序的终止,也就是会作为遍历器对象遍历结束的标志。遍历遇到return语句,会将它后面的表达式的值作为返回的对象的value属性值,如果遍历结束没有return语句,则返回对象的value属性值为undefined(程序结束默认就是return undefined;
  • yield表达式只能用在 Generator 函数里面
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号

警告: 不要在 forEach、map、filter...等方法内,将回调函数声明为生成器函数,然后再使用yield表达式,因为它们规定了接收参数就是一个普通函数,不会接收生成器函数,最好的做法就是将外部函数声明为生成器函数,然后使用for循环(除了这里的原因外,大家也不要抵制写for循环,虽然它麻烦,但是它的效率却是最高的)

Symbol.iteratorfor...of 循环

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。

因为 Generator 函数就是遍历器生成函数,所以可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

默认调用 Iterator 接口(即Symbol.iterator方法)的场合:

  • 解构赋值
  • 扩展运算符
  • yield* 表达式
  • 任何接受数组作为参数的场合(Array.from()、Promise.all()、Promise.race()...

for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围:

  • 数组(Array
  • SetMap 结构
  • 类数组对象(arguments对象、DOM NodeList 对象)
  • Generator 函数返回的遍历器对象
  • 字符串(String

next 方法

yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function *foo(x) { 
    var y = x * (yield "Hello"); // yield "Hello" 即yield表达式
    return y; 
} 
var it = foo( 6 ); 
var res = it.next(); // 启动生成器,不传入参数
res.value; // "Hello" 
res = it.next( 7 ); // 向等待的yield传入7,即设置上一个yield表达式结果为7
res.value; // 42
复制代码

注意: next方法携带的参数,是上一个yield表达式的返回值。另外,第一次调用next方法,代表启动生成器,还没有暂停的 yield 来接受这样一个值,所以即使你写了也是无效,规范和所有兼 容浏览器都会默默丢弃传递给第一个 next() 的任何东西。

return 方法

遍历器对象的return方法,可以返回给定的值,并且终结遍历 Generator 函数。若不提供参数,则返回值的value属性为undefined。 如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}
var g = gen();  // 得到遍历器对象
g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }     返回指定值并终结遍历
g.next()        // { value: undefined, done: true }
复制代码

注意: 如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。

throw 方法

遍历器对象的throw方法,用于在函数体外抛出错误,然后在 Generator 函数体内捕获。

注意:

  • 如果 Generator 函数体内未捕获(try...catch),那么可以在外部捕获,如果外部也没有捕获,那么程序将报错,直接中断执行
  • throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法
  • throw方法被内部捕获以后,会附带执行一次next方法
  • Generator 函数体内抛出的错误,也可以被函数体外的catch捕获
  • Generator 函数体内抛出错误且没有被内部捕获,它就不会再执行下去了

next()、throw()、return() 的共同点

都是让 Generator 函数恢复执行,只是使用了不同的语句替换yield表达式:

  • next()是将yield表达式替换成一个值
  • throw()是将yield表达式替换成一个throw语句
  • return()是将yield表达式替换成一个return语句

生成器委托—— yield* 表达式

yield* 表达式用于在 Generator 函数内部,调用另一个 Generator 函数,省去了我们手动完成遍历的繁琐过程。

function* foo() {
    yield 'a';
    yield 'b';
}
function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}
// 等同于
function* bar() {
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
}
复制代码

生成器 + Promise—— async/await

一个简单的 🌰 :

function foo(x,y) { 
    return request(    // request 是一个用 Promise 封装的异步请求方法
        "http://some.url.1/?x=" + x + "&y=" + y 
    ); 
} 
function *main() { 
    try { 
        var text = yield foo( 11, 31 ); 
        console.log( text ); 
    } catch (err) { 
        console.error( err ); 
    } 
}
var it = main(); 
var p = it.next().value; 
// 等待promise p决议
p.then( 
    function(text){ 
        it.next( text ); 
    }, 
    function(err){ 
        it.throw( err ); 
    } 
)
复制代码

考虑如果该生成器函数内有多个异步请求,那么每次我们都需要如此处理,即等待promise决议后手动调用next方法完成生成器函数,那代码会越来越多,显得繁琐和难于处理,所以我们需要一个工具函数来简化我们的操作。下面是一个工具函数的 🌰 :

function run(gen) { 
    var args = [].slice.call( arguments, 1), it; 
    // 在当前上下文中初始化生成器
    it = gen.apply( this, args ); 
    // 返回一个promise用于生成器完成
    return Promise.resolve() 
    .then( function handleNext(value){ 
        // 对下一个yield出的值运行
        var next = it.next( value ); 
        return (function handleResult(next){ 
            // 生成器运行完毕了吗?
            if (next.done) { 
                return next.value; 
            } 
            // 否则继续运行
            else { 
                return Promise.resolve( next.value ) 
                .then( 
                 // 成功就恢复异步循环,把决议的值发回生成器
                    handleNext, 
                    // 如果value是被拒绝的 promise,
                     // 就把错误传回生成器进行出错处理
                    function handleErr(err) { 
                        return Promise.resolve( 
                            it.throw( err )
                        ) 
                        .then( handleResult ); 
                    } 
                ); 
            } 
        })(next); 
    } ); 
}
// 使用
function *main() { 
 // .. 
} 
run( main );
复制代码

这个工具函数也不必死磕,注意到它的返回值是Promise且内部是通过递归完成生成器函数的执行就好了。
其实上面这就是async/await的实现原理,对于async/await需要注意的地方:

  • async函数的返回值是 Promise 对象,可以用then方法指定下一步的操作
  • await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
  • await命令后面是一个thenable对象,那么await会将其等同于 Promise 对象
  • 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。两种解决办法:

  • await 放在 try...catch 结构里面
  • await 后面的 Promise 对象再跟一个 catch 方法

注意: 如果不是特别强调执行顺序,那么请让多个请求并发执行,使用 Promise.all 方法,然后再 await 等待所有异步的执行结果,利用并发来提高程序性能。

作者:伟大的兔神
链接:https://juejin.im/post/5ef5681d6fb9a07e847229ff

服务推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值