这是ES6学习系列的第二篇笔记,总结了两种异步编程解决方案Generator函数和Promise对象的特性,结合下篇Async函数对ES6+的异步编程解决方案做一个总结。加深对JavaScript异步编程的认知。
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属性是一个布尔值,表示当前遍历是否结束。
Generator函数返回的遍历器对象只有调用next方法才会遍历下一个状态,其实是提供了一种可暂停执行的函数,yield就是暂停标志。其next运行逻辑如下:
1. 遇到yield语句后停止执行,将yield后面表达式的值作为返回对象的value值
2. 在调用next方法时继续往下执行,直到遇到下一条yield语句
3. 如果没有遇到yield,则一直执行到函数结束直到return语句为止,将return后面的表达式的值作为返回对象的value值
4. 如果没有return语句则返回对象的value属性值为undefined
yield语句的几点注意:
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
}
上面我们提到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
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
}
Generator.prototype.throw()
Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出异常,然后在函数体内捕获
几点注意:
Generator.prototype.return()
Generator函数返回的遍历器对象有一个return方法,可以返回给定的值并终结Generator函数的遍历
几点注意:
代码示例:
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}
在一个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语句那么返回的是其内部值。
异步操作的同步化表达
以前异步操作往往通过回调函数来实现,因为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是一个用来传递异步操作的消息对象,代表了某个在未来才会知道结果的事件,并且这个事件提供了统一的API可供进一步处理。
Promise有如下特点:
ES6中,Promise对象是一个构造函数,用来生成Promise示例
var promise = new Promise(function(resolve, reject){
if(/*调用成功*/){
resolve(data);
}else{
reject(error);
}
});
Promise构造函数接收一个函数作为参数,该函数有两个参数:resolve和reject。它们是由JavaScript引擎提供的两个函数,不需要自己部署。
resolve与reject函数的作用
Promise实例创建后可以通过then函数指定完成和失败时的回调函数。then函数可以传递两个参数,第一个为操作成功即Promise实例状态从Pending变为Resolved时调用。第二个为操作失败即Promise实例状态从Pending变为Rejected时调用。其中第二个参数是可选的。两者都接收Promise实例传出的值作为参数。
promise.then(function(value){
//成功时执行操作
},function(error){
//失败时执行操作
});
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);
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实例包装为一个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();
两个方法的区别在于:
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实例。其方法参数会传递给实例的回调函数。
then()和catch()方法处理调用链尾端时如果出现错误将无法被捕捉,done()方法主要用来解决该问题,done()方法处于调用链的尾端用来保证向全局抛出任何可能的错误。finally方法接受一个普通的回调函数作为参数,该函数无论如何都会执行。
funcA().then().then().catch().done();
funcB().then().finally((s) => console.log(s))