【ES6学习】— (2)异步编程Generator函数和Promise对象简介

这是ES6学习系列的第二篇笔记,总结了两种异步编程解决方案Generator函数和Promise对象的特性,结合下篇Async函数对ES6+的异步编程解决方案做一个总结。加深对JavaScript异步编程的认知。

一、Generator函数

1.是什么—Generator函数简介

Generator函数是ES6提供的一种异步编程解决方案。
从语法上,可以理解为一个状态机,里面封装了多个内部状态;
从执行上,Generator函数返回一个遍历器对象,因此可以视为一个遍历器对象生成函数,返回的遍历器对象可以遍历Generator函数内部的每一个状态;
从形式上,Generator函数就一个普通函数,但是有两个特性,一是function命令与函数名之间有一个星号*;二是函数体内部使用yield语句定义不同的内部状态;
另外既然是异步编程解决方案,从异步编程的角度来看,可以理解为内部具有多个异步操作,每个异步操作用yield语句标记,通过next方法分步执行。

代码示例:

function* hellGenerator(){
  yield 'hello';
  yield 'world';
  return 'ending';
}
//调用之后返回的是一个遍历器对象而不是已执行了函数
var hw = hellGenerator();//注意这里函数内部并没有执行
//只有调用遍历器对象的next函数才会继续执行,使得指针移向下一个状态
//每次调用next函数内部指针从函数头部或上一次停下来的地方开始执行,直到遇到下一条yield或者return语句为止

//开始执行直到遇到第一个field为止;{value:'hello', done:false}value表示当前yield语句或者return的值,done表示遍历是否已经结束
hw.next();
//从上次field停下的地方开始执行,一直执行到下一个yield;{value:'world', done:'false}
hw.next();
//从上次field执行一直到return语句;{value:'ending', done:true}
hw.hext();
//上面Generator函数已经执行完毕,next执行返回的对象value属性为undefined,done为true,以后每次执行next都返回这个值。
hw.next();

简要总结下Generator函数的运行:调用Generator函数得到一个代表Generator函数的内部指针的遍历器对象,每调用该对象的next方法,都会返回一个有value和done属性的对象,value属性表示当前内部状态的值,是yield属性后面表达式的值,done属性是一个布尔值,表示当前遍历是否结束。

2.有什么 — Generator函数特性介绍

1.yield语句

Generator函数返回的遍历器对象只有调用next方法才会遍历下一个状态,其实是提供了一种可暂停执行的函数,yield就是暂停标志。其next运行逻辑如下:
1. 遇到yield语句后停止执行,将yield后面表达式的值作为返回对象的value值
2. 在调用next方法时继续往下执行,直到遇到下一条yield语句
3. 如果没有遇到yield,则一直执行到函数结束直到return语句为止,将return后面的表达式的值作为返回对象的value值
4. 如果没有return语句则返回对象的value属性值为undefined

yield语句的几点注意:

  • yield语句后面的表达式只有调用next方法时才会执行,等于为JavaScript提供了惰性求值的语法功能
  • return语句只会执行一次返回一个值。yield译为’生成’,可以执行多次使得Generator函数返回一系列值,从某一角度可以认为是yield作为生产器为Generator函数生成了一系列值
  • Generator可以不用yield语句,这样就变成了一个暂缓执行的函数,只有在调用next时执行
  • yield语句不能在普通函数中使用,包括forEach方法
  • yield语句如果用在一个表达式中则必须放在圆括号里面,如果用在函数参数或者赋值表达式的右边则不需要
  • yield后面表达式的值是作为value的属性值返回的,其语句本身没有返回值,或者说总是返回undefined

function* gen(){
  let num1 = 1;
  let num2 = 2;
  let a = 1 + yield ( num1+ num2);
  console.log(a);//undefined  调用next方法会返回求和后的值3,但是表达式中其实是undefined,所以1+undefined得到的a的值也是undefined
}

2.next方法参数

上面我们提到yield语句总是返回undefined,next方法可以带一个参数,该参数会被当做上一条yield语句的返回值。通过next参数我们可以在Generator函数运行的不同阶段从外部向内部注入不同的值从而调整函数的行为。
代码示例:

function* dataConsumer(){
  console.log('started');
  console.log('1.${yield}');
  console.log('2.${yield}');
  return 'result';
}
let obj = dataConsumer();
obj.next();//started
obj.next('a');//1.a
obj.next('b');//2.b

3.for…of循环

for…of循环可以自动的遍历Generator函数且此时不需要再调用next方法。当返回对象的的done属性为true时for…of则结束循环且不包含该返回对象,因此return语句返回的结果不会被for…of遍历到。实际上,for…of循环、扩展运算符(…)、解构赋值和Array.from()方法内部都是使用的遍历器接口,因此他们都可以将Generator函数的返回值遍历器对象作为参数。
代码示例:

function* foo(){
  yield 1;
  yield 2;
  yield 3;
  return 4;
  for(let num of foo()){
    console.log(num);//1 2 3
  }
}

function* f1(){
  yield 1;
  yield 2;
  return 4;
  yield 3;

  [...f1()];//[1, 2]
  let [x, y] = f1();//[1, 2]
  Array.from(f1());//[1, 2]
}

原生的JavaScript对象没有遍历接口,通过for…of可以为其提供遍历接口:

//方式一:获取其所有key的遍历设置yield值
function* enery1(obj){
  let keys = Reflect.ownKeys(obj);

  for(let propKey of keys){
    yield [propKey, obj[propKey]];
  }
}
let obj1 = {first:'1', second:'2'};
for(let [k, v] of enery1(obj1)){
  console.log('${k}', '${v}');//first:1, second:2
}

//方式二:将Generator函数加到对象的Symbol.iterator属性上
function* enery2(){
  let propKeys = Object.keys(this);
  for(let key of propKeys){
    yield [key, this[key]];
  }
}

let obj2 = {first:'1', second:'2'};
obj2[Symbol.iterator] = enery2;
for(let [k, v] of obj2){
  console.log('${k}', '${v}');//first:1, second:2
}

4.throw与ruturn方法

Generator.prototype.throw()

Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出异常,然后在函数体内捕获
几点注意:

  • 如果Generator函数内部没有部署try-catch,那么throw方法抛出的错误将被外界的try-catch捕获
  • 如果Generator函数内部没有部署try-catch,那么throw方法抛出错误后将直接终止遍历,如果是用全局的throw命令则不影响遍历。如果内部部署了try-catch也不会影响下一次遍历
  • Generator内部抛出的错误可以被函数体外的catch捕获,一旦Generator函数执行过程中抛出错误则立即终止,视为已经执行结束

Generator.prototype.return()

Generator函数返回的遍历器对象有一个return方法,可以返回给定的值并终结Generator函数的遍历

几点注意:

  • 调用return方法后,如果有参数,那么返回值的value属性值就是return方法的参数,如果没有则为undefined
  • 如果Generator函数中有try-finally代码块,那么return方法会推迟到finally代码块执行完在执行

代码示例:

function* numbers(){
  yield 1;
  try{
    yield 2;
    yield 3;
  }finally{
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next();//{done:false, value:1}
g,next();//{done:false, value:2}
g.return(7);//{done:false, value:4}
g.next();//{done:false, value:5}
g.next();//{done:true, value:7}

5.yield*语句

在一个Generator语句内部调用另一个Generator函数是无效的,需要使用yield*语句。简单来说就是如果yield语句后面跟的是一个遍历器对象,那么需要在yield命令后面加上,表明返回的是一个遍历器对象。本质上yield不过是for…of的简写形式,因此任何数据接口只要有Iterator接口就可以用yield*遍历。yield语句等同于在Generator函数内部部署一个for…of循环。
代码示例:

function* foo(){
  yield 'a';
  yield 'b';
}
function* bar(){
  yield 'x';
  foo();
  yield:'y';
}
for(let v of bar){
  console.log(v);//x y
}
function* bar(){
  yield 'x';
  yield* foo();
  yield:'y';
}
//加上yield*语句等同于在Generator内部加了一个for...of循环
function* bar(){
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

function* bar(){
  yield 'a';
  for(let v of foo){
    console.log(v);
  }
  yield 'b';
}

当内部调用一个Generator函数时,如果不用yield语句标识,那么value值将返回一个遍历器对象,如果使用了yield语句那么返回的是其内部值。

3.能做什么,怎么做 — Generator用途举例

异步操作的同步化表达

以前异步操作往往通过回调函数来实现,因为Generator函数可以暂停操作,因此现在通过Generator函数可以将异步操作写在yield语句中,然后将异步操作完成后的操作写在yield语句的下面等调用next方法时在执行。
可以把异步操作写在yield语句里面,等到调用next方法时在向后执行。异步操作的后续操作可以放在yield语句下面,这样可以不需要写回调函数。

//比如我们从服务端加载数据后需要刷新界面的操作,用回调的方式写法如下:
function loadDataFromWeb(url, function(data){ refreshUI(data); });
  //采用Generator函数如下
function* loadData(){
  let data = yield loadDataFromWeb(url);
  refreshUI(data);
}
var loader = loadData();
//加载数据
loader.next().value;//第一次next,从服务端加载数据
//刷新界面
loader.next();//第二次调用next,刷新界面

控制流管理

如果一个多步操作非常耗时,使用回调函数会使代码显得混乱,用Generator可以改善代码运行流程,另外通过Promise对象也可以实现逻辑以及代码上简洁的链式调用,但是因为加入了大量的Promise语法从代码可读性而言不如Generator函数清晰简洁。

//将多步回调操作转为yield语句表示,然后可以通过自动执行的方式使其按步骤运行
function* longRunningTask(){
  try{
    var value1 = yield step1();
    var value2 = yield step1(value1);
    var value3 = yield step1(value2);
    var value4 = yield step1(value3);
    //do something with value4
  }catch(e){
  }
}

二、Promise对象

1.Promise对象简介

Promise是一个用来传递异步操作的消息对象,代表了某个在未来才会知道结果的事件,并且这个事件提供了统一的API可供进一步处理。
Promise有如下特点:

  • Promise对象有三种状态:Pending(进行中)、Resolved(已完成)和Rejected(已失败)。其状态不受外界影响,只有异步操作可以决定当前是哪一种状态,其他任何操作都无法改变,
  • 状态一旦改变就不会再次改变。Promise的状态改变只有两种可能:从Pending变为Resolved和从Pending变为Rejected.
  • 无法取消Promise,一旦新建会立即执行,无法中途取消
  • 如果不设置回调函数,那么Promise内部抛出的错误不会反应到外部

2.Promise属性与方法简介

(1)reslove与reject处理

ES6中,Promise对象是一个构造函数,用来生成Promise示例

var promise = new Promise(function(resolve, reject){
  if(/*调用成功*/){
      resolve(data);
  }else{
      reject(error);
  }
  });

Promise构造函数接收一个函数作为参数,该函数有两个参数:resolve和reject。它们是由JavaScript引擎提供的两个函数,不需要自己部署。

resolve与reject函数的作用

  • resolve函数在异步操作成功时调用,用来将Promise对象的状态从Pending(未完成)变为Resolved(已完成),并将异步操作的结果作为参数传递出去。
  • reject函数在异步操作失败时调用,用来将Promise状态从Pending(未完成)变为Rejected(失败),并将异步操作报出的错误作为参数传递出去。

Promise实例创建后可以通过then函数指定完成和失败时的回调函数。then函数可以传递两个参数,第一个为操作成功即Promise实例状态从Pending变为Resolved时调用。第二个为操作失败即Promise实例状态从Pending变为Rejected时调用。其中第二个参数是可选的。两者都接收Promise实例传出的值作为参数。

promise.then(function(value){
  //成功时执行操作
  },function(error){
    //失败时执行操作
    });
(2)then方法

then方法是定义在原型对象Promise.prototype上的,其作用是为Promise实例添加状态改变时的回调函数。其返回的也是一个Promise实例因此可以实现链式调用,完成一组按次序调用函数的执行

getData().then(function(value){
  return map(value);
  }).then(function(value){
    console.log(value)
    });
    //如果结合ES6的箭头函数可以使代码更加的简化
    getData()
    .then(value => map(value))
    .then(value => console.log(value));

这里作个插曲,看到上面的代码熟悉RxJava的同学是不是感到似曾相识。在Android里面RxJava结合Retrofit访问网络的代码和这里神似,加上Lambda表达式的代码看起来简直不要太像,如果换成method reference方法引用代码会更加的短小精悍。当然个人感觉Lambda表达式和方法引用会降低代码的可理解性,自己玩玩还好,不建议在项目中大量使用。

//Retrofit+RxJava+Lambda
ApiService.getGankApi().getHistoryDate()
               .map(gankDate -> gankDate.getLastDate())
               .flatMap(calendar -> getGankDayData(calendar))
               .map(dayData -> dayData.gankDayDataToGankItem())
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(observer);

//Retrofit+RxJava+Method Reference
ApiService.getGankApi().getHistoryDate()
             .map(GankDate::getLastDate)
             .flatMap(this::getGankDayData)
             .map(GankDayData::gankDayDataToGankItem)
             .subscribeOn(Schedulers.io())
             .observeOn(AndroidSchedulers.mainThread())
             .subscribe(observer);
(3)catch方法

catch方法用来指定发生错误时的回调函数,相当于then(null, rejection)。虽然then可以同时定义成功和失败两种状态的回调函数,但一般来说不建议在then中定义Rejected状态的回调函数,而应该使用catch函数。

//不建议
promise
  .then(function(data){
    //Resolved
  }, function(err){
    //Rejected
    });
  //建议
  promise
    .then(function(){
      //Resolved
      })
    .catch(function(err){
      //Rejected
      });

catch方法的几点注意

  • Promise对象的错误具有冒泡性质,会一直向后传递直到被捕获为止。
  • 如果没有指定catch方法,那Promise对象抛出的错误不会传递到外层代码,发生错误时将没有任何反应
  • 当状态变为Resolved后再抛出错误是无效的
  • catch方法返回的还是Promise对象,因此可以继续调用then方法。但在catch后的then方法抛出的错误不会被前面的catch方法捕获
(4)Promise.all()与Promise.race()方法

这两个方法都是用来将多个Promise实例包装为一个Pormise实例.
示例代码:

var p = Promise.all([p1,p2,p3]);
var p = Promise.race([p1,p2,p3]);
//使用示例
var promises = [p1,p2,p3];
Promise.race(promises).then().catch();
Promise.all(promises).then().catch();

两个方法的区别在于:

  • 对于all方法,只有p1,p2,p3的状态都变为Resolved,p的状态才会变成Resolved。此时p1,p2,p3的返回值组成一个数组返回给p的回调函数。p1,p2,p3只要有一个状态变为Rejected,p的状态就会变成Rejected,第一个Rejected的实例的返回值会传递给p的回调函数
  • 对于race方法,p1,p2,p3只要有一个状态变为Resolved,p的状态就会变为Resolved,首先改变状态的Promise实例的返回值会传递给p的回调函数。
    两个方法的参数如果不是Promise实例就会先调用Promise.resolve方法将其转化为Promise实例。
(4)Promise.resolve()与Promise.reject()方法

Promise.resolve()可以将现有对象转换为新的Promise实例。如果方法参数不具有then方法(即不是thenable对象),则该方法实例的状态为Resolved,如果设置了回调函数则其回调函数会立即执行。
Promise.resolve()方法允许不带参数,因此可以通过Promise.resolve()方法方便的得到一个Promise对象。

var p = Promise.resolve("abcde");
p.then(function(s){
  console.log(s);
  })
  //快速得到一个Promise实例
  var pro = Promise.resolve();

Promise.reject()方法返回一个状态为Rejected的Promise实例。其方法参数会传递给实例的回调函数。

(5)done()与finally()方法

then()和catch()方法处理调用链尾端时如果出现错误将无法被捕捉,done()方法主要用来解决该问题,done()方法处于调用链的尾端用来保证向全局抛出任何可能的错误。finally方法接受一个普通的回调函数作为参数,该函数无论如何都会执行。

funcA().then().then().catch().done();

funcB().then().finally((s) => console.log(s))

你可能感兴趣的:(JavaScript,异步)