生如夏花之绚烂,死如秋叶之静美
学习总结 + 个人理解,巩固 + 方便查阅,大家愿意看的简单看看就好
在上一篇《你不知道的JS系列——深入理解Promise》,我们看到了 Promise 相对于回调表达程序异步所体现出来的优越性:顺序性和可信任性,但 Promise 就是最好了吗? Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。 那么有没有一种以顺序的、同步的来表达异步的方式,因为这更符合我们大脑的思考模式,此时不简单的生成器(Generator)登场了。直接说吧,ES6 中 最完美的世界 就是生成器(看似同步的异步代码)和 Promise(可信任可组合)的结合。我们平时应用最多的 async/await 就是它们的组合实现。 Tips: 生成器与遍历器具有不可分割的关系,要了解生成器,那么首先得需要了解遍历器。
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 函数(生成器)是 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表达式就是暂停标志。即调用next方法遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
注意:
return语句作为程序的终止,也就是会作为遍历器对象遍历结束的标志。遍历遇到return语句,会将它后面的表达式的值作为返回的对象的value属性值,如果遍历结束没有return语句,则返回对象的value属性值为undefined(程序结束默认就是return undefined;)yield表达式只能用在 Generator 函数里面yield表达式如果用在另一个表达式之中,必须放在圆括号里面yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号警告: 不要在 forEach、map、filter...等方法内,将回调函数声明为生成器函数,然后再使用yield表达式,因为它们规定了接收参数就是一个普通函数,不会接收生成器函数,最好的做法就是将外部函数声明为生成器函数,然后使用for循环(除了这里的原因外,大家也不要抵制写for循环,虽然它麻烦,但是它的效率却是最高的)
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
因为 Generator 函数就是遍历器生成函数,所以可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
默认调用 Iterator 接口(即Symbol.iterator方法)的场合:
解构赋值扩展运算符yield* 表达式任何接受数组作为参数的场合(Array.from()、Promise.all()、Promise.race()...)for...of循环内部调用的是数据结构的Symbol.iterator方法。 for...of循环可以使用的范围:
数组(Array)Set 和 Map 结构类数组对象(arguments对象、DOM NodeList 对象)Generator 函数返回的遍历器对象字符串(String)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方法,可以返回给定的值,并且终结遍历 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方法,用于在函数体外抛出错误,然后在 Generator 函数体内捕获。
注意:
如果 Generator 函数体内未捕获(try...catch),那么可以在外部捕获,如果外部也没有捕获,那么程序将报错,直接中断执行throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法throw方法被内部捕获以后,会附带执行一次next方法Generator 函数体内抛出的错误,也可以被函数体外的catch捕获Generator 函数体内抛出错误且没有被内部捕获,它就不会再执行下去了都是让 Generator 函数恢复执行,只是使用了不同的语句替换yield表达式:
next()是将yield表达式替换成一个值throw()是将yield表达式替换成一个throw语句return()是将yield表达式替换成一个return语句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'; } 复制代码一个简单的 🌰 :
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 等待所有异步的执行结果,利用并发来提高程序性能。
作者:伟大的兔神 链接:https://juejin.im/post/5ef5681d6fb9a07e847229ff