【前端进阶】 ES6之“生成器”学习+ 生成器和Promise的结合使用

前言:

【前端进阶】 ES6之“生成器”学习+ 生成器和Promise的结合使用_第1张图片
ES6生成器是一种可以实现看似同步的异步流程控制表达风格。

1.1 打破完整运行

在代码中几乎普遍依赖的一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。
这是个错误的假定。ES6引入了一个新的函数类型,它并不符合这种运行到结束的特性,这类新函数被称为生成器。

看下面代码:

var x=1;
    function *foo(){
      x++;
      yield;//暂停
      console.log("x:",x);
    }
    function bar(){
      x++;
    }

ES6代码中只是暂停点的语法是yield。运行到这里,程序会暂停。

注意:很多地方生成器的声明格式是 function* foo(){…},而不是我这里使用的function *foo(){…};唯一的区别是 * 位置的风格不同。这两种形式在功能和语法上是等同的。

看下面代码如何运行生成器:

	var it=foo();//构造一个迭代器it来控制这个生成器
	
    it.next();//这里启动foo()
    
    console.log(x);
    bar();
    console.log(x);
    it.next();

【前端进阶】 ES6之“生成器”学习+ 生成器和Promise的结合使用_第2张图片
分析运行结果:

  1. it=foo()运算并没有执行生成器*foo(),而只是构造了一个迭代器,这个迭代器会控制它的执行。
  2. 在第一个it.next()启动了生成器*foo(),并运行了 *foo()的第一行x++。
  3. *foo()在yield语句处暂停,在这一点上第一个it.next()调用结束。此时 *foo()仍然在运行并且是活跃的,但是处于暂停状态。
  4. 我们查看x的值,此时是2 。
  5. 我们调用bar(),它通过x++再次递增x。
  6. 我们再次查看x的值,此时为3 。
  7. 最后的it.next()调用从暂停处恢复了生成器* foo()的执行,打印出x:3。

显然,foo()启动了,但是没有完整运行,它在yield处暂停了。后面恢复了foo()并让它运行到结束,但这不是必须的。

生成器就是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。

1.2 输入和输出

生成器是一个函数,那么它就可以接受参数也能够返回值。
看下面代码:

 function *foo(x,y){
      return x*y;
    }
    var it=foo(6,7);
    var res=it.next();
    console.log(res.value);//42

分析结果:

  1. it=foo(6,7);只是创建了一个迭代对象,把它赋值给一个变量it。用于控制生成器
  2. 调用it.next()来指示生成器开始继续运行,停在下一个yield处或者直到生成器结束。
  3. 这个next(…)调用的结果是一个对象,它有一个value属性。换句话说,yield会导致生成器在执行过程中发送出一个值,这有点类似于中间的return。

迭代消息传递
除了能够接受参数并提供返回值之外,生成器甚至提供了更强大更引人注目的内建消息输入输出能力,通过yield和next(…)实现。

看下面代码:

	function *foo(x){
    	var y=x*(yield);
    	return y;
 	  }
   	var it=foo(6);
   	it.next();
  	var res=it.next(7);
   	console.log(res.value);//42

运行结果分析:
在*foo()内部,开始执行语句var y=x…,但随后遇到了一个yield表达式。它们会在这一点暂停 *foo(),接下来调用it.next(7),这一句把值7传回作为被暂停的yield表达式的结果。即var y=6 * 7;

注意:yield和next(…)调用有一个不匹配,一般来说,需要的next(…)调用要比yield语句多一个。
为什么不匹配呢?
因为第一个next(…)总是会启动一个生成器,并运行到第一个yield处。不过,是第二个next()调用完成第一个被暂停的yield表达式,以此类推。

两个问题的故事
只考虑生成器的代码:

var y=x*(yield);
return y;

第一个yield基本上是提出了第一个问题:“这里我应该插入什么值?”。
谁来回答这个问题呢?第一个next()已经运行,使得生成器启动并运行到此处,所以显然它无法回答这个问题。因此必须由第二个next()调用回答第一个yield提出的这个问题。

看到不匹配了吗?第二个对应第一个?

我们转换一下视觉,不从生成器的视觉来看这个问题,而是从迭代器的角度。
消息是双向传递的----yield,作为一个表达式可以发出消息响应next()调用,next()也可以向暂停的yield表达式发送值。
看下面代码:

	function *foo(x){
     var y=x*(yield "Hello World");
     return y;
   }
   var it =foo(6);
   var res=it.next();//第一个next()并不传入任何东西
   console.log(res.value);//Hello World
   res=it.next(7);//向等待的yield传入7
   console.log(res.value);//42

yield…和next(…)这一对组合起来,在生成器的执行过程中构成了一个双向消息传递系统。

只看下面迭代器的代码:

   var res=it.next();//第一个next()并不传入任何东西
   console.log(res.value);//“Hello World”
   res=it.next(7);//向等待的yield传入7
   console.log(res.value);//42

分析:

  1. 我们并没有向第一个next()调用发送值,这是有意为之。只有暂停的yield才能接受这样一个通过next(…)传递的值,而在生成器的起始处我们调用第一个next()时,还没有暂停的yield来接受这样的一个值。所以第一个next()最好不传送值。

第一个next()调用基本上就是在提出一个问题:生成器*foo()要给我的下一个值是什么。”。谁来回答这个问题?第一个yield “Hello World”表达式。

那么第二个next(7)调用再次提出这样的问题:生成器将要产生的下一个值是什么?。但是上面代码没有yield来回答这个问题了,这该怎么处理。
return语句来回答这个问题。就算没有return语句,也会默认的return undefined来回答it.next(7)调用提出的问题。

yield和next()建立的双向消息传递是非常强大的。

1.3 生成器产生值

1.3.1 生产者与迭代器

假定你要产生一系列值,其中每个值都与前面一个都特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值。

要实现这一点很多人会想到使用函数的闭包。类似如下:

var gimmeSomething=(function(){
      var nextVal;
      return function(){
        if(nextVal===undefined){
          nextVal=1;
        }else{
          nextVal=(3*nextVal)+6;
        }
        return nextVal;
      }
    })();

giimeSomething();//1
giimeSomething();//9
giimeSomething();//33
giimeSomething();//105

看看下面使用迭代器是如何实现的:

 var something=(function(){
      var nextVal;
      return {
        [Symbol.iterator]:function(){return this;},
        next:function(){
          if(nextVal===undefined){
            nextVal=1;
          }else{
            nextVal=(3*nextVal)+6;
          }
          return {done:false,value:nextVal};
        }
      }
    })();
something.next().value;//1
something.next().value;//9
something.next().value;//33
something.next().value;//105

代码分析:

  1. [Symbol.iterator]的[…]是ES6的新特性,被称为计算属性名,即为可以指定一个表达式并用这个表达式并用这个表达式的结果作为属性的名称。
  2. next()调用返回一个对象。这个对象有两个属性:done是一个boolean值,标时迭代器的完成状态;value中放置迭代值。

ES6还新增了一个for…of循环,这意味着可以通过原生循环语法自动迭代标准迭代器:

for(var v of something){
     console.log(v);
     if(v>500){
       break;
     }
   }

//1  9  33  105  321  969

代码分析:因为我们的迭代器something总是返回done:false,因此这个for…of循环将永远运行下去,这也就是为什么我们要在里面放一个break条件。迭代器永不结束是完全没有问题的,但是也有一些情况下,迭代器会在有限的值集合上运行,并最终返回done:true。

for…of循环在每次迭代中自动调用next(),它不会向next()传入任何值,并且会在接收道done:true之后停止。这对于在一组数据上循环很方便。

除了构造自己的迭代器,许多的JS内建数据结构,比如Array,有默认的迭代器:

 var a=[1,3,5,7,9];
   for(var v of a){
     console.log(v);
   }
  
  //1,3,5,7,9

哈哈,数组不用定义迭代器就可以实现循环噢,对象的话,就要自己定义一个迭代器属性才可以循环。但是,如果你只是想迭代一个对象的所有属性的话(不需按对象定义时的属性顺序输出),可以通过Object.keys(…)返回一个array,类似于for(var v of Object.keys(obj))这样也可以实现迭代。这样跟for…in循环类似。

1.3.2 iterable

iterable(可迭代),即指一个包含可以在其值上迭代的迭代器的对象。

从ES6开始,从一个iterable中提取迭代器的方法是:iterable必须支持一个函数,其名称是专门的ES6符号值Symbol.iterator。调用这个函数时,它会返回一个迭代器。通常没调用一次会返回一个全新的迭代器。
看下面代码:

    var a=[1,3,5,7,9];
    var it=a[Symbol.iterator]();
    it.next().value;//1
    it.next().value;//3
    it.next().value;//5

这个就是上面使用for…of的原理,a就是一个iterable。for…of循环自动调用它的Symbol.iterator函数来构建一个迭代器。

前面的代码中定义迭代器有下面这句代码:


return
{
	[Symbol.iterator]:function(){return this;}
	next:function(){
          if(nextVal===undefined){
            nextVal=1;
          }else{
            nextVal=(3*nextVal)+6;
          }
          return {done:false,value:nextVal};
        }
    };

在使用for…of循环时,它会寻找并调用它的Symbol.iterator函数。我们将这个函数定义为简单的return this,也就是把自身返回,即可调用next属性。

1.3.3 生成器迭代器

上面了解了迭代器,我们回到生成器中。生成器与迭代器有什么关系呢?当你执行’一个生成器,就得到了一个迭代器:

function *foo(){....};
var it=foo();//it就是一个迭代器

可以通过生成器实现前面的这个something无限数字序列生产者,类似这样:

  function *foo(){
    var nextVal;
    while(true){
      if(nextVal===undefined){
        nextVal=1;
      }else{
        nextVal=(3*nextVal)+6;
      }
      yield nextVal;
    }
  }

因为生成器会在每个yield处暂停,函数*something()的状态(作用域)会被保持,即意味着不需要闭包在调用之间保持变量状态。

停止生成器
在之前的例子中,看起来似乎*something()生成器的迭代器示例在循环中的break调用之后就永远留在了挂起状态。
其实有个隐藏的特性会帮你管理此事。for…of循环的“异常结束”,通常由break、return、或者未捕获异常引起,会向生成器的迭代器发送一个信号使其中止。

如果在生成器内有try…finally语句,它总是运行,即使生成器已经外部结束。

function *foo(){
 try{
    var nextVal;
    while(true){
      if(nextVal===undefined){
        nextVal=1;
      }else{
        nextVal=(3*nextVal)+6;
      }
      yield nextVal;
    }
   ]
   finally{
   	console.log("finally")
   }
  }

之前的循环的break会触发finally语句。

for(var v of something){
     console.log(v);
     if(v>500){
       break;
     }
   }

但是,也可以在外部通过return(…)手工终止生成器的迭代器实例:

        
function *foo(){
  try{
    var nextVal;
    while(true){
      if(nextVal===undefined){
        nextVal=1;
      }else{
        nextVal=(3*nextVal)+6;
      }
      yield nextVal;
    }
  }
  finally{
    console.log("clean up");
  }
}

   var it=foo();
    for(var v of it){
      console.log(v);
      if(v>500){
        console.log(
          it.return().value="Hello World"
        )
      }
    }
//1,9,33,105,321,969
//clean up
//Hello World

//it.return()是返回一个对象{done:true,value:"Hello World"},done:true所以才会停止生成器

调用it.return(…)之后,它会立即终止生成器,这当然会运行finally语句。另外,它还会把返回的value设置为传入return()的内容,这也就是"Hello World"被传出去的过程。现在我们也不需要包含break语句的,因为生成器的迭代器已经被设置为done:true,所以for…of循环会下一个迭代终止。

1.4 异步迭代生成器

我们来看下面这个代码:

function foo(x,y,cb){
   	ajax("http://some.url.1/x="+x+"&y"+y,cb);//假ajax是一个异步请求函数,请求成功回调cb函数处理
 }

 foo(11,31,function(err,text){
   if(err){
     console.error(err);
   }else{
     console.log(text);
   }
 })

那我们如何用生成器实现上述功能呢?

function foo(x,y){
  ajax(
    "http://some.url.1?x="+x+"&y"+y,function(err,text){
      if(err){
        it.throw(err);
      }else{
        it.next(text);
      }
    }
  )}

  function *main(){
    try{
      var text=yield foo(11,31);
      console.log(text);
    }
    catch(err){
      console.error(err);
    }
  }

var it=main();
it.next();

我们看上去,与上面的回调函数实现该功能的代码长度对比起来,这段代码更长,可能也复杂一些,但是,不要被表面现象欺骗了,生成器作为ES6推出的新特性实际要好得多。

分析代码:

  1. 看最重要代码:var text=yield foo(11,31);console.log(text;)我们调用了一个普通函数foo(…),而且显然能够从ajax()调用中得到text,即使它是异步的。

与下面代码对比:

var data=ajax("...");
console.log(data);//undefined

这段代码是在打印时无法获得data值,而上一段代码可以。区别就在于生成器中使用的yield。这就是奥秘所在,正是这一点使得我们看似阻塞同步的代码,实际上并不会阻塞整个程序,它只是暂停或阻塞了生成器本身的代码。
上一段代码的yield不仅只是阻塞了生成器的代码,还实现了消息传递,在生成器在yield处暂停,本质上就是在提出一个问题:“我应该返回什么值来赋值给text?”,谁来回答这个问题?

看一下foo()。如果ajax请求成功,我们调用it.next(data);这会响应数据恢复生成器,意味着我们暂停的yield表达试直接接收到了这个值。然后随着生成器代码继续运行,这个值被赋值给局部变量text。

回头一看,我们在生成器内部有了看似同步的代码,但隐藏在背后的是,在foo()内的运行完全可以异步。

生成器实现同步错误处理

前面我们已经得知生成器给我们带来了很多好处。让我们再看看生成器内部的try…catch:

 try{
      var text=yield foo(11,31);
      console.log(text);
    }
 catch(err){
      console.error(err);
    }

这是如何工作的?我们使用回调函数或Promise实现异步时使用try…catch不是无法捕获异步错误吗?
我们已经看到yield是如何让赋值语句暂停来等待foo(…)完成,使得响应完成后可以被赋给text,精彩部分在于yield暂停也使得生成器能够捕获错误。通过这段代码把错误抛出去给生成器:

if(err){
	//向*main()抛出一个错误
	it.throw(err);
}

生成器yield暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误。我们可以把错误抛入生成器中。

那当我们从生成器向外抛出错误呢?

 function *main(){
     var x=yield "Hello World";
     yield x.toLowerCase();//在这里引发一个异常,传入的x=42,数字没有该内置函数,所以报错
   }
   
   var it=main();
   console.log(it.next().value);//"Hello World"
   try{
     it.next(42);
   }
   catch(err){
     console.error(err);//TypeError
   }

可以看到,由生成器抛出来的错误可以在迭代器中正常捕获。
当然,我们也可以通过throw(…)给生成器抛入一个错误,基本上就是给生成器一个处理它的机会;如果没有处理的话,迭代器代码就必须处理:

 function *main(){
     var x=yield "Hello World";
     console.log(x);
   }
   
   var it=main();
   it.next();
   try{
     it.throw("hhh");
   }
   catch(err){
     console.error(err);//hhhh,在生成器没有处理,因此我来处理
   }

在异步代码中实现看似同步的错误处理(通过try…catch)在可读性和合理性方面都是一个巨大的进步。

1.5 生成器+Promise

看一个ajax例子,基于Promise的实现方法:

  function foo(x,y){
    return request("http://url1");
  }
  
  foo(11,31);
  .then(
    function(text){
      console.log(text);
    },
    function(err){
      console.error(err);
    })

那我们如何使用promise与生成器结合呢?

  1. 上面代码支持Promise的foo()在发出request请求之后返回一个promise。这暗示我们可以通过foo()构造一个promise,然后通过生成器把它yield出来,然后迭代器控制代码就可以接收到这个promise了。
  2. 那迭代器应该对promise做些什么呢?它应该侦听这个promise的决议(完成或拒绝),然后要么使用完成消息恢复生成器的运行,要么向生成器抛出一个带有拒绝原因的错误。

小结:强调一遍,获得Promise和生成器最大效用的最自然方法就是yield出来一个promise,然后通过这个promise来控制生成器的迭代器。

先来试试?

    function foo(x,y){
      return request("http://some.url");
    }
    
    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);
      })

看到了吧,从yield出来一个promise,然后通过迭代器来处理promise。

1.6支持promise的Generator Runner

看到上面的代码,你是否会提出这样一个问题?那我要在生成器中yield出多个promise,岂不是得编写不同的Promise链来处理吗?有没有一种方法可以实现“重复迭代控制”,每次会生成一个promise,等其决议后再继续。

看某个Promise库提供的工具:

function run(gen){
  var args=[].slice.call(arguments,1);
  var it=gen.apply(this,args);//在当前上下文中初始化生成器
  return Promise.resolve()
          .then(
            function handleNext(value){
                var next=it.next(value);
                return (function handleResult(next){
                          if(next.done){ //生成器运行完毕了吗?结束
                              return next.value;
                          }else{//没结束,继续运行
                            return Promise.resolve(next.value)
                                    .then(
                                      handleNext,//成功就恢复异步循环,把决议的值发回生成器
                                      function handleErr(err){
                                      //如果value是被拒绝的promise,就把错误传回生成器进行错误处理
                                            return Promise.resolve(it.throw(err))
                                                      .then( handleResult );
                                      });
                          }
                })(next);//返回时立即执行,next是传入的参数
          });
}

正如上面所示,你可能不想写这么复杂的工具,并且也特别不希望为每个使用生成器都重复使用这段代码。所以,一个工具或库中的辅助函数绝对是必要的。尽管有提供的辅助函数,但还是建议你花费几分钟学习这段代码,以更好理解生成器+Promise协同运作模式。

我们如何使用上面的工具函数呢?

function *main(){
	//...
}
run(main);

就是这样,这样运行run(…)的方式,它会自动异步运行你传给它的生成器,直到结束。

1.7 生成器中的Promise并发

想像这样一个场景:你需要从不同的来源数据获取数据,然后把响应结合在一起以形成第三个请求,最终把最后一条响应打印出来。

function *foo(){
  var result=yield Promise.all([
    request("http://some.url1"),
    request("http://some.url2")
  ]);
  var [a,b]=result;
  var r=yield request("http://some.url30"+a+","+b);
  console.log(r);
}
run(foo);

分析:

  1. 使用了Promise.all()来实现了两个请求的并发。
  2. [a,b]是ES6的解析赋值,把var a=…var b=…赋值语句简化为var [a,b[=result。
  3. 最后使用了上面定义的run()来执行生成器。

你可能感兴趣的:(javaScript)