ES6 中的 generator 函数究竟是什么
我们在学习 js 的时候应该都知道一个概念:一旦函数开始执行,它将运行直至完成,没有其他的代码可以在运行期间干扰它。
但是在 ES6 中引入了一种新型的函数,它不按照“运行至完成”的规则。这种新型的函数称为“generator”。
var a = 1;function func() {a++;func2();console.log(a)
}
function func2() {a++;
}
func();
// 3
在上面的 func 函数中,a++ 运行完后会执行 func2() 函数,最终 a 的值为 3。
要是 func2() 不存在,但以某种方式依然可以在 a++ 和 console.log(a) 语句之间运行呢?这可能吗?
在抢占式(preemptive) 多线程语言中,func2() 去“干扰”并正好在两个语句之间那一时刻运行,实质上是可能的。但 JS 不是抢占式的,也不是多线程的。但是,如果 func() 本身可以用某种办法在代码的这一部分指示一个“暂停”,那么这种“干扰”(并发)的协作形式就是可能的。
比如:
var a = 1;
function *func() {a++;yield;console.log(a);
}
function bar() {a++
}
我们很可能在大多数其他的JS文档/代码中看到,一个 generator 的声明的格式为
function* func() { .. }
而不是上面的function *func() { .. }
,它们之间唯一的区别是*
的位置不同。这两种形式在功能性/语法上是完全一样的,还有第三种function*foo() { .. }
(没空格)形式。
现在我们来运行一下上面的代码:
// 构建一个迭代器it来控制generator
let it = func();
a; // 1
it.next();
a; // 2
bar();
a; // 3
it.next();
// 3
看起来很陌生对吧,我们来讲解一下这个过程:
- it = func(); func 不是一个普通函数,func() 并不会运行它,而是构建了一个用来控制它执行的迭代器(iterator);
- 我们观察 a 的值,还是 1;
- it.next() 启动了 func 函数的执行,并且运行到 func 函数的第一行,也就是 a++;之后在 yield 语句暂停,此时第一个 it.next() 调用结束,此时 func 函数还是运行的,不会被垃圾回收机制回收掉,但是它现在处于暂停节点,等待下一次的 next() 方法重新启动;
- 再次观察 a 的值,变成 2;说明运行了 a++;
- 此时执行 bar() 方法,再次对 a 进行递增;
- 最后执行 it.next(),重新从暂停的地方启动函数,执行 console.log(a),把 a 的值打印出来。
generator 是一种函数,它可以开始和停止一次或多次(在遇到 yield 关键词时会暂停,执行 next() 方法启动),甚至没必要一定要完成。
generator 是一个函数,这也就意味着它是可以接受参数及返回值的:
function *func(x, y) {return x * y;
}
// 传参跟普通函数一样
let it = func(1, 2)
it.next() // {value: 2, done: true}
我们可以很明显发现,虽然 func(1, 2)
这样的传参跟普通函数一样,但是它实际上并不会执行,我们只是创建了迭代器对象,将它赋值给变量 it,当我们调用 it.next() 时,它指示 func(…) 从现在的位置向前推进,直到遇到一个 yield 或者到函数的最后。
next(…) 调用的结果是一个带有 value 属性的对象,它持有从 func(…) 返回的任何值(如果有的话)。换句话说,yield 导致在 generator 运行期间,一个值被从中发送出来,有点儿像一个中间的 return。
generator 除了接收参数和拥有返回值,它们还内置有更强大,更吸引人的输入/输出消息能力,这是通过使用 yield 和 next(…) 实现的:
function *func(x) {let y = x * (yield);return y;
}let it = func( 1 );// 开始执行 func(..)
it.next(); // {value: undefined, done: false}it.next( 2 ); // {value: 2, done: true}
在 it = func(1)
中将 1 作为参数 x 传入。之后调用 it.next()
开始启动 func()
。
之后在 func
内部,开始执行 let y = x * (yield)
,但是它遇到 yield 后暂停了,在下一个 next 方法中传递了一个值 2,此时 2 作为 yield 的结果。因此,赋值语句实际上是 let y = 1 * 2
,最后把 y return 出去,作为 next() 方法的结果。
从上面的几个实例中我们可以看到,next 总是比 yield 多一个,因为第一次 next 是用于启动 generator 的,之后的其他 next 才会跟 yield 相对应。
除了可以通过 next 给 yield 传值外,还可以通过 yield 给 next 的结果赋值:
function *func(x) {let y = x * (yield "leo");return y;
}let it = func(1);
it.next(); // {value: 'leo', done: false}it.next(2); // {value: 2, done: true}
因为只有一个暂停的 yield 才能接收这样一个被 next(…) 传递的值,但是当我们调用第一个 next() 时,在generator 的最开始并没有任何暂停的 yield 可以接收这样的值。语言规范和所有兼容此语言规范的浏览器只会忽略任何传入第一个 next() 的值。传递这样的值是一个坏主意,因为我们只不过创建了一些令人困惑的代码。所以,我们要记得总是用一个无参数的 next() 来启动 generator。
迭代器
一个 generator 本身在技术上讲并不是一个 iterable,但是当我们执行 generator 时,我们就能得到一个迭代器:
我们写一个无限数字序列生成器:
function *genNumber() {let nextNumber;while(true) {if(nextNumber === undefined) {nextNumber = 1;} else {nextNumber += 1;}yield nextNumber;}
}
通常来说在一个真实的 JS 程序中含有一个 while…true 循环通常是一件非常不好的事情,如果它没有一个 break 或 return 语句,那么它就很可能永远运行,并同步阻塞/锁定浏览器 UI。但是在 generator 函数中,如果循环中含有 yield,那它就是完全没有问题的,因为 generator 将在每次迭代后暂停,可以重新回到主程序或事件循环队列中。
let gen = genNumber();
for (var v of gen) {console.log( v );// 停止循环if (v > 500) {break;}
}
genNumber 是一个 generator 函数,调用这个 generator 可以生成一个迭代器给 for…of 使用,这个迭代器中有一个 Symbol.iterator 函数,会把 next 方法中的 value 读取出来。
而在循环中的 break 被调用后,func 实例基本上被留在了一个永远挂起的状态。
gen // genNumber {<closed>}
gen.next(); // {value: undefined, done: true}
for…of 循环的“异常完成”(或者叫做 “提前终结”),一般是由break,return,或未捕捉的异常导致的——会向 generator 的迭代器发送一个信号,以使它终结。技术上讲,for…of 循环也会在循环正常完成时向迭代器发送这个信号。
虽然一个 for…of 循环将会自动发送这种信号,但是我们也可以通过调用 generator 的 return() 方法来手动发送:
let gen = genNumber();
for (var v of gen) {console.log( v );// 停止循环if (v > 500) {console.log(gen.return('停止循环')) // {value: '停止循环', done: true}}
}
在 generator 中还有个特性,如果在内部指定一个 try…finally ,它将总是被执行,即便是 generator 从外部被完成。
function* genNumber() {try {let nextNumber;while (true) {if (nextNumber === undefined) {nextNumber = 1;} else {nextNumber += 1;}yield nextNumber;}} finally {console.log('finally');}
}
let gen = genNumber();
for (var v of gen) {console.log( v );// 停止循环if (v > 500) {console.log(gen.return('停止循环'))}
}
// ...
// finally
// {value: '停止循环', done: true}
从上面的打印可以看到,在执行 generator 的 return 方法后,会先触发内部的 finally 块(如果它存在的话),之后才打印出 return 的返回结果(返回的 value 设置为传入 return(…) 的任何值)。我们现在也不必再包含一个 break,因为 generator 的迭代器会被设置为 done:true,所以 for…of 循环会在下一次迭代时终结。
generator 处理异步
我们先来看一段代码:
function foo(x,y,cb) {ajax("http://xxxx?x=" + x + "&y=" + y,cb);
}
foo( 1, 2, function(err,text) {if (err) {console.error( err );}else {console.log( text );}
});
使用 generator 实现相同的逻辑:
function foo(x,y,cb) {ajax("http://xxxx?x=" + x + "&y=" + y,function (err, data) {if(err) {it.throw(err);} else [it.next(data)]});
}function *main() {try {let data = yield foo(1 ,2);console.log(data);} catch(err) {console.log(err);}
}var it = main();
it.next();
在generator内部的代码看起来完全是同步的(除了yield关键字本身),但实际上在 foo(…) 内部,操作可以完全是异步的。
除了写法上看起来跟同步的一样外,它还可以使用 try…catch 捕获。
看上面的代码,在 yield 之后,我们在 foo 函数内部使用 it.throw 抛出一个错误,使得这个错误被传递给 yield,最终被 try…catch 捕获。同样,也可以通过 yield 把错误传递给 next:
function *main() {var x = yield "leo";yield x.toLowerCase(); // 引发一个异常
}var it = main();it.next().value; // leotry {// 给 yield 赋值为 2,导致 x 没有 toLowerCase 方法it.next( 42 );
}
catch (err) {console.error( err ); // TypeError
}
与 Promise 结合
在 async/await 出现之前,最有意思的就将 generator 与 Promise 进行结合使用:
function sendRequest(x, y) {return request('http:/xxx/?x=' + x + '&y=' + y);
}function *main() {try {let data = yield sendRequest(1, 2);console.log(data);} catch(err) {console.log(err)}
}let it = main();
let p = it.next().value;
p.then(function fulfilled(data) {it.next(data);},function rejected(err) {it.throw(err);}
)
在 async/await 出现之后,我们就有更简便的写法了:
async function main() {try {let data = await sendRequest(1, 2);console.log(data);} catch(err) {console.log(err);}
}