TOP1

前言

在上个章节中我们通过 Generator 体验了一种全新的异步流程书写方式, 我们在异步操作前面加上 yield 命名,通过promise对异步状态的绑定和指针就能控制程序是否交出执行权,同时也朝着”以同步方式书写异步代码”的目标,跨出了关键性的一步。

但是我们也发现了一些问题:

  1. var fetch = require('node-fetch');
  2. function* gen(){
  3. var url = 'https://api.github.com/users/github';
  4. var result = yield fetch(url);
  5. console.log(result.bio);
  6. }
  7. var g = gen();
  8. var result = g.next();
  9. result.value.then(function(data){
  10. return data.json();
  11. }).then(function(data){
  12. g.next(data);
  13. });

如果 Generator 函数中存在多个异步操作,我们必须通过next方法,手动的去改变内部指针。 这个过程会非常麻烦,所以我们要提供一种能自动执行机制,当异步操作有了结果,自动交回执行权。

两种方法可以做到这一点。

  1. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
  2. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

第一种方式就如刚刚演示的代码,需要充斥着大量的回调跟then方法的调用。 接下来我们就来讲讲第二种方式将异步操作包装成 Thunk 函数。

TOP2

什么是thunk 函数?

Thunk函数早在上个世纪60年代就诞生了。
那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是“求值策略”,即函数的参数到底应该何时求值。

  1. var x = 1;
  2. function f(m){
  3. return m * 2;
  4. }
  5. f(x + 5)

上面代码先定义函数 f,然后向它传入表达式 x + 5 。请问,这个表达式应该何时求值?

一种意见是“传值调用”(call by value),即在进入函数体之前,就计算 x + 5 的值(等于6),再将这个值传入函数 f 。C语言就采用这种策略。

  1. f(x + 5)
  2. // 传值调用时,等同于
  3. f(6)

另一种意见是“传名调用”(call by name),即直接将表达式 x + 5 传入函数体,只在用到它的时候求值。

  1. f(x + 5)
  2. // 传名调用时,等同于
  3. (x + 5) * 2

传值调用和传名调用,哪一种比较好?回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

  1. function f(a, b){
  2. return b;
  3. }
  4. f(5 * 6 / 7 - 8, 9);

上面代码中,函数 f 的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。
因此,有一些计算机学家倾向于”传名调用”,即只在执行时求值。

TOP3

Thunk 函数的含义

编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

  1. var x = 6;
  2. function f(n){
  3. return n * 2;
  4. }
  5. f(x + 5);
  6. // 等同于
  7. var thunk = function () {
  8. return x + 5;
  9. };
  10. function f(thunk){
  11. return thunk() * 2;

上面代码中,函数 f 的参数 x + 5 被一个函数替换了。凡是用到原参数的地方,对 Thunk 函数求值即可。
这就是 Thunk 函数的定义,它是”传名调用”的一种实现策略,用来替换某个表达式。

下面我们就基于 Thunk 函数写一个 Generator 执行器。

  1. var fetch = require('node-fetch');
  2. function* gen(){
  3. var url = 'https://api.github.com/users/github';
  4. var result = yield fetch(url);
  5. console.log(result.bio);
  6. }
  7. function run(gen){
  8. var g = gen();
  9. function next(data){
  10. var result = g.next(data);
  11. if (result.done) return result.value;
  12. result.value.then(function(data){
  13. next(data);
  14. });
  15. }
  16. next();
  17. }
  18. run(gen);

上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数头部的第一段的状态,然后判断 Generator 函数是否结束(result.done属性),如果没结束,就给第一段的异步操作返回的promise中通过then方法添加回调,并调用next方法,让指针从上一次停下来的地方开始执行,直到遇到下一个yield 表达式 或者return语句为止。

这样我们就能把手动的去改变内部指针,变成了自动执行。