异步过渡方案Generator
在使用 Generator
前,首先知道 Generator
是什么。
如果读者有 Python 开发经验,就会发现,无论是概念还是形式上,ES2015 中的 Generator
几乎就是 Python 中 Generator
的翻版。
Generator
本质上是一个函数,它最大的特点就是可以被中断,然后恢复执行。通常来说,当开发者调用一个函数之后,这个函数的执行就脱离了开发者的控制,只有函数执行完毕之后,控制权才能重新回到调用者手中,因此程序员在编写方法代码时,唯一
能够影响方法执行的只有预先定义的 return
关键字。
Promise
也是如此,我们也无法控制 Promise
的执行,新建一个 Promise
后,其状态自动转换为 pending
,同时开始执行,直到状态改变后我们才能进行下一步操作。
而 Generator
函数不同,Generator
函数可以由用户执行中断或者恢复执行的操作,Generator
中断后可以转去执行别的操作,然后再回过头从中断的地方恢复执行。
1. Generator 的使用
Generator
函数和普通函数在外表上最大的区别有两个:
- 在
function
关键字和方法名中间有个星号(*)。 - 方法体中使用
yield
关键字。
function* Generator() { yield "Hello World"; return "end"; }
和普通方法一样,Generator
可以定义成多种形式:
// 普通方法形式 function* generator() {} //函数表达式 const gen = function* generator() {} // 对象的属性方法 const obi = { * generator() { } }
Generator 函数的状态
yield
关键字用来定义函数执行的状态,在前面代码中,如果 Generator
中定义了 x
个 yield
关键字,那么就有 x + 1
种状态(+1是因为最后的 return
语句)。
2. Generator 函数的执行
跟普通函数相比,Generator
函数更像是一个类或者一种数据类型,以下面的代码为例,直接执行一个 Generator
会得到一个 Generator
对象,而不是执行方法体中的内容。
const gen = Generator();
按照通常的思路,gen
应该是 Generator()
函数的返回值,上面也提到Generator
函数可能有多种状态,读者可能会因此联想到 Promise
,一个 Promise
也可能有三种状态。不同的是 Promise
只能有一个确定的状态,而 Generator
对象会逐个经历所有的状态,直到 Generator
函数执行完毕。
当调用 Generator
函数之后,该函数并没有立刻执行,函数的返回结果也不是字符串,而是一个对象,可以将该对象理解为一个指针,指向 Generator
函数当前的状态。(为了便于说明,我们下面采用指针的说法)。
当 Generator
被调用后,指针指向方法体的开始行,当 next
方法调用后,该指针向下移动,方法也跟着向下执行,最后会停在第一个遇到的 yield
关键字前面,当再次调用 next
方法时,指针会继续移动到下一个 yield
关键字,直到运行到方法的最后一行,以下面代码为例,完整的执行代码如下:
function* Generator() { yield "Hello World"; return "end"; } const gen = Generator(); console.log(gen.next()); // { value: 'Hello World', done: false } console.log(gen.next()); // { value: 'end', done: true } console.log(gen.next()); // { value: undefined, done: true }
上面的代码一共调用了三次 next
方法,每次都返回一个包含执行信息的对象,包含一个表达式的值和一个标记执行状态的 flag
。
第一次调用 next
方法,遇到一个 yield
语句后停止,返回对象的 value
的值就是 yield
语句的值,done
属性用来标志 Generator
方法是否执行完毕。
第二次调用 next
方法,程序执行到 return
语句的位置,返回对象的 value
值即为 return
语句的值,如果没有 return
语句,则会一直执行到函数结束,value
值为 undefined
,done
属性值为 true
。
第三次调用 next
方法时,Generator
已经执行完毕,因此 value
的值为undefined
。
2.1 yield 关键字
yield
本意为 生产 ,在 Python、Java 以及 C# 中都有 yield
关键字,但只有Python 中 yield
的语义相似(理由前面也说了)。
当 next
方法被调用时,Generator
函数开始向下执行,遇到 yield
关键字时,会暂停当前操作,并且对 yield
后的表达式进行求值,无论 yield
后面表达式返回的是何种类型的值,yield
操作最后返回的都是一个对象,该对象有 value
和 done
两个属性。
value
很好理解,如果后面是一个基本类型,那么 value
的值就是对应的值,更为常见的是 yield
后面跟的是 Promise
对象。
done
属性表示当前 Generator
对象的状态,刚开始执行时 done
属性的值为false
,当 Generator
执行到最后一个 yield
或者 return
语句时,done
的值会变成 true
,表示 Generator
执行结束。
注意:yield关键字本身不产生返回值。例如下面的代码:
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: undefined, done: true }
为什么第二个 next
方法执行后,y
的值却是 undefined
。
实际上,我们可以做如下理解:next
方法的返回值是 yield
关键字后面表达式的值,而 yield
关键字本身可以视为一个不产生返回值的函数,因此 y
并没有被赋值。上面的例子中如果要计算 y
的值,可以将代码改成:
function* foo(x) { let y; yield y = x + 1; return 'end'; }
next
方法还可以接受一个数值作为参数,代表上一个 yield
求值的结果。
function* foo(x) { const y = yield(x + 1); return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next(10)); // { value: 10, done: true }
上面的代码等价于:
function* foo(x) { let y = yield(x + 1); y = 10; return y; } const gen = foo(5); console.log(gen.next()); // { value: 6, done: false } console.log(gen.next()); // { value: 10, done: true }
next
可以接收参数代表可以从外部传一个值到 Generator
函数内部,乍一看没有什么用处,实际上正是这个特性使得 Generator
可以用来组织异步方法,我们会在后面介绍。
2.2 next 方法与 Iterator 接口
一个 Iterator
同样使用 next
方法来遍历元素。由于 Generator
函数会返回一个对象,而该对象实现了一个 Iterator
接口,因此所有能够遍历 Iterator
接口的方法都可以用来执行 Generator
,例如 for/of
、aray.from()
等。
可以使用 for/of
循环的方式来执行 Generator
函数内的步骤,由于 for/of
本身就会调用 next
方法,因此不需要手动调用。
注意:循环会在 done
属性为 true
时停止,以下面的代码为例,最后的 'end'
并不会被打印出来,如果希望被打印,需要将最后的 return
改为 yield
。
function* Generator() { yield "Hello Node"; yield "From Lear" return "end" } const gen = Generator(); for (let i of gen) { console.log(i); } // 和 for/of 循环等价 console.log(Array.from(Generator()));;
前面提到过,直接打印 Generator
函数的示例没有结果,但既然 Generator
函数返回了一个遍历器,那么就应该具有 Symbol.iterator
属性。
console.log(gen[Symbol.iterator]);
// 输出:[Function: [Symbol.iterator]]
3. Generator 中的错误处理
Generator
函数的原型中定义了 throw
方法,用于抛出异常。
function* generator() { try { yield console.log("Hello"); } catch (e) { console.log(e); } yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 输出
// Hello
// throw error
// Node
上面代码中,执行完第一个 yield
操作后,Generator
对象抛出了异常,然后被函数体中 try/catch
捕获。当异常被捕获后,Generator
函数会继续向下执行,直到遇到下一个 yield
操作并输出 yield
表达式的值。
function* generator() { try { yield console.log("Hello World"); } catch (e) { console.log(e); } console.log('test'); yield console.log("Node"); return "end"; } const gen = generator(); gen.next(); gen.throw("throw error");
// 输出
// Hello World
// throw error
// test
// Node
如果 Generator
函数在执行的过程中出错,也可以在外部进行捕获。
function* generator() { yield console.log(undefined, undefined); return "end"; } const gen = generator(); try { gen.next(); } catch (e) { }
Generator
的原型对象还定义了 return()
方法,用来结束一个 Generator
函数的执行,这和函数内部的 return
关键字不是一个概念。
function* generator() { yield console.log('Hello World'); yield console.log('Hello 夏安'); return "end"; } const gen = generator(); gen.next(); // Hello World gen.return(); // return() 方法后面的 next 不会被执行 gen.next();
4. 用 Generator 组织异步方法
我们之所以可以使用 Generator
函数来处理异步任务,原因有二:
Generator
函数可以中断和恢复执行,这个特性由yield
关键字来实现。Generator
函数内外可以交换数据,这个特性由next
函数来实现。
概括一下 Generator
函数处理异步操作的核心思想:先将函数暂停在某处,然后拿到异步操作的结果,然后再把这个结果传到方法体内。
yield
关键字后面除了通常的函数表达式外,比较常见的是后面跟的是一个 Promise
,由于 yield
关键字会对其后的表达式进行求值并返回,那么调用 next
方法时就会返回一个 Promise
对象,我们可以调用其 then
方法,并在回调中使用 next
方法将结果传回 Generator
。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data); });
上面的代码中,Generator
函数封装了 readFile_promise
方法,该方法返回一个 Promise
,Generator
函数对 readFile_promise
的调用方式和同步操作基本相同,除了 yield
关键字之外。
上面的 Generator
函数中只有一个异步操作,当有多个异步操作时,就会变成下面的形式。
function* gen() { const result = yield readFile_promise("foo.txt"); console.log(result); const result2 = yield readFile_promise("bar.txt"); console.log(result2); } const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
然而看起来还是嵌套的回调?难道使用 Generator
的初衷不是优化嵌套写法吗?说的没错,虽然在调用时保持了同步形式,但我们需要手动执行 Generator
函数,于是在执行时又回到了嵌套调用。这是 Generator
的缺点。
5. Generator 的自动执行
对 Generator
函数来说,我们也看到了要顺序地读取多个文件,就要像上面代码那样写很多用来执行的代码。无论是 Promise
还是 Generator
,就算在编写异步代码时能获得便利,但执行阶段却要写更多的代码,Promise
需要手动调用 then
方法,Generator
中则是手动调用 next
方法。
当需要顺序执行异步操作的个数比较少的情况下,开发者还可以接受手动执行,但如果面对多个异步操作就有些难办了,我们避免了回调地狱,却又陷到了执行地狱里面。我们不会是第一个遇到自动执行问题的人,社区已经有了很多解决方案,但为了更深入地了解 Promise
和 Generator
,我们不妨先试着独立地解决这个问题,如何能够让一个 Generator
函数自动执行?
5.1 自动执行器的实现
既然 Generator
函数是依靠 next
方法来执行的,那么我们只要实现一个函数自动执行 next
方法不就可以了吗,针对这种思路,我们先试着写出这样的代码:
function auto(generator) { const gen = generator(); while (gen.next().value !== undefined) { gen.next(); } }
思路虽然没错,但这种写法并不正确,首先这种方法只能用在最简单的 Generator
函数上,例如下面这种:
function* generator() { yield 'Hello World'; return 'end'; }
另一方面,由于 Generator
没有 hasNext
方法,在 while
循环中作为条件的:gen.next().value !== undefined
在第一次条件判断时就开始执行了,这表示我们拿不到第一次执行的结果。因此这种写法行不通。
那么换一种思路,我们前面介绍了 for/of
循环,那么也可以用它来执行 Generator
。
function* Generator() { yield "Hello World"; yield "Hello 夏安"; yield "end"; } const gen = Generator(); for (let i of gen) { console.log(i); }
// 输出结果
// Hello World
// Hello 夏安
// end
看起来没什么问题了,但同样地也只能拿来执行最简单的 Generator
函数,然而我们的主要目的还是管理异步操作。
5.2 基于Promise的执行器
前面实现的执行器都是针对普通的 Generator
函数,即里面没有包含异步操作,在实际应用中,yield
后面跟的大都是 Promise
,这时候 for/of
实现的执行器就不起作用了。
通过观察,我们发现 Generator
的嵌套执行是一种递归调用,每一次的嵌套的返回结果都是一个 Promise
对象。
const g = gen(); const result = g.next(); result.value.then(function (data) { g.next(data).value.then(function (data) { g.next(data); }) });
那么,我们可以根据这个写出新的执行函数。
function autoExec(gen) { function next(data) { const result = gen.next(data); // 判断执行是否结束 if (result.done) return result.value; result.value.then(function (data) { next(data); }); } next(); }
这个执行器因为调用了 then
方法,因此只适用于 yield
后面跟一个 Promise
的方法。
5.3 使用 co 模块来自动执行
为了解决 generator
执行的问题,TJ 于2013年6月发布了著名 co
模块,这是一个用来自动执行 Generator
函数的小工具,和 Generator
配合可以实现接近同步的调用方式,co
方法仍然会返回一个 Promise
。
const co = require("co"); function* gen() { const result = yield readFilePromise("foo.txt"); console.log(result); const result2 = yield readFilePromise("bar.txt"); console.log(result2); } co(gen);
只要将 Generator
函数作为参数传给 co
方法就能将内部的异步任务顺序执行,要使用 co
模块,yield
后面的语句只能是 promsie
对象。
到此为止,我们对异步的处理有了一个比较妥当的方式,利用 generator+co
,我们基本可以用同步的方式来书写异步操作了。但 co
模块仍有不足之处,由于它仍然返回一个 Promise
,这代表如果想要获得异步方法的返回值,还要写成下面这种形式:
co(gen).then(function (value) { console.log(value); });
另外,当面对多个异步操作时,除非将所有的异步操作都放在一个 Generator
函数中,否则如果需要对 co
的返回值进行进一步操作,仍然要将代码写到 Promise
的回调中去。
到此这篇关于JavaScript Generator异步过度的实现详解的文章就介绍到这了,更多相关JavaScript Generator 内容请搜索阿兔在线工具以前的文章或继续浏览下面的相关文章希望大家以后多多支持阿兔在线工具!