Generator 基本概念和使用
初步理解 Generator
Generator
函数是 ES6
提供的一种异步编程解决方案,语法行为与传统函数完全不同。
对 Generator 的理解
状态机,内部封装了多个状态;
遍历器对象生成函数,执行Generator
会得到一个遍历器对象;
Generator 的语法特征
function
关键字和函数名之间有 *
;
内部可以使用yield
表达式;
// 这里的 one, two, three 可以看作该 Generator 函数内部封装的三个状态
function* oneTwoThreeGenerator () {
// 返回一个具有 next 方法的对象(也就是遍历器对象), next 可以访问 Generator 函数内部封装的状态
const oneTwoThreeGeneratorExec = oneTwoThreeGenerator ();
// Object [Generator] {}, 该对象具有 next 方法
console . log ( " oneTwoThreeGeneratorExec: " , oneTwoThreeGeneratorExec );
// 每一次 next, 都会返回当前指针指向的那个值, 然后封装成 --> { value: xxx, done: false / true }, done 表明当前是否遍历完, value 表示指针指向的那个状态
// 如果 done 为 true, 则表明当前指针已经指向 oneTwoThreeGenerator (状态机)最后的一个状态
// console.log('oneTwoThreeGeneratorExec.next(): ', oneTwoThreeGeneratorExec.next()) // { value: 'one', done: false }
// console.log('oneTwoThreeGeneratorExec.next(): ', oneTwoThreeGeneratorExec.next()) // { value: 'two', done: false }
// console.log('oneTwoThreeGeneratorExec.next(): ', oneTwoThreeGeneratorExec.next()) // { value: 'three', done: true }
// console.log('oneTwoThreeGeneratorExec.next(): ', oneTwoThreeGeneratorExec.next()) // { value: 'undefined', done: true }
yield 表达式
yield
表达式只能用在 Generator
函数中,并且在 Generator
函数的执行过程中,只要遇到 yield
表达式, Generator
函数就会将执行权交出去(也就是暂停执行)。
只有在遍历器对象调用next(), throw(), return()...
的时候,Generator
函数才会恢复执行。
yield 的特征
遇到yield
表达式Generator
函数暂停执行;
yield
表达式后面的表达式会作为next()
的返回值中的 value
返回出去,比如:
yield 1 ---> { value: 1, done: boolean }
;
yield
用在别的表达式中时,自己需要被()
包裹起来。有两种情况例外:
function foo ( param1 , param2 ) {
console . log ( param1 , param2 );
// const a = 2 + yield 1 // error
// console.log('b', 2 * yield 1 + 2) // error
const funcDemo4Exec = funcDemo4 ();
// 为什么这一次 next, foo 函数才执行, 这里可以思考一下
Generator 函数的执行流程
遇到yield
表达式Generator
函数暂停执行;
调用next()
方法恢复执行,然后重复第1
步;
当遇到return xxx
时,将{value: xxx, done: true}
返回出去之后,终止执行。
这里要注意的是,如果函数没有显式的return xxx
,也会有隐式的return undefined
在函数的结尾;
终止之后,后续所有的next()
的结果都是{value: undefined, done: true}
;
function foo ( param1 , param2 ) {
console . log ( param1 , param2 );
// const a = 2 + yield 1 // error
// console.log('b', 2 * yield 1 + 2) // error
const funcDemo4Exec = funcDemo4 ();
// { value: 1, done: false }, 指针指向第一个 yield 1 表达式, 此时这个 next 还不算经过 foo 函数, 所以 foo 还没有被执行
console . log ( " funcDemo4Exec.next(): " , funcDemo4Exec . next ());
// { value: 2, done: false }, 指针指向第二个 yield 2 表达式, 此时这个 next 还不算经过 foo 函数, 所以 foo 还没有被执行
console . log ( " funcDemo4Exec.next(): " , funcDemo4Exec . next ());
// 此时调用 next, 指针继续往下走, 去找暂停点( yield 或者 return ), 找到 yield 3, 此时 next 指向该 yield 表达式, 途中经过了 foo 函数, 因此 foo 被执行了 会输出 ( undefined, undefined ), 这里也表明 yield 表达式的默认值是 undefined
// 这里指针的走向是: yield 2 ---> yield 3
console . log ( " funcDemo4Exec.next(): " , funcDemo4Exec . next ());
// 这里在调用 next, 指针由 yield3 ---> return undefined(函数最后隐式返回), 途中经过了 console.log('c: ', c), 因此会有输出 ( 'c': undefined )
console . log ( " funcDemo4Exec.next(): " , funcDemo4Exec . next ());
* funcDemo4Exec.next(): { value: 1, done: false }
* funcDemo4Exec.next(): { value: 2, done: false }
* funcDemo4Exec.next(): { value: 3, done: false }
* funcDemo4Exec.next(): { value: undefined, done: true }
Generator 函数内部 return 和 yield 的区别
相同点:
两者后面的表达式的值都可以被封装成{ value: xxx, done: xxx }
返回出去作为 next
函数的返回值;
都是Generator
函数执行的暂停点;
不同点:
在Generator
函数的执行过程中:
遇到return
之后,该Generator
的执行状态就变为终止了,后面的语句都不会被执行到了(除非遇到try...finally
,具体请看 Generator.prototype.return ),反复调用next
,得到的只能是{done: true, value: undefined}
,也就是说同一个Generator
函数中只有一个return
会生效;
而遇到yield
表达式,Generator
函数的执行状态只是变为暂停,当你再次调用next
时,该函数还会继续往下执行,也就是说同一个Generator
函数中可以有多个yield
生效;
console . log ( " after return in Generator " );
const funcDemo2Exec = funcDemo2 ();
// { value: 1, done: false }
console . log ( " funcDemo2Exec.next(): " , funcDemo2Exec . next ());
// { value: 2, done: false }
console . log ( " funcDemo2Exec.next(): " , funcDemo2Exec . next ());
// { value: 3, done: true }, 这里执行到 return, done 直接变为 true, 后续的 next 将会一直返回 { value: undefined, done: true }
console . log ( " funcDemo2Exec.next(): " , funcDemo2Exec . next ());
// Generator 函数终止之后返回的值: { value: undefined, done: true }
console . log ( " funcDemo2Exec.next(): " , funcDemo2Exec . next ());
Generator 和 Interator 的关系
Symbol.iterator
是一个常量值,类型是 Symbol
类型。
下文中遍历器对象和迭代器对象指的是同一个意思。
常见的有四种操作会隐式的用到迭代:
Array.from(obj)
;
[...obj] ----> 扩展运算符
;
let [ a, b, c ] = obj ----> 解构赋值
;
for(let xxx of obj) {}
;
…
注意的是:
对象的展开:{...obj}
用到的不是迭代;
迭代会自动调用next
, 取出其中的next
返回值中的value
,一直到{done: true}
就终止,
并且忽略终止时的value(done 为 true 时, value 对应的值)
;
let obj = { name: " xzq " , age: 18 } ;
// 这里我们知道 obj 上没有 [Symbol.iterator] 对应的方法, 因此 直接 返回 空数组
// console.log('Array.from(obj): ', Array.from(obj)); // []
// obj[Symbol.iterator] = function* () {
// Iterator 遍历器对象是一个具有 next(必须得有), return(非必须), throw(非必须) 方法的对象,Generator 生成器函数得到的就是一个 Iterator 遍历器对象
// 也就是说 Generator 生成器函数可以看做得到 Iterator 遍历器对象的一种简化写法
obj [ Symbol . iterator ] = function () {
return nextIndex < arr . length
? { value: arr [ nextIndex ++ ] }
// console.log('Array.from(obj): ', Array.from(obj)); // [1, 2]
// console.log([...obj]) // [1, 2]
// let [ a, b ] = obj // a: 1, b: 2
Iterator 遍历器对象的[Symbol.iterator]函数返回值等于该遍历器对象
const demoFunc4Exec = demoFunc4 (); // demoFunc4Exec: 遍历器对象
const selfDemoFunc4Exec = demoFunc4Exec [ Symbol . iterator ](); // selfDemoFunc4Exec: 遍历器对象
console . log ( selfDemoFunc4Exec === demoFunc4Exec ); // true
// 这就意味着, 你同样可以迭代遍历器对象, 因为它同样具有[Symbol.iterator]
for ( let res of demoFunc4 ()) {
console . log ( res ); // 1, 2
for…of 和 Iterator 迭代器的关系
const demoFunc4Exec = demoFunc4 (); // demoFunc4Exec: 遍历器对象
const selfDemoFunc4Exec = demoFunc4Exec [ Symbol . iterator ](); // selfDemoFunc4Exec: 遍历器对象
console . log ( selfDemoFunc4Exec === demoFunc4Exec ); // true
// 这就意味着, 你同样可以迭代遍历器对象, 因为它同样具有[Symbol.iterator]
for ( let res of demoFunc4 ()) {
console . log ( res ); // 1, 2
next 方法的参数
首先我们知道,yield expression
后面的expression
会封装为{value: expression, done: boolean}
作为外界的next()
的返回值。
如果yield
后面没有表达式, 那么此时认为expresstion
为undefined
。
这就可以看作Generator
函数内部向外部通信的一种方式,那么是否有一种外部向内部通信的方式呢?
这里next(param)
传递的 param
,就是向内部传值的一种方式。
next 参数是如何传递的?
遍历器对象调用next(param)
时,Generator
会先将指针的起点(执行next(param)
移动指针之前)指向的 yield expression
整体替换成param
, 然后指针会由暂停到开始往下移动,然后执行代码,直到遇到yield, return, throw...
才暂停。
这里可以看到,当next(param)
第一次执行的时候,指针是由函数开始 ----> 第一个 yield expression
,也就是说指针的起点并不是yield expression
,那么当前没有可以替换的值,所以第一次next(param)
传入的参数会被是无效的(会被v8
引擎忽略)。
next
只能传递一个参数,如果同时传递多个参数,后面的参数会被忽略。
console . log ( " res1 " , res1 ); // res1 hello
console . log ( " res2 " , res2 ); // world
const demoFunc1Exec = demoFunc1 ();
// next(param) 中的 param 会替换指针起点的 yield 表达式(第一个 next 传递的参数是无效的)
// { value: 1, done: false }
console . log ( " demoFunc1Exec.next(): " , demoFunc1Exec . next ( " 无效的参数 " ));
// { value: 1, done: false } const res1 = 'hello'
console . log ( " demoFunc1Exec.next(): " , demoFunc1Exec . next ( " hello " ));
// yield 2 --> return undefined
// 这里 next 只能接收一个参数, 后面多出来的参数是无效的
// { value: 1, done: false } const res2 = 'world'
" demoFunc1Exec.next(): " ,
demoFunc1Exec . next ( " world " , " 无效的参数 " ),
demoFunc1Exec . next (); // { value: undefined, done: true }
next 调用后,Generator 内部是如何执行的
我们知道,当next
调用时,Generator
内部指针会移动,当指针移动到 yield expression
时,expression
会先执行,然后指针会暂停住。
请看下面的例子:
let end = yield console . log ( " next 后, 先执行 yield 后的表达式 " );
const demoFunc3Exec = demoFunc3 ();
// 函数开始 --> yield console.log('next 后, 先执行 yield 后的表达式')
// 执行指针结尾指向的 yield 后的表达式, 表达式的返回值可以通过 next() 获取到, 这里我们知道 console.log() 的返回值为 undefined
console . log ( demoFunc3Exec . next ()); // { value: undefined, done: false }
// next(true) 传递的参数为 true, 先将指针的起点(移动指针之前)指向的 yield console.log('next 后, 先执行 yield 后的表达式')替换为 true
// 然后指针开始向下移动, let end = true --> xxx
// 在指针移动的过程中, 执行了 let end = true, 然后 if(true) break, 那么就退出了循环
// 这里等于说指针走向为: yield --> return undefined
console . log ( demoFunc3Exec . next ( true )); // { value: undefined, done: true }
console . log ( demoFunc3Exec . next ()); // { value: undefined, done: true }
总结
这里next
执行过程是先将Generator
函数内部指针的起点(执行next(param)
移动指针之前指向的yield
)替换为next
传入的参数,然后再往下执行。
这里其实你也可以发现,只有当指针移动之后,上一行的 JS
语句才会执行。比如:let end = yield xxx
。
遇到yield, return, throw...
再暂停。
throw 方法
throw(param)
同样可以传参, 如果没有显式的传,那么此时param
被视作undefined
,然后 (throw param)
替换yield expression
整体。
throw()
什么情况有返回值,那就是此时Generator
函数的内部指针必须指向yield 或者 return
。
Generator 函数体内部的异常被外部捕获
// 内部没有 try catch, 有了未捕获的异常, Generator 函数直接终止
const demoFunc1Exec = demoFunc1 ();
// Generator 函数内部指针: 函数开始执行 --> yield 1
// 先替换 yield 1 变为 throw undefined, 然后会隐式调用 next(), 然后指针开始往下, 代码开始执行, 先发现内部没有 try...catch
// 异常往外冒泡, 被外部 try...catch 捕获
demoFunc1Exec . throw (); // 此时的 throw() 没有返回值, 因为此时的指针没有指向 yield 或者 return, 函数直接异常终止了
console . log ( " 外部捕获: " , e ); // ‘外部捕获’: undefined(因为 throw undefined)
Generator 函数体外部可以抛异常让其内部捕获
console . log ( " 内部捕获: " , e ); // 内部捕获: undefined
const demoFunc1Exec = demoFunc1 ();
// Generator 函数内部指针: 函数开始执行 --> yield 1
// 先替换 yield 1 变为 throw undefined, 然后会隐式调用 next(), 然后指针开始往下, 代码开始执行, 先发现内部有 try...catch
// 异常被内部 try...catch 捕获, 指针正常往下, 执行经过的 console.log(1), 直到遇到 yield 2 时停止, 然后指针此时指向 yield 2, 此时的 throw() 就有返回值了 { value: 2, done: false }
Generator 函数体内部的异常被内部捕获
console . log ( " 内部捕获: " , e ); // 内部捕获: outside generator error
throw " inner generator error " ;
const demoFunc1Exec = demoFunc1 ();
// Generator函数内部指针: 函数开始执行 --> yield 1
// 先替换 yield 1 变为 throw 'outside generator error', 然后隐式调用 next(), 然后指针开始往下, 代码开始执行, 先发现内部有 try...catch
// 异常被内部 try...catch 捕获, 指针正常往下, 执行经过的 catch 代码块里的 console.log('内部捕获: ', e), 然后执行: throw 'inner generator error'
// 抛出异常, 发现内部没有 try 捕获了, 因此 next 此时指向 throw 语句, 也就意味着 throw() 没有返回值了, generator 函数直接终止, 然后 done 直接被置为 true, 以后的 next() 返回值都为 { value: undefined, done: true }
// 最后你会发现内部未捕获的异常冒泡到外部, 被外部的 try 捕获
demoFunc1Exec . throw ( " outside generator error " );
console . log ( " 外部捕获 " , e ); // 外部捕获 inner generator error
console . log ( " demoFunc1Exec.next(): " , demoFunc1Exec . next ()); // {value: undefined, done: true}
console . log ( " demoFunc1Exec.next(): " , demoFunc1Exec . next ()); // {value: undefined, done: true}
console . log ( " demoFunc1Exec.next(): " , demoFunc1Exec . next ()); // {value: undefined, done: true}
如果多次调用 throw
console . log ( " 内部捕获: " , e ); // 内部捕获: undefined
const demoFunc1Exec = demoFunc1 ();
// Generator 函数内部指针: 函数开始执行 --> yield 1
// 先替换 yield 1 变为 throw undefined, 然后隐式调用 next(), 然后指针开始往下, 代码开始执行, 先发现内部有 try...catch
// 异常被内部 try...catch 捕获, 指针正常往下, 执行经过的 console.log(1), 直到遇到 yield 2 时停止
// 先替换 yield 2 为 throw undefined, 然后隐式调用 next(), 然后指针开始往下, 代码开始执行, 发现内部没有 try...catch 代码, 而 next 此时指向 throw 语句, 不能正常继续往下指向 return undefined, 因此此时的 throw() 没有返回值
// Generator 函数终止, 后面如果再次手动调用 next(), 返回值 { done: true, value: undefined }
// 异常往外冒泡, 外界也没有 try...catch, 发现未捕获的异常
也就是说,内部的try...catch
只能捕获外界的一个 generator.throw()
,这是因为throw()
还会隐式的调用next()
。
return 方法
首先来个简单的例子:
const demoFunc5Exec = demoFunc5 ();
// Generator 内部指针: 函数开始执行 --> yield 1
console . log ( " demoFunc5Exec.next(): " , demoFunc5Exec . next ()); // { value: 1, done: false }
// 此时指针指向 yield 1, 将其替换为 return 'return', 然后隐式调用 next(), 指针往下移动, 然后执行 let data1 = return 'return'
// 等于说指针走向为: yield1 --> return 'return', 那么此时 Generator 等于说运行完了
console . log ( ` demoFunc5Exec.return('return'): ` , demoFunc5Exec . return ( " return " )); // { value: 'return', done: true }
console . log ( " demoFunc5Exec.next(): " , demoFunc5Exec . next ()); // { value: undefined, done: true }
return
一般情况都会终止函数的执行,但是有个特殊情况,那就是遇到 try...finally
。这时,return
会延迟到 finally
中的代码执行完再执行。
console . log ( " try --- 1 " );
console . log ( " try --- 2 " );
console . log ( " 不会执行: try --- 3 " );
console . log ( " 会执行: finally " );
// 这里的输出结果是: try --- 1, try --- 2, 会执行: finally
所以接下来结合Generator.prototype.return
的示例你就明白了:
console . log ( " data1: " , data1 );
console . log ( " data2: " , data2 );
console . log ( " data3: " , data3 );
console . log ( " 不会被 return 所终止 " );
console . log ( " data4: " , data4 );
console . log ( " 会被 return 所终止 " );
console . log ( " data5: " , data5 );
const demoFunc5Exec = demoFunc5 ();
// Generator内部指针: 函数开始执行 --> yield 1
// 此时指针指向 yield 1, 将其替换为 return 'return', 然后隐式调用 next(), 指针往下移动, 然后执行 let data1 = return 'return',
// 那么暂时当前指针走向为 yield1 --> return 'return', 结合前文函数中遇到 return 时会先执行 finally 中的代码, 然后再执行 return
// 然后执行 console.log('不会被 return 所终止'), 当指针遇到 yield 4 时暂停
// 那么此时真正的指针走向为 yield1 --> yield 4
console . log ( ` demoFunc5Exec.return('return'): ` , demoFunc5Exec . return ( " return " )); // { value: 4, done: false }
// finally 执行完之后再返回去执行 return
// 因此指针走向为 yield 4 --> return 'return', 过程中会经过 console.log('data4: ', undefined)。
// 这里我们会发现, return 一直延迟到 finally 中的代码执行完之后, 才执行。
console . log ( " demoFunc5Exec.next(): " , demoFunc5Exec . next ()); // { value: 'return', done: true }
console . log ( " demoFunc5Exec.next(): " , demoFunc5Exec . next ()); // { value: undefined, done: true }
throw,return,next 总结
三者都是让Generator
恢复执行的操作,也就是可以理解为throw, return
都具有next
的操作;
三者都能通过传递参数的方式向Generator
内部传递一定的信息:
throw(error)
可以向内部传递error
;
return(value) 和 next(value)
可以向内部传递value
;
throw, return, next
三者可以理解为先替换yield expression
,而且总是先替换指针的起点(执行next(param)
移动指针之前)指向的yield expression
,然后再执行next
,指针才往下执行;
三者的返回值都是当前指针末尾指向(也就是调用 next()
指针移动之后的指向)的 yield xx, return xx ...
表达式的值 ({ value: xx, done: boolean }
);
yield* 表达式
当遇到嵌套Generator
函数的时候,外部没有一种很好的方式能够使得内部的Generator
进行迭代。
这个时候如果我们手动迭代就很麻烦, yield*
就是为了解决这种情况出现的。
function* insideFunc1 () {
function* outsideFunc1 () {
// 如果不在内部自己遍历的话, 这个内部的 遍历器 将永远 暂停执行
yield console . log ( " 外部1 " );
// 手动递归太麻烦, 而且会忽略 done 为 true 时的值, 也就是 return 的值
// 具体可以参考前面的章节 Generator 和 Interator 的关系
for ( let insideRes of insideFunc1 ()) {
console . log ( " insideRes: " , insideRes );
// 使用 yield* 更加优雅, 且不会忽略 done 为 true 时的值
// 这个内部 return 的值会作为 yield* insideFunc1() 的返回值拿出来
// 而上面那种手动递归的方式显然没有方法拿到这个内部 return 的值
const insideRetureVal = yield* insideFunc1 ();
yield console . log ( " 外部2 " );
return console . log ( " 外部终止 " );
const outsideFunc1Exec = outsideFunc1 ();
// 对于 yield* 的方式, 我们外部的 next 可以访问到内部
// outsideFunc1Exec.next()
// outsideFunc1Exec.next() // 这一步才执行 内部的 遍历
// outsideFunc1Exec.next() // { value: undefined, dont: true }
// outsideFunc1Exec.next()
yield* 和常规迭代方式的异同
我们知道常规的会触发迭代的方式有4
种:for...of, let [a, b] = arr, [...arr], Array.from()
。
它们的相同点:
都可以对部署了[Symbol.iterator]
接口(也就是 obj[Symbol.iterator] === Generator
)的进行迭代,yield* [], yield* Generator(); for(let res of []), for(let res of Generator())
;
它们的不同点:
yield*
不会忽略 { value: xxx, done: true }
,其中done
为true
时的value
。而后者会忽略;
yield*
不是自动迭代,而是在外部Generator
进行next
的时候,让这个next
可以执行到 yield*
后面的Generator
内部;
常规迭代方式可以自动迭代,而且获取到其中的value
;
function* innersideFunc4 () {
function* outsideFunc4 () {
const v = yield* innersideFunc4 ();
// 这里可以拿到内部 return 的值, 并且不会终止掉外部 Generator
// 等同于(后者会忽略 done 为 true 时的 value )
// for (const iterator of innersideFunc4()) {
const outsideFunc4Exec = outsideFunc4 ();
// 函数开始 --> yield '内部执行1'
// { value: '内部执行1', done: false }
console . log ( " outsideFunc4Exec.next(): " , outsideFunc4Exec . next ());
// yield '内部执行1' --> yield '内部执行2'
// { value: '内部执行2', done: false }
console . log ( " outsideFunc4Exec.next(): " , outsideFunc4Exec . next ());
// 这里需要注意的是,当遇见内部的 return 时不会终止掉外部的 Generator 函数,而是终止内部,然后指针跳到外部
// yield '内部执行2' --> return '内部执行终止'(它会作为 yield* 表达式的值) --> yield '外部执行1'
// 这个过程中会执行 console.log('v: ', '内部执行终止')
// { value: '外部执行1', done: false }
console . log ( " outsideFunc4Exec.next(): " , outsideFunc4Exec . next ());
// yield '外部执行1' --> return '外部执行终止'
// { value: '外部执行终止', done: true }
console . log ( " outsideFunc4Exec.next(): " , outsideFunc4Exec . next ());
// { value: 'undefined', done: true }
console . log ( " outsideFunc4Exec.next(): " , outsideFunc4Exec . next ());
Generator 结合异步操作
异步操作同步表达
yield Promise . resolve . then ( () => console . log ( 1 ));
yield Promise . resolve . then ( () => console . log ( 3 ));
const stepGenExec = stepGen ();
done = stepGenExec . next () . done ;
虽说能够同步表达,但是由于在外部的while
中是同步代码,而Promise.resolve().then
是异步代码,因此这里执行顺序有时候并不会如我们想象那样。
这里我们需要结合Thunk
或者Promise
来在合适的时候交回Generator
的执行权,也就是说我需要知道异步函数何时执行完。
而不是直接无脑直接next
。
结合 Promise 或者 Thunk 实现真正异步操作同步化
结合 Thunk 来实现
首先什么Thunk
呢?
在传统编程概念中, 对于函数的传参有两种情况:
传名调用(调用的时候再求值);
传参调用(先求值, 当作参数放进去);
const log = console . log . bind ( console );
function demo1 ( param1 , skip ) {
// 针对将 param1 改造成了 thunk 的形式
if ( typeof param1 === " function " ) {
// 对于 传名调用 是将 1 + 2 传进去, 然后调用 log() 时再求值
// 对于 传参调用 是先在外面求和, 然后将 3 传入进去
// 有些时候, 如果参数计算比较复杂, 这个时候类似于 按需加载 的传名调用效率肯定更高
const demo1Param1 = () =>
new Array ( 1000 ) . fill ( 100 ) . reduce ( ( prev , item ) => prev + item , 0 );
// 针对这种情况先求和, 在放入进去, 而且这个时候, 在函数中甚至还有可能没有用到该参数
// log(demo1Param1(), true)
// 这个时候, 出现了一种 thunk 的概念来解决这种情况, 它是一种将传名调用 --> 传参调用的一种方式
function demo1Thunk1 ( param1 ) {
// 将参数通过 thunk 函数包裹, 然后传入进去, 再需要的时候再 调用该函数 得到原本的参数
// log(demo1Thunk1(demo1Param1))
这里就看的出来,对于传参调用,有时候会造成性能上的损失,而传名调用,就可以避免这个问题,类似于按需加载。
而thunk
在其中就是用来 将传名调用 —> 传参调用的一种方式,多个参数被放到一个临时函数中,在需要的时候再调用这个临时函数,这个临时函数就是传统意义上的thunk
函数。
但是在JS
中用法和这常规用法又有所不同,在JS
中thunk
也是一个临时函数,只不过他其中放的不是多个参数,而是一个函数,它就是一个被改造了对原函数的传参方式的函数。
在JS
中,必须是参数中有回调的函数,才会被认为能够转化为thunk
,这点很特殊。
function transForm2Thunk ( fn ) {
// 第一次执行, 将其他的参数传入当前函数, 返回一个函数
return function ( ... args ) {
return fn . call ( this , ... args , cb )
const readFileThunk = transForm2Thunk ( fs . readFile )( path , {encoding: ' utf-8 ' ... options } )
readFile ( path , {encoding: ' utf-8 ' ... options }, cb )
接下来结合thunk
,来让Generator
能够实现异步操作同步表达:
// yield 后表达式的返回值会传到外界 --> { value: xx, done: false }
const data1 = yield readFileThunk ( demo1TxtPath , { encoding: " utf-8 " } );
console . log ( " data1: " , data1 );
const data2 = yield readFileThunk ( demo2TxtPath , { encoding: " utf-8 " } );
console . log ( " data2: " , data2 );
const data3 = yield readFileThunk ( demo3TxtPath , { encoding: " utf-8 " } );
console . log ( " data3: " , data3 );
const demoFunc2Exec = demoFunc2 ();
// 那么什么时候恢复执行比较好呢,那就是在异步函数执行完之后的回调函数里 next 显然是最好的
// 这个时候,内部的 yield 将 异步函数执行完之后的 回调函数 传出来, 我们在外界进行 next
demoFunc2Exec . next () . value ( ( err , data ) => {
demoFunc2Exec . next ( data ) . value ( ( err , data ) => {
demoFunc2Exec . next ( data ) . value ( ( err , data ) => {
demoFunc2Exec . next ( data );
为了解决前文中提出需要知道异步函数何时执行完的问题,这里巧妙的利用的 thunk
和 yield expression
会将表达式的返回值传递出来的特性。
这样就能得到一个参数为回调函数的函数,传入的回调函数执行的时候就是内部异步函数执行完的时刻。
这样我们就能在Generator
外部传入这个回调函数,在这个回调函数中进行next
,让暂停的Generator
在合适的时机(异步函数执行完的时候)恢复执行。
结合 Promise 来实现
通过上文结合thunk
实现的逻辑我们知道了Generator
异步操作同步表达的关键逻辑了:
那就是我们需要在Generator
内部通知到到外部何时异步函数执行完,并且提供给外界一个操作入口来调用 next
恢复Generator
的执行,从而达成异步操作同步化的效果。
const fsPromise = require ( " fs/promises " );
// 每一个 yield 表达式的后面都是一个运行结果 Promise 对象的表达式
const data1 = yield fsPromise . readFile ( demo1TxtPath , { encoding: " utf-8 " } );
console . log ( " data1: " , data1 );
const data2 = yield fsPromise . readFile ( demo2TxtPath , { encoding: " utf-8 " } );
console . log ( " data2: " , data2 );
const data3 = yield fsPromise . readFile ( demo3TxtPath , { encoding: " utf-8 " } );
console . log ( " data3: " , data3 );
const demoFunc3Exec = demoFunc3 ();
// 我们只有在 then 中 next(交回程序执行权), 让程序继续执行
// next() --> { value: Promise, done: xxx }
demoFunc3Exec . next () . value . then ( ( data ) => {
demoFunc3Exec . next ( data ) . value . then ( ( data ) => {
demoFunc3Exec . next ( data ) . value . then ( ( data ) => {
demoFunc3Exec . next ( data );
这里为啥不需要thunk
,因为fsPromise.readFile
自己返回的就是一个Promise
,我们可以在外界获取它,然后再其then
的时候,让暂停的Generator
恢复执行,这里思想其实和回调函数是一样的。
上文可以发现,如果不能自动执行Generator
,每次都需要自己手动调用,很麻烦,那么如何自动调用Generator
呢?
自动执行 Generator
自动执行的关键就是:在什么时候交出执行权,什么时候收回执行权。
只要能在交出执行权的时候(指针暂停),提供下一次收回执行权的操作(指针恢复执行),那么该Generator
就会自动执行下去。
Thunk 版
yield
后面只能是thunk
函数
function runByThunk ( generator ) {
const generatorExec = generator ();
// 对回调进行封装, 这样意味着回调的时候, 如果 done 不为 true, 就会自动 next 让 Generator 继续执行
const cb = function ( err , data ) {
// 第一次 next 的参数(data)会被忽略, 所以这里没影响
// 第一次 next 的时候, yield 表达式后面的异步函数开始执行了, 当其执行完之后会自动调用回调, 也就是 value(cb)
// 前文我们知道, 我们需要在 cb 中让 Generator 恢复执行, 因此我们将 cb 进行封装, 在其中加入 next(data) 的 操作,
// 顺便将异步执行的结果返回出去, 然后当 done 时, 表示 所有的 yield 执行完毕
const { done , value } = generatorExec . next ( data ) ;
// value 其实就是一个被 thunk 函数, 它是异步的, 它的参数是回调函数, 当异步函数执行完之后会调用传入的回调函数
// 这里就形成了一个递归逻辑, 只要异步函数执行完就会调用 cb, 然后 cb 又会重复这个步骤,直到 `Generator` 内部执行完 --> done: true
Promise 版
yield
后面只能是Promise
function runByPromise ( generator ) {
// 对回调进行封装, 这样意味着 then 的时候, 如果 done 不为true, 就会自动 next 让Generator继续执行
// value 必须是 Promise, 也就是 yield 表达式后面是 Promise
const { value , done } = g . next ( data );
// value 是一个 Pending 状态的 Promise, 在它状态发生改变时(异步操作执行完时)执行 cb
// 这里就形成了一个递归逻辑, 只要异步函数执行完就会调用 cb, 然后 cb 又会重复这个步骤,直到 `Generator` 内部执行完 --> done: true
参考链接
ES6-Generator
ES6-Generator-Async
Iterator 和 for…of 循环