生如夏花之绚烂,死如秋叶之静美
说明
学习总结 + 个人理解,巩固 + 方便查阅,大家愿意看的简单看看就好
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方法依次遍历,每次遍历会返回一个有着value和done两个属性的对象。
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运算符优先级很低)yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
警告: 不要在 forEach、map、filter...等方法内,将回调函数声明为生成器函数,然后再使用yield表达式,因为它们规定了接收参数就是一个普通函数,不会接收生成器函数,最好的做法就是将外部函数声明为生成器函数,然后使用for循环(除了这里的原因外,大家也不要抵制写for循环,虽然它麻烦,但是它的效率却是最高的)
Symbol.iterator 与 for...of 循环
ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
因为 Generator 函数就是遍历器生成函数,所以可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
默认调用 Iterator 接口(即Symbol.iterator方法)的场合:
- 解构赋值 (部分或完全消耗迭代器)
- 扩展运算符(
spread运算符...完全消耗迭代器) yield*表达式- 任何接受数组作为参数的场合(
Array.from()、Promise.all()、Promise.race()...)
for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围:
- 数组(
Array) Set和Map结构- 类数组对象(
arguments对象、DOM NodeList对象) Generator函数返回的遍历器对象- 字符串(
String)
next 方法
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数(其实可以带多个参数),该参数就会被当作上一个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语句
补充一个用自定义迭代器来产生无限斐波纳契序列的例子:
var Fib = {
[Symbol.iterator]() {
var n1 = 1, n2 = 1;
return {
// 使迭代器成为iterable
[Symbol.iterator]() { return this; },
next() {
var current = n2;
n2 = n1;
n1 = n1 + current;
return { value: current, done: false };
},
return(v) {
console.log("Fibonacci sequence abandoned.");
return { value: v, done: true };
}
};
}
};
for (var v of Fib) {
console.log( v );
if (v > 50) break;
}
// 1 1 2 3 5 8 13 21 34 55
// Fibonacci sequence abandoned.
生成器委托—— 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 等待所有异步的执行结果,利用并发来提高程序性能。