异步编程之——理解es6的Generators(生成器 )

前言

学习Generators之前我们先理解一下es6的迭代器:迭代器详解

正文

什么是Generators?

生成器也是ES6新增加的一种特性。它的写法和函数非常相似,只是在声明时多了一个” * ”号。

function* say(){}
const say = function*(){}

注意:这个 ※ 只能写在function关键字的后面。

但是生成器和普通函数并不是只有写法上的区别,本质上,普通函数在调用后,必定执行该函数直到函数结束或者return为止,中途是不能停止的,而生成器则是可以暂停的的函数。

function* say(){
    yield "开始";
    yield "执行中";
    yield "结束";
}
let it = say(); // 调用say方法,得到一个迭代器
console.log(it) // [object Generator]
console.log(it.next()); // { value: '开始', done: false }
console.log(it.next()); // { value: '执行中', done: false }
console.log(it.next()); // { value: '结束', done: false }
console.log(it.next()); // { value: undefined, done: true }

调用生成器的say方法,此时的函数并没有执行而得到的是一个迭代器,通过前言中迭代器的认识,我们可以知道调用迭代器next()方法,say函数开始执行,当遇到yield时,函数被挂起并返回一个对象,其中包含value属性,它的值是yield后面跟着的数据,并且done的值为false。再次执行next,函数又被激活,并继续往下执行,直到遇到下一个yield。当所有的yield都执行完了,再次调用next时得到的value就是undefined,done的值为true。

当然Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

function* f() {
  console.log('执行了!')
}
var generator = f();
setTimeout(function () {
  generator.next()
}, 2000);   //两秒后 -> "执行了!"

如果你能理解前言中的迭代器,那么此时的生成器也就很好理解了。它可以通过yield关键字将函数的执行挂起,或者理解成暂停。它的外部在通过调用next方法,让函数继续执行,直到遇到下一个yield,或函数执行完毕,它的yield,其实就是next方法执行后挂起的地方,并得到你返回的数据。

所以生成器由此可以理解为 可以暂停(暂缓执行)的函数

生成器的特征和自带方法

next 是否需要传参数?

下面再看一个例子

 function * say(x) {
     let y = 2 * (yield x)
     let z = yield (y / 3)
     return x+y+z
 }
let it = say(5)
console.log(it.next());//{value: 5, done: false}
console.log(it.next());//{value: NaN, done: false}
console.log(it.next());//{value: NaN, done: false}

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

下面我们给next传入参数:

function* say(x) {
   let y = 2 * (yield x)
   let z = yield(y / 3)
   console.log(x,y,z);//9,18,5
   return x + y + z
}
let it = say(6)
console.log(it.next());//{value: 6, done: false}
console.log(it.next(9));//{value: 6, done: false}
console.log(it.next(5));//{value: 29, done: true}

这里需要认真理解下,
第一个it.next()时返回的是 (yield x)的值,say(6)传参为6,所以第一个next() 返回结果为x:6
第二个it.next(9)时传值为9,这里的参数会替换上一个next的返回值来进行计算,所以结果为x:9,y= 2 * x =18,则返回 z = y/3 = 6,最终的返回结果为 z:6。
第三个it.next(5)时传值为5,更改上一个返回结果z:6,则z=5, 综上的x:9,y:18,z:5,所以xyz之和为29。

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。

使用for of循环时不需要调用next()

for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
用生成器来完成斐波那契数列:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (curr=1;curr;[prev, curr] = [curr, prev + curr]) {
    yield curr;
  }
}
for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}
yiled* 表达式

ES6 提供了yield*表达式,,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

function* inner() {
  yield 'hello!';
}

function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

生成器的实例方法

throw

Generator 在函数体外抛出一个错误,然后在函数体内捕获。例如

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
return

返回给定值,并终结生成器。例如

function *gen2(){
    yield 1;
    yield 2;
    yield 3;
}
let g2 = gen1();
g2.next();//{value:1,done:false}
g2.return();//{value:undefined,done:true}
g2.next();//{value:undefined.done:true}

遍历器对象g2 调用return方法后,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true。
如果return方法调用时,不提供参数,则返回值的value属性为undefined。

生成器的实用场景?

Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。

实现异步代码的同步化

有时候我们希望在异步操作中加入同步的行为。比如,我想打印4句话,但是每句话都在前一句话的基础上延迟2秒输出。代码如下:
使用定时器:

setTimeout(function(){
    console.log("first");
    setTimeout(function(){
        console.log("second");
        setTimeout(function(){
            console.log("third");
            setTimeout(function(){
                console.log("fourth");
            },2000);
        },2000);
    },2000);
},2000);

使用回调函数实现异步,你想在前面的异步操作完成后再进行接下来的动作,那只能在它的回调函数中进行,这样就会越套越多,代码越来越来复杂,俗称“回调地狱”。

那么用promise优化一下该功能:

let timeout = function(time){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve();
        },time);
    });
}
console.log("go on");
timeout(2000).then(function(){
    console.log("first");
    return timeout(2000);
}).then(function(){
    console.log("second");
    return timeout(2000);
}).then(function(){
    console.log("third");
    return timeout(2000);
}).then(function(){
    console.log("fourth");
    return timeout(2000);
});

Promise作为ES6提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。

那么使用Generators怎么实现该功能:

//定时器包含Promise方法,方便之后调用
function sleep(time, data) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(console.log(data))
                }, time);
            })
        }
//生成器函数
function* timeout(time) {
            let sleep1 = yield sleep(2000, 'first')
            let sleep2 = yield sleep(2000, 'second')
            let sleep3 = yield sleep(2000, 'third')
            let sleep4 = yield sleep(2000, 'fourth')
            return console.log('go on');
        }
// 手动执行
let it = timeout();
it.next().value.then(() => {
            it.next().value.then(() => {
                it.next().value.then(() => {
                    it.next().value.then(() => {
                        it.next()
                    })
                })
            })
});
// 自动执行
function run(gen) {
            var g = gen()
            function next(data) {
                var result = g.next(data) //{value: Promise, done: false}
                if (result.data) return result.value //如果done:true,则返回最后一项
                result.value.then(data => next(data))//如果不是,就继续调用next()进行下一步
            }
            next()
}
run(timeout)

上述代码就使用了生成器+promise来完成了,定时2秒打印的功能,手动执行其实就是用then方法,层层添加回调函数。而自动执行只要 Generator 函数还没执行到最后一步,next函数就调用自身,一次实现自动执行。

这样,在生成器中,就可以像写同步代码一样来实现异步操作。

但是生成器这种方式需要编写外部的执行器,而执行器的代码写起来一点也不简单。除了可以使用外部模块来自动执行之外,我们在es7中还会学习到一个基于生成器实现的语法糖 :async和await

那么我们看一下async和await怎么实现该功能?

 function sleep(time, data) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(console.log(data))
                }, time);
            })
        }
  async function timeout() {
       let sleep1 = await sleep(2000, 'first')
       let sleep2 = await sleep(2000, 'second')
       let sleep3 = await sleep(2000, 'third')
       let sleep4 = await sleep(2000, 'fourth')
       return console.log('go on');
  }
 timeout()

就这样,像同步一样写异步代码,很简单的实现了,大喊一声牛逼!但是因为是es7的语法,所以还是要考虑兼容性的。

在ES7中,加入了async函数来处理异步。它实际上只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用await替代yield,async替代生成器的(*)号。

第一句用了await。它替代了之前的yield。后面同样需要跟上一个Promise对象。接下来的打印语句会在上面的异步操作完成后执行。外部调用时就和正常的函数调用一样,但它的实现原理和生成器是类似的。因为有了async关键字,所以它的外部一定会有相应的执行器来执行它,并在异步操作完成后执行回调函数。只不过这一切都被隐藏起来了,由JS引擎帮助我们完成。我们需要做的就是加上关键字,在函数中使用await来执行异步操作。这样,可以大大的简化异步操作。

附:自动执行的插件–co模块

Generators可以像同步一样写异步代码,但是执行的过程却有点不尽人意,所以可以借助插件来完成 :co模块。

从上面的例子可以看出,generator 函数体可以停在 yield 语句处,直到下一次执行 next()。co 模块的思路就是利用
generator 的这个特性,将异步操作跟在 yield 后面,当异步操作完成并返回结果后,再触发下一次 next() 。

结语

以上,算是对es6的Generators(生成器 )进行了完整的了解,还有更多处理异步的方法和各自优缺点对比请看另一篇 :JavaScript异步编程是什么? 异步编程都有哪些解决方案?
借鉴:
https://www.cnblogs.com/jaxu/p/6372809.html
http://es6.ruanyifeng.com/#docs/generator

如果本文对你有帮助的话,请不要忘记给我点赞打call哦~o( ̄▽ ̄)do
有其他问题留言 over~

你可能感兴趣的:(ES6/ES7/8/9...)