什么是 Generator 函数?
Generator 函数是 ES6 引入的一种异步编程解决方案,可以理解为 Generator函数是一个状态机,封装了多个内部状态。与普通函数不同,Generator 函数可以暂停执行和恢复执行,这种特性使其成为处理异步操作的强大工具。
基本语法和用法
定义 Generator 函数
Generator 函数在语法上与传统函数有所不同,主要体现在两个方面:
function关键字后面有一个星号(*)- 函数体内使用
yield表达式来定义不同的内部状态
javascript
复制下载
//1.必须调用next方法,使得指针移向下一个状态
function* f(){
yield 'hello';
yield 'world';
return 'ending';
}
const g = f();
g.next();
// { value: 'hello', done: false }
g.next();
// { value: 'world', done: false }
g.next();
// { value: 'ending', done: true }
执行机制
调用Generator函数后,该函数并不会执行,返回的也不是函数的运行结果,而是一个指向内部状态的指针对象(Iterator Object),所以必须调用遍历器对象的next方法,使得指针移向下一个状态。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
惰性求值特性
yield后面的表达式不会立即求值,只有当调用next方法、指针移动到该语句时,才会进行求值。因此等于为 JavaScript 提供了手动的"惰性求值"(Lazy Evaluation)的语法功能。
javascript
复制下载
//2.yield后面的表达式,只有当调用了next方法,指针移动到该语句时,才会执行该语句。因此等于为 JavaScript 提供了手动的"惰性求值"(Lazy Evaluation)的语法功能
function* gen() {
yield 123 + 456;
}
// 上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。
暂缓执行函数
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
javascript
复制下载
//3.Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了');
}
const generator = f();
setTimeout(() => {
generator.next();
}, 2000);
上面的代码会在 2 秒后输出"执行了",说明 Generator 函数的执行可以被延迟。
yield 表达式的使用限制
javascript
复制下载
//4. 另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
// yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
Generator 函数与 Iterator 接口
与Interator接口的关系
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
为对象添加 Iterator 接口
javascript
复制下载
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
// 上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。
遍历器对象的自引用性
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
javascript
复制下载
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
// 上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。
for...of 循环与 Generator
for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
javascript
复制下载
// for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
// 上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。
next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
参数传递的示例
javascript
复制下载
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
//上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN。
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
//如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
第一次调用 next 方法的特殊性
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
Generator 函数的异步应用
协程概念
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
(1)第一步,协程A开始执行。
(2)第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
(3)第三步,(一段时间后)协程B交还执行权。
(4)第四步,协程A恢复执行。
上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
同步流程的异步操作
Generator 函数的一个重要实际意义就是用来处理异步操作,用同步的代码写法,实现异步的操作流程。
javascript
复制下载
function one(){
setTimeout(() => {
console.log(111);
g.next();
},1000)
}
function two(){
setTimeout(() => {
console.log(222);
g.next();
},1000)
}function three(){
setTimeout(() => {
console.log(333);
g.next();
},1000)
}
function* gen(){
yield one();
yield two();
yield three();
}
const g = gen();
g.next();
上面的代码展示了如何使用 Generator 函数按顺序执行异步操作,每个异步操作完成后通过调用g.next()来启动下一个操作。
异步数据的顺序获取
在实际应用中,我们经常需要按顺序获取多个异步数据,Generator 函数可以很好地处理这种情况。
javascript
复制下载
// 模拟异步操作
function getUsers(){
setTimeout(() => {
let data = '用户数据';
g.next(data); //data将会作为下一个yield表达式的参数
},1000)
}
function getOrders(){
setTimeout(() => {
let data = '订单数';
g.next(data);
},1000)
}
function getGoods(){
setTimeout(() => {
let data = '商品数据';
g.next(data);
},1000)
}
function* gen(){
let users = yield getUsers();
console.log(users);
let orders = yield getOrders();
console.log(orders);
let goods = yield getGoods();
console.log(goods);
}
let g = gen();
g.next();
这段代码展示了如何通过 Generator 函数顺序获取用户数据、订单数据和商品数据,每个异步操作的结果都会传递给下一个操作。在getUsers函数中,通过g.next(data)将获取的用户数据作为参数传递给下一个yield表达式,这样let users = yield getUsers()中的users变量就能接收到数据。
实际应用场景
1. 异步流程控制
Generator 函数最直接的应用场景就是异步流程控制。通过yield和next的配合,可以以同步的方式编写异步代码,提高代码的可读性和可维护性。这种写法避免了传统的回调地狱,使异步代码的逻辑更加清晰。
2. 迭代器生成
Generator 函数是生成迭代器的天然工具,可以轻松实现各种复杂的迭代逻辑。由于 Generator 函数返回的就是一个迭代器对象,因此可以很方便地用于实现自定义的数据结构遍历。
3. 状态机
Generator 函数非常适合用于实现状态机,因为其本身就是一个包含多个状态的状态机。每个yield表达式代表一个状态,通过next方法可以在不同状态间切换。
4. 数据流处理
Generator 函数可以用于处理数据流,特别是在需要按需生成或处理数据的场景中。通过yield可以暂停数据生成,通过next可以继续生成,这种特性非常适合处理大数据集或实时数据流。
注意事项
-
yield 表达式的使用限制:
yield表达式如果用在另一个表达式之中,必须放在圆括号里面yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号
-
错误处理:
- Generator 函数内部可以部署错误处理代码,捕获函数体外抛出的错误
- 可以使用
try...catch语句来捕获通过throw方法抛出的错误
-
return 方法:
- Generator 函数返回的遍历器对象还有一个
return方法,可以返回给定的值,并终结遍历 Generator 函数 - 调用
return方法后,Generator 函数的遍历就终止了,后续调用next方法都会返回{value: undefined, done: true}
- Generator 函数返回的遍历器对象还有一个
-
throw 方法:
- 遍历器对象还有一个
throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获
- 遍历器对象还有一个
与 Async/Await 的关系
虽然 Generator 函数为异步编程提供了很好的解决方案,但在 ES2017 中引入了 Async/Await,这实际上是 Generator 函数的语法糖。Async/Await 基于 Generator 函数和 Promise,提供了更简洁的异步编程语法。
理解 Generator 函数的工作原理对于深入理解 Async/Await 至关重要。实际上,Async/Await 可以看作是 Generator 函数的自动化执行版本,不需要手动调用next方法。
总结
Generator 函数是 ES6 提供的一种强大的异步编程解决方案,它通过yield关键字可以暂停函数执行,通过next方法可以恢复函数执行。这种特性使得 Generator 函数非常适合用于:
- 异步操作的同步化表达
- 控制流管理
- 为任意对象部署 Iterator 接口
- 作为数据结构
- 实现复杂的迭代逻辑
Generator 函数的核心价值在于它提供了一种暂停和恢复执行的能力,这使得我们能够以同步的方式编写异步代码,大大提高了代码的可读性和可维护性。虽然在实际开发中,Async/Await 已经很大程度上取代了 Generator 函数在异步处理中的地位,但理解 Generator 函数的工作原理对于深入理解 JavaScript 异步编程模型仍然非常重要。
Generator 函数不仅是理解 Async/Await 的基础,也是处理复杂异步流程的强大工具。在某些场景下,比如需要手动控制执行流程的复杂异步操作,Generator 函数仍然有其独特的优势。通过掌握 Generator 函数,我们能够更好地理解 JavaScript 的异步编程本质,写出更加优雅和高效的代码。