ECMAScript 6(22)Thunk函数

1. 异步

  • 异步编程的几种方法
  1. 回调函数
  2. 事件监听
  3. 发布/订阅
  4. Promise 对象
  5. Generator 函数
  • 比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步连续的执行就叫做同步

  • 一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。

  • 回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。

  • 依次读取两个以上的文件的情况,就会出现多重嵌套。

  • Promise 是解决回调函数嵌套的一种方法,链式.then((res)=>return ‘这次结果’+res).then((res)=>{// res就是上次一的结果,再次return会进入下一then}) , 但是这种方法的缺点就是耦合性太高了,代码冗余,一堆then

  • Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

2. Thunk 函数

  • Thunk 函数是自动执行 Generator 函数的一种方法。
  • 函数的求值策略分为两种:
  1. 传名策略:在需要求值的时候才求值;
  2. 传值策略:在传入函数的时候就求值;
var [x,y] = [1,2]
function foo(a) {
    return a * 10   // (x+y)*10 把表达式传进来然后再计算结果,叫做【传名策略】
}
foo(x + y)  // x+y= 3, 先计算结果,然后把结果传递进去,叫做【传值策略】
  • 简单说就是进入函数前计算结果就是传值,在函数体内计算就叫传名
  • ps : 基本类型传的是值,引用类型传的是地址。

2.1 传值策略的优点

举例 :

function foo(a) {
    let count = 0;
    for (let i = 0; i < 10000; i++) {
        count += a * i
    }
    return count;
}
foo(1 + 2)
  • 在这种情况下,【传值策略】的1+2只计算了一次,而若是【传名策略】,那么1+2显然要计算10000次,性能消耗明显大于前者。

2.2 传名策略的优点

  • 当作为参数的表达式,在函数内部不使用时,先计算就会造成性能消耗了
  • 使用的时候在计算,避免不必要的计算消耗

2.3 thunk 函数的含义

  • 编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) {
  return m * 2;
}

f(x + 5);

// 等同于

var thunk = function () {
  return x + 5;
};

function f(thunk) {
  return thunk() * 2;
}

3.js中的Thunk函数

  • 三个特点
  1. 有且只有一个参数是callback的函数
  2. 原函数是多参数函数
  3. 通常 callback 第一个参数是 error
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
  • 改造后Thunk函数的几个优点:
  1. 只关心回调函数的实现,不关心其他参数;
  2. 实现了柯里化。简单来说,假如我引用了一个函数,他有3个参数,但我每次使用它,有2个参数的值都是固定的,只有第3个参数的值不固定,那么我每次都这么写2个固定参数是很麻烦的,柯里化可以解决这个问题。
  3. 和Generator结合后,异步函数的写法与同步函数的写法十分相似;
  • Thunk 函数转换器。
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};
  • 使用转换器
var readFile = Thunk(qs.readFile)
readFile(fileName)(callback)
  • 完整的例子
function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);

ft(1)(console.log) // 1

4. Generator 函数自执行

  • Thunk 函数真正的威力,在于可以自动执行 Generator 函数。
function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);

例子

// 一个普通的异步函数,接受3个参数,分别是延迟时间,执行处理函数,和回调函数
function delay(time, dealCallback, callback) {
    setTimeout(function () {
        dealCallback()
        callback()
    }, time ? time : 1000)
}

// 改造 Thunk 函数
function Thunk(time, dealCallback) {
    return function (callback) {
        delay(time, dealCallback, callback)
    }
}

// 一个Generator函数,目的是连续发起2次异步请求
function *g() {
    yield Thunk(null, function () {
        console.log('first')
    })
    yield Thunk(500, function () {
        console.log('second')
    })
}
// 一个用于自动执行(管理)Generator函数的Thunk函数
// Generator函数的每个yield表达式,都是Thunk函数(不然next没有办法作为参数传过去)
function thunkForGenerator(callback) {
    let g = callback()

    function next() {
        let result = g.next()
        if (result.done) {
            return
        }
        result.value(next) // result.value() 就是执行Thunk函数, 然后把next当做回调函数传*g, g函数会触发一次1000ms延迟的异步(thunkForAsync函数),等异步执行完毕后,g函数执行了thunkForGenerator函数的next方法,将控制权重新还给了他;
    }

    // 这里启动函数
    next()
}
// 执行管理函数,将Generator函数作为参数传给他即可
thunkForGenerator(g)

5. 基于 Promise 对象的自动执行

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
  • 手动执行
var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});
  • 自执行
function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

6. co 模块

  • Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

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

  1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

  2. Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

  • co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co.

co模块github

参考文献: 阮一峰es6

你可能感兴趣的:(ES6系列,es6)