应用

Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。

(1)异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

  1. function* loadUI() {
  2. showLoadingScreen();
  3. yield loadUIDataAsynchronously();
  4. hideLoadingScreen();
  5. }
  6. var loader = loadUI();
  7. // 加载UI
  8. loader.next()
  9. // 卸载UI
  10. loader.next()

上面代码中,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。

Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

  1. function* main() {
  2. var result = yield request("http://some.url");
  3. var resp = JSON.parse(result);
  4. console.log(resp.value);
  5. }
  6. function request(url) {
  7. makeAjaxCall(url, function(response){
  8. it.next(response);
  9. });
  10. }
  11. var it = main();
  12. it.next();

上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

下面是另一个例子,通过 Generator 函数逐行读取文本文件。

  1. function* numbers() {
  2. let file = new FileReader("numbers.txt");
  3. try {
  4. while(!file.eof) {
  5. yield parseInt(file.readLine(), 10);
  6. }
  7. } finally {
  8. file.close();
  9. }
  10. }

上面代码打开文本文件,使用yield表达式可以手动逐行读取文件。

(2)控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

  1. step1(function (value1) {
  2. step2(value1, function(value2) {
  3. step3(value2, function(value3) {
  4. step4(value3, function(value4) {
  5. // Do something with value4
  6. });
  7. });
  8. });
  9. });

采用 Promise 改写上面的代码。

  1. Promise.resolve(step1)
  2. .then(step2)
  3. .then(step3)
  4. .then(step4)
  5. .then(function (value4) {
  6. // Do something with value4
  7. }, function (error) {
  8. // Handle any error from step1 through step4
  9. })
  10. .done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。

  1. function* longRunningTask(value1) {
  2. try {
  3. var value2 = yield step1(value1);
  4. var value3 = yield step2(value2);
  5. var value4 = yield step3(value3);
  6. var value5 = yield step4(value4);
  7. // Do something with value4
  8. } catch (e) {
  9. // Handle any error from step1 through step4
  10. }
  11. }

然后,使用一个函数,按次序自动执行所有步骤。

  1. scheduler(longRunningTask(initialValue));
  2. function scheduler(task) {
  3. var taskObj = task.next(task.value);
  4. // 如果Generator函数未结束,就继续调用
  5. if (!taskObj.done) {
  6. task.value = taskObj.value
  7. scheduler(task);
  8. }
  9. }

注意,上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《异步操作》一章。

下面,利用for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法。

  1. let steps = [step1Func, step2Func, step3Func];
  2. function* iterateSteps(steps){
  3. for (var i=0; i< steps.length; i++){
  4. var step = steps[i];
  5. yield step();
  6. }
  7. }

上面代码中,数组steps封装了一个任务的多个步骤,Generator 函数iterateSteps则是依次为这些步骤加上yield命令。

将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。

  1. let jobs = [job1, job2, job3];
  2. function* iterateJobs(jobs){
  3. for (var i=0; i< jobs.length; i++){
  4. var job = jobs[i];
  5. yield* iterateSteps(job.steps);
  6. }
  7. }

上面代码中,数组jobs封装了一个项目的多个任务,Generator 函数iterateJobs则是依次为这些任务加上yield*命令。

最后,就可以用for...of循环一次性依次执行所有任务的所有步骤。

  1. for (var step of iterateJobs(jobs)){
  2. console.log(step.id);
  3. }

再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。如果想要依次执行异步的步骤,必须使用后面的《异步操作》一章介绍的方法。

for...of的本质是一个while循环,所以上面的代码实质上执行的是下面的逻辑。

  1. var it = iterateJobs(jobs);
  2. var res = it.next();
  3. while (!res.done){
  4. var result = res.value;
  5. // ...
  6. res = it.next();
  7. }

(3)部署 Iterator 接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

  1. function* iterEntries(obj) {
  2. let keys = Object.keys(obj);
  3. for (let i=0; i < keys.length; i++) {
  4. let key = keys[i];
  5. yield [key, obj[key]];
  6. }
  7. }
  8. let myObj = { foo: 3, bar: 7 };
  9. for (let [key, value] of iterEntries(myObj)) {
  10. console.log(key, value);
  11. }
  12. // foo 3
  13. // bar 7

上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署next方法。

下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。

  1. function* makeSimpleGenerator(array){
  2. var nextIndex = 0;
  3. while(nextIndex < array.length){
  4. yield array[nextIndex++];
  5. }
  6. }
  7. var gen = makeSimpleGenerator(['yo', 'ya']);
  8. gen.next().value // 'yo'
  9. gen.next().value // 'ya'
  10. gen.next().done // true

(4)作为数据结构

Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

  1. function* doStuff() {
  2. yield fs.readFile.bind(null, 'hello.txt');
  3. yield fs.readFile.bind(null, 'world.txt');
  4. yield fs.readFile.bind(null, 'and-such.txt');
  5. }

上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。

  1. for (task of doStuff()) {
  2. // task是一个函数,可以像回调函数那样使用它
  3. }

实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。

  1. function doStuff() {
  2. return [
  3. fs.readFile.bind(null, 'hello.txt'),
  4. fs.readFile.bind(null, 'world.txt'),
  5. fs.readFile.bind(null, 'and-such.txt')
  6. ];
  7. }

上面的函数,可以用一模一样的for...of循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。