Thunk函数的使用

90 阅读6分钟

编译器的求值策略通常分为传值调用以及传名调用, Thunk函数是应用于编译器的传名调用实现, 往往是将参数放到一个临时函数中, 再将这个临时函数传入函数体, 这个临时函数就叫做Thunk函数

求值策略

编译器的求值策略通常分为传值调用以及传名调用, 在下面的例子中, 将一个表达式作为参数进行传递, 传值调用以及传名调用中实现的方式有所不同

let x = 1;

function s(y) {
  console.log(y+1);		// 3
}

s(x + 1);

在上述的例子中, 无论是使用传值调用还是使用传名调用, 执行的结果都是一样的, 但是其调用过程不同:

  • 传值调用: 首先计算x+1, 然后将计算结果2传递到s函数, 即相当于调用s(2)
  • 传名调用: 直接将x+1表达式传递给y, 使用时再计算x+1, 即相当于计算(x+1)+1

传值调用与传名调用各有利弊, 传值调用比较简单, 但是对参数求值的时候, 实际上还没用到这个参数, 有可能造成没有必要的计算. 传名调用可以调用这个问题, 但是实现相对来说比较复杂

let x = 1;

function s(y) {
  console.log(y+1);			// 3
}

s(x + 1, x + 2);

在上面这个例子中, 函数s并没有用到x + 2这个表达式求得的值, 使用传名调用的话将表达式传入而并未计算, 只要在函数中没有用到x + 2这个表达式就不会计算, 使用传值调用的话就会首先将x + 2的值计算然后传入, 如果没有用到这个值, 那么就多了一次没有必要的计算. Thunk函数就是作为传名调用的实现而构建的, 往往是将参数放到一个临时函数之中, 再将这个临时函数传入函数体, 这个临时函数就叫做Thunk函数

let x = 1;
function s(y) {
  console.log(y + 1);		// 3
}

s(x + 1);

// 等价于

let x = 1;

function s(thunk) {
  console.log(thunk() + 1);		// 3
}

let thunk = function() {
  return x + 1;
}

s(thunk);

JavaScript的Thunk函数

JavaScript中的求值策略是传值调用, 在JavaScript中使用Thunk函数需要手动进行实现且含义有所不同, 在JavaScript中, Thunk函数替换的不是表达式, 而是多参数函数, 将其替换成单参数的版本, 且只接受回调函数作为参数

// 假设一个延时函数需要传递一些参数
const delayAsync = function(time, callback, ...args) {
  setTimeout(() => callback(...args), time);
}

const callback = function(x, y, z) {
  console.log(x, y, z);
}

delayAsync(1000, callback, 1, 2, 3);

// 使用Thunk函数

const thunk = function(time, ...args) {
  return function(callback) {
    setTimeout(() => callback(...args), time);
  }
}

const callback = function(x, y, z) {
  console.log(x, y, z);
}

const delayAsyncThunk = thunk(1000, 1, 2, 3);
delayAsyncThunk(callback);

实现一个简单的Thunk函数转换器, 对于任何函数, 只要参数有回调函数, 就能写成Thunk函数的形式

const converToThunk = function(funct) {
  return function(...args) {
    return function(callback) {
      return funct.apply(this, args);
    }
  }
};

const callback = function(x, y, z) {
  console.log(x, y, z);
}

const delayAsyncThunk = converToThunk(function(time, ...args) {
  setTimeout(() => callback(...args), time);
});

thunkFunct = delayAsyncThunk(1000, 1, 2, 3);
thunkFunct(callback);

Thunk函数在ES6之前可能应用比较少, 但是在ES6之后, 出现了Generator函数, 通过使用Thunk函数就可以用于Generator函数的自动流程管理. 首先是关于Generator函数的基本使用, 调用一个生成器函数并不会马上执行它里面的语句, 而是返回一个这个生成器的迭代器iterator对象, 他是一个指向内部状态对象的指针. 当这个迭代器的next()方法被首次(后续)调用时, 其内的语句会执行到第一个(后续)出现yield的位置为止, yield后紧跟迭代器要返回的值, 也就是指针就会从函数头部或者上一次停下来的地方开始执行到下一个yield. 或者如果用的是yield*, 则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)

function* f(x) {
  yield x + 10;
  yield x + 20;
  return x + 30;
}
const g = f(1);
console.log(g);						// f {<suspended>}
console.log(g.next());		// {value: 11, done: false}
console.log(g.next());		// {value: 21, done: false}
console.log(g.next());		// {value: 31, done: true}
console.log(g.next());		// {value: undefined, done: true}

由于Generator函数能够将函数的执行暂时挂起, 那么他就完全可以操作一个异步任务, 当上一个任务完成之后再继续下一个任务, 下面这个例子就是将一个异步任务同步化表达, 当上一个延时定时器完成之后才会进行下一个定时器任务, 可以通过这种方式解决一个异步嵌套的问题, 例如利用回调的方式需要在一个网络请求之后加入一次回调进行下一次请求, 很容易造成回调地狱, 而通过Generator函数就可以解决这个问题, 事实上async/await就是利用的Generator函数以及Promise实现的异步解决方案

let it = null;

function f(){
    let rand = Math.random() * 2;
    setTimeout(function(){
        if(it) it.next(rand);
    },1000)
}

function* g(){ 
    let r1 = yield f();
    console.log(r1);
    let r2 = yield f();
    console.log(r2);
    let r3 = yield f();
    console.log(r3);
}

it = g();
it.next();

虽然上边的例子能够自动执行, 但是不够方便, 现在实现一个Thunk函数的自动流程管理, 其自动帮我们进行回调函数的处理, 只需要在Thunk函数中传递一些函数执行所需要的参数, 比如例子中的index, 然后就可以编写Generator函数的函数体, 通过左边的变量接收Thunk函数中funct执行的参数, 在使用Thunk函数进行自动流程管理时, 必须保证yield后是一个Thunk函数 关于自动流程管理run函数, 首先需要知道在调用next()方法时, 如果传入了参数, 那么这个参数会传给上一条执行的yield语句左边的变量, 在这个函数中, 第一次执行next()时并为传递参数, 而且在第一个yield上边也并不存在接受变量的语句, 无需传递参数, 接下来就是判断是否执行完这个生成器函数, 在这里并没有执行完, 那么将自定义的next函数传入res.value中, 这里需要注意res.value是一个函数, 此时我们将自定义的next函数传递后, 就将next的执行权限交予了f这个函数, 在这个函数执行完异步任务后, 会执行回调函数, 在这个回调函数中会触发生成器的下一个next方法, 并且这个next方法是传递了参数的, 上文提到传入参数后会将其传递给上一条执行的yield语句左边的变量, 那么在这一次执行中会将这个参数值传递给r1, 然后再继续执行next, 不断往复, 直到生成函数结束运行, 这样就实现了流程的自动管理

function thunkFunct(index) {
  return function f(funct) {
    let rand = Math.random() * 2;
    setTimeout(() => {
      console.log(funct);
      return funct({
        rand: rand,
        index: index
      })
    }, 1000);
  }
}

function* g() {
  let r1 = yield thunkFunct(1);
  console.log(r1.index, r1.rand);
  let r2 = yield thunkFunct(2);
  console.log(r2.index, r2.rand);
  let r3 = yield thunkFunct(3);
  console.log(r3.index, r3.rand);
}

function run(generator) {
  let g = generator();
  
  let next = function(data) {
    let res = g.next(data);
    if (res.done) return;
    res.value(next);
  }
  
  next();
}

run(g);