从 生成器 到 promise+async

本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识

1、认识生成器

思考如下代码:

1 let x = 1 2 function foo() { 3 x++ 4 bar() 5 console.log(x) // 3 6 } 7 function bar() { 8 x++ 9 } 10 foo()

如上代码,我们确定知道,运行foo函数时,bar函数一定也会在 x++后执行,于是得到 x = 3,那么有没有其他方式,能不能在 foo函数 x++后,先暂停一下,外部在执行下 bar 函数,然后再往下执行呢?(这其实是属于抢占线程的方式,js 并不支持)如下代码:

1 let x = 1 2 function *foo() { 3 x++ 4 console.log('第一次next执行到 yield处,暂停') 5 yield 6 console.log('第二次执行next') 7 console.log(x) // 3 8 } 9 function bar() { 10 x++ 11 } 12 let it = foo() 13 it.next() 14 bar() 15 it.next()

解释:

上面代码新增了几个可能没有见过的标志符 一个*号yield,*的作用是标识foo函数是一个生成器,当生成器函数执行时,遇到内部的 yield便会暂停函数执行,而是等待下一次的 恢复执行(next)

代码释义:

1// 并不会真的执行foo函数,而是构造 foo 函数的迭代器 iterator (it) 2let it = foo() 3// 这是第一次执行,内部会执行到 yield 便停止,等待下一次 next 再恢复执行 4it.next() 5在这期间便执行其他代码 bar() 6// 第二次执行,从上一次 yield 处恢复执行 7it.next()

上面的代码我们可以知道,生成器函数,可以实现让函数暂停和恢复执行,生成器函数先构造迭代器,然后通过迭代器上的 next 方法控制函数的执行

2、简单补充下迭代器

迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。(引用于 《你不知道的javascript中卷》),从上面例子中, 不断调用next方法来恢复函数执行应该能领悟到 一步步得到一系列值 这句话的意思。

迭代器会返回一个对象 {value: xxx, done: false} ,值 value 和当前迭代状态 done,会依据这个状态来判定是否结束迭代,es6 新增了 Symbol.iterator 来调用迭代,实际上部分 js 数据已经内置了 迭代方法,比如数组

1var a = [1,3,5,7,9]; 2for (var v of a) { 3 console.log( v ); 4} 5// 1 3 5 7 9 6for of 其实就自动使用迭代来完成的, 7而且在面对数组这样有限的集合时,最后会自动的 返回 done true 来停止迭代 8同时我们也可以使用 breakreturn 的方式来结束迭代

3、深入一点 生成器

上面我们只是简单了解了生成器和迭代相关的知识,接下来我们稍微深入了解下生成器 思考如下代码:

1 var z = 1 2 function *foo() { 3 var x = yield 2 4 z++ 5 var y = yield(x * z) 6 console.log(x,y,z) 7 } 8 var it = foo() 9 var y1 = it.next() 10 var y2 = it.next(6) 11 var y3 = it.next(8) 12 有点复杂了,接下来来分析下

上面的代码比刚刚的栗子复杂多了,不仅仅是简单控制程序的启动和暂停,而是多了消息的传递(输入,输出) yield 可以向外部传递值,可以理解为return ,同时也可以接收由外部 next( xxx ) 传递过来的值。

代码释义:

1 // 生成 foo 函数的迭代器 2 var it = foo() 3 / *第一次,执行到第一个 yield 处, 4 *此时 yield 2,表示向外界输出了一个25 *则 y1 = {value: 2, done: false} 6 */ 7 var y1 = it.next() 8 /** 9 *第二次,从第一个 yield 处恢复执行,同时 next(6),向内部输入 6 10 * x = 6 z++ ---> 2 11 * 遇到 yield(x * z) 停止,并且向外返回 6*2 = 12 12 * y2 = {value: 12, done: false} 13 **/ 14 var y2 = it.next(6) 15 /** 16 * 第三次,next(8) 则 内部 y = 8 17 **/ 18 var y3 = it.next(8) 19 所以最终 console.log(x,y,z) 6 8 2

总结下:

  1. 当第一次调用生成器 var it = foo() 会构造一个迭代器,但不会真的执行
  2. 第一次调用 it.next()时, 函数会执行到第一个 yield处,并将yield 后的值返回(相当于return),(注意:第一次调用的next() 传递的参数是无效的)
  3. 第二次调用next(xx),可以传递参数,参数将赋值到 第一次 yield 处,函数继续执行到 下一个yeild处停止,(下一个yield 也可以返回值),然后一直这样进行下去
  4. it.next() 都会返回一个对象 { value: xxx,done: boole },done 用来标识迭代是否结束,当没有返回值时 value 就是 undefined

4、生成器和异步

上面我们大体了解清楚了生成器的使用,现在我们来看看用生成器来控制异步

思考如下代码:

1 // 模拟一个 ajax 的请求函数 2 function ajax(x,y,time = 3000) { 3 setTimeout(() => { 4 it.next( x + y ) 5 },time) 6 } 7 function *mian() { 8 try { 9 let data = yield ajax(1,2) 10 console.log('ajax 结果',data) 11 } catch (error) { 12 console.log('错误',error) 13 } 14 15 } 16 let it = mian() 17 it.next()

在之前,可能会考虑使用回调的方式去解决这样的异步问题,现在我们可以通过生成器去控制,其实就是当异步结束的时候,通过构造的迭代器去调用,从而实现异步的控制。

因为 yield阻塞 mian 函数的执行,所以当等到异步结束时,再通过 it.next( x + y )重新启动 mian 函数,从而完成异步的控制。

生成器也可以抛出错误,**(it.throw)**也可以通过 throw 手动输出错误,使得我们可以使用 try catch 来捕捉同步错误。

5、生成器 和 Promise

1 之前写请求可能是这样的 2 function foo(x,y) { 3 return request( 4 "http://some.url.1/?x=" + x + "&y=" + y 5 ); 6 } 7 foo( 11, 31 ).then( 8 function(text){ 9 console.log( text ); 10 }, 11 function(err){ 12 console.error( err ); 13 } 14 );

foo 会返回一个 Promise,使得我们再去操作,当搭配 生成器时

1 function *main() { 2 try { 3 // 返回一个 Promise 4 var text = yield foo( 11, 31 ); 5 console.log( text ); 6 } 7 catch (err) { 8 console.error( err ); 9 } 10 } 11 var it = main() 12 var p = it.next().value 13 // Promise 有结果后,next 传入 结果值 14 p.then( 15 function(text){ 16 it.next( text ); 17 }, 18 function(err){ 19 it.throw( err ); 20 } 21 );

上面的代码可以做知道,我们可以直接 yield 一个 Promise,当Promise决议后,再输入结果值,在 es7之前,会通过一些 库取实现这样的封装,es7后新增了 async await 这样的语法

1 async function main() { 2 try { 3 var text = await foo( 11, 31 ); 4 console.log( text ); 5 } 6 catch (err) { 7 console.error( err ); 8 } 9 }

上面代码没有了 *,也不需要 yield 出 Promise,而是使用 await 等待他的决议。 如果你 await 了一个 Promise,async 函数就会自动获知要做什么,它会暂停这个函数(就像生成器一样),直到 Promise 决议。我们并没有在这段代码中展示这一点,但是调用一个像 main() 这样的 async 函数会自动返回一个 promise。在函数完全结束之后,这个 promise会决议。(引用于原文)

6、实现一个 async await

1 // 异步请求 2 function ajax(x,y,time = 3000) { 3 return new Promise((resolve) => { 4 setTimeout(() => { 5 resolve(x + y) 6 },time) 7 }) 8 } 9 // 其实结构和 async await 就很类似了 10 function *mian() { 11 try { 12 let data = yield ajax(5,6) 13 return data 14 } catch (error) { 15 console.log('错误', error) 16 } 17 } 18 19 run(mian).then((res) => { 20 console.log(res,'res') 21 }) 22 /** 23 * run 函数内部其实就是返回promise,循环调用生成器的方式来实现的 24 **/ 25 function run(gen) { 26 var args = [].slice.call(arguments, 1), it; 27 // 在当前上下文中初始化生成器 28 it = gen.apply(this, args); 29 // 返回一个promise用于生成器完成 30 return Promise.resolve().then(function handleNext(value) { 31 // 对下一个yield出的值运行 32 var next = it.next(value); 33 return (function handleResult(next) { 34 // 生成器运行完毕了吗? 35 if (next.done) { 36 return next.value; 37 } 38 // 否则继续运行 39 else { 40 return Promise.resolve(next.value).then( 41 // 成功就恢复异步循环,把决议的值发回生成器 42 handleNext, 43 // 如果value是被拒绝的 promise, 44 // 就把错误传回生成器进行出错处理 45 function handleErr(err) { 46 return Promise.resolve(it.throw(err)).then(handleResult); 47 } 48 ); 49 } 50 })(next); 51 }); 52 }

个人理解: async 和 await 其实就是于、把之前想要结合 生成器+promise 的方式包装了下,当async 标志的函数,内部 await 一个promise,内部就会暂停执行,当 promise有结果后(可以理解为有结果就 next( data ) 恢复函数执行),就将结果值返回了

想起以前,总有面试官问,你知道 async await 吗?我说,async 不就是一个标识符吗,表明这是一个异步函数,会自动将return 的值封装成 promise,当内部遇到 await 便会暂停函数执行,或许面试官想听的是 关于生成器方面的吧,如果有更好的回答,或者解释,不妨留言探讨。

7、后续补充

在前面我们知道了 async其实就是 Generator函数的语法糖,把 * 号换成了 async,把 yield 换成了 await,那么,这样有什么好处或者区别呢?

  1. async修饰符,使得和其他函数调用无差异,不需要调用next()方法,同时直接返回一个promise
  2. async和await语义更好,不像*和yield,同时 yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

对比下两种方式,就能体会到差异了

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

swap空间的增减方法

(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap