Giter Club home page Giter Club logo

thunks's Introduction

thunks

A small and magical composer for all JavaScript asynchronous.

NPM version Build Status js-standard-style Coverage Status Downloads

中文说明

thunks 的作用域和异常处理设计

Compatibility

ES5+, support node.js and browsers.

Summary

Implementations

  • Toa A powerful web framework rely on thunks.
  • T-man Super test manager for JavaScript.
  • thunk-redis The fastest thunk/promise-based redis client, support all redis features.
  • thunk-disque A thunk/promise-based disque client.
  • thunk-stream Wrap a readable/writable/duplex/transform stream to a thunk.
  • thunk-queue A thunk queue for uncertainty tasks evaluation.
  • thunk-loop Asynchronous tasks loop (while (true) { ... }).
  • thunk-mocha Enable support for generators in Mocha with backward compatibility.
  • thunk-ratelimiter The fastest abstract rate limiter.
  • thunk-workers Thunk-based task scheduler that executes synchrounous and/or asynchronous tasks under concurrency control.
  • file-cache Read file with caching, rely on thunks.

And a mountain of applications in server-side or client-side.

What is a thunk

  1. ALGOL thunks in 1961

  2. thunk is a function that encapsulates synchronous or asynchronous code inside.

  3. thunk accepts only one callback function as an arguments, which is a CPS function.

  4. thunk returns another thunk function after being called, for chaining operations.

  5. thunk passes the results into a callback function after being excuted.

  6. If the return value of callback is a thunk function, then it will be executed first and its result will be sent to another thunk for excution, or it will be sent to another new thunk function as the value of the computation.

Demo

with thunk function

const thunk = require('thunks')()
const fs = require('fs')

thunk(function (done) {
  fs.stat('package.json', done)
})(function (error, res) {
  console.log(error, res)
})

with async function

thunk(async function () {
  console.log(await Promise.resolve('await promise in an async function'))

  try {
    await new Promise((resolve, reject) => {
      setTimeout(() => reject('catch promise error in async function'), 1000)
    })
  } catch (err) {
    console.log(err)
  }
})()

with generator function

const thunk = require('thunks')()
const fs = require('fs')
const size = thunk.thunkify(fs.stat)

// generator
thunk(function * () {
  // yield thunk function
  console.log(yield size('thunks.js'))
  console.log(yield size('package.json'))

  // yield async function
  console.log(yield async () => 'yield an async function in generator function')

  // yield generator function
  console.log(yield function * () { return 'yield an async function in generator function' })

    // parallel run
  console.log(yield thunk.all([
    size('thunks.js'),
    size('package.json')
  ]))
})()

chain, sequential, parallel

const thunk = require('thunks')()
const fs = require('fs')
const size = thunk.thunkify(fs.stat)

// sequential
size('.gitignore')(function (error, res) {
  console.log(error, res)
  return size('thunks.js')

})(function (error, res) {
  console.log(error, res)
  return size('package.json')

})(function (error, res) {
  console.log(error, res)
})

// sequential
thunk.seq([
  size('.gitignore'),
  size('thunks.js'),
  size('package.json')
])(function (error, res) {
  console.log(error, res)
})

// parallel
thunk.all([
  size('.gitignore'),
  size('thunks.js'),
  size('package.json')
])(function (error, res) {
  console.log(error, res)
})

Installation

Node.js:

npm install thunks

Bower:

bower install thunks

browser:

<script src="/pathTo/thunks.js"></script>

API

const thunks = require('thunks')
const { thunks, thunk, slice, Scope, isAsyncFn, isGeneratorFn, isThunkableFn } = from 'thunks'

thunks([scope])

Matrix of thunk, it generates a thunkFunction factory (named thunk) with it's scope. "scope" refers to the running evironments thunk generated(directly or indirectly) for all child thunk functions.

  1. Here's how you create a basic thunk, any exceptions would be passed the next child thunk function:
const thunk = thunks()
  1. Here's the way to create a thunk listening to all exceptions in current scope with onerror, and it will make sure the exceptions are not being passed to the followed child thunk function, unless onerror function returns true.
const thunk = thunks(function (error) { console.error(error) })

Equals:

const scope = new thunks.Scope(function (error) { console.error(error) })
const thunk = thunks(scope)
  1. Create a thunk with onerror, onstop and debug listeners. Results of this thunk would be passed to debug function first before passing to the followed child thunk function.
const thunk = thunks({
  onstop: function (sig) { console.log(sig) },
  onerror: function (error) { console.error(error) },
  debug: function () { console.log.apply(console, arguments) }
})

Equals:

const scope = new thunks.Scope({
  onstop: function (sig) { console.log(sig) },
  onerror: function (error) { console.error(error) },
  debug: function () { console.log.apply(console, arguments) }
})
const thunk = thunks(scope)

The context of onerror, onstop and debug is a scope. Even multiple thunk main functions with different scopes are composed, each scope would be separate from each other, which means, onerror, onstop and debug would not run in other scopes.

thunks.pruneErrorStack

Default to true, means it will prune error stack message.

thunks.onerror(error)

Default to null, it is a global error handler.

Class thunks.Scope

const scope = new thunks.Scope({
  onstop: function (sig) { assert.strictEqual(this, scope) },
  onerror: function (error) { assert.strictEqual(this, scope) },
  debug: function () { assert.strictEqual(this, scope) }
})
const thunk = thunks(scope)

thunk(thunkable)

This is the thunkFunction factory, to create new thunkFunction functions.

The parameter thunkable value could be:

  1. a thunkFunction function, by calling this function a new thunkFunction function will be returned
let thunk1 = thunk(1)
thunk(thunk1)(function (error, value) {
  console.log(error, value) // null 1
})
  1. a thunkLike function function (callback) {}, when called, passes its results to the next thunkFunction function
thunk(function (callback) {
  callback(null, 1)
})(function (error, value) {
  console.log(error, value) // null 1
})
  1. a Promise object, results of Promise would be passed to a new thunkFunction function
let promise = Promise.resolve(1)

thunk(promise)(function (error, value) {
  console.log(error, value) // null 1
})
  1. objects which implements the method toThunk
let obj = {
  toThunk: function () {
    return function (done) { done(null, 1) }
  }
}
// `obj` has `toThunk` method that returns a thunk function
thunk(obj)(function (error, value) {
  console.log(error, value) // null 1
})
  1. objects which implement the method toPromise
const Rx = require('rxjs')
// Observable instance has `toPromise` method that returns a promise
thunk(Rx.Observable.fromPromise(Promise.resolve(123)))(function (error, value) {
  console.log(error, value) // null 123
})
  1. Generator and Generator Function, like co, but yield anything
thunk(function * () {
  var x = yield 10
  return 2 * x
})(function * (error, res) {
  console.log(error, res) // null, 20

  return yield thunk.all([1, 2, thunk(3)])
})(function * (error, res) {
  console.log(error, res) // null, [1, 2, 3]
  return yield thunk.all({
    name: 'test',
    value: thunk(1)
  })
})(function (error, res) {
  console.log(error, res) // null, {name: 'test', value: 1}
})
  1. async/await function
thunk(async function () {
  console.log(await Promise.resolve('await promise in an async function'))

  try {
    await new Promise((resolve, reject) => {
      setTimeout(() => reject('catch promise error in async function'), 1000)
    })
  } catch (err) {
    console.log(err)
  }
})(function * () {
  console.log(yield async () => 'yield an async function in generator function')
})()
  1. values in other types that would be valid results to pass to a new child thunk function
thunk(1)(function (error, value) {
  console.log(error, value) // null 1
})

thunk([1, 2, 3])(function (error, value) {
  console.log(error, value) // null [1, 2, 3]
})

You can also run with this:

thunk.call({x: 123}, 456)(function (error, value) {
  console.log(error, this.x, value) // null 123 456
  return 'thunk!'
})(function (error, value) {
  console.log(error, this.x, value) // null 123 'thunk!'
})

thunk.all(obj)

thunk.all(thunkable1, ..., thunkableN)

Returns a child thunk function.

obj can be an array or an object that contains any value. thunk.all will transform value to a child thunk function and excute it in parallel. After all of them are finished, an array containing results(in its original order) would be passed to the a new child thunk function.

thunk.all([
  thunk(0),
  function * () { return yield 1 },
  2,
  thunk(function (callback) { callback(null, [3]) })
])(function (error, value) {
  console.log(error, value) // null [0, 1, 2, [3]]
})

thunk.all({
  a: thunk(0),
  b: thunk(1),
  c: 2,
  d: thunk(function (callback) { callback(null, [3]) })
})(function (error, value) {
  console.log(error, value) // null {a: 0, b: 1, c: 2, d: [3]}
})

You may also write code like this:

thunk.all.call({x: [1, 2, 3]}, [4, 5, 6])(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] [4, 5, 6]
  return 'thunk!'
})(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] 'thunk!'
})

thunk.seq([thunkable1, ..., thunkableN])

thunk.seq(thunkable1, ..., thunkableN)

Returns a child thunk function.

thunkX can be any value, thunk.seq will transform value to a child thunk function and excute it in order. After all of them are finished, an array containing results(in its original order) would be passed to the a new child thunk function.

thunk.seq([
  function (callback) {
    setTimeout(function () {
      callback(null, 'a', 'b')
    }, 100)
  },
  thunk(function (callback) {
    callback(null, 'c')
  }),
  [thunk('d'), function * () { return yield 'e' }], // thunk in array will be excuted in parallel
  function (callback) {
    should(flag).be.eql([true, true])
    flag[2] = true
    callback(null, 'f')
  }
])(function (error, value) {
  console.log(error, value) // null [['a', 'b'], 'c', ['d', 'e'], 'f']
})

or

thunk.seq(
  function (callback) {
    setTimeout(function () {
      callback(null, 'a', 'b')
    }, 100)
  },
  thunk(function (callback) {
    callback(null, 'c')
  }),
  [thunk('d'), thunk('e')], // thunk in array will be excuted in parallel
  function (callback) {
    should(flag).be.eql([true, true])
    flag[2] = true
    callback(null, 'f')
  }
)(function (error, value) {
  console.log(error, value) // null [['a', 'b'], 'c', ['d', 'e'], 'f']
})

You may also write code like this:

thunk.seq.call({x: [1, 2, 3]}, 4, 5, 6)(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] [4, 5, 6]
  return 'thunk!'
})(function (error, value) {
  console.log(error, this.x, value) // null [1, 2, 3] 'thunk!'
})

thunk.race([thunkable1, ..., thunkableN])

thunk.race(thunkable1, ..., thunkableN)

Returns a child thunk function with the value or error from one first completed.

thunk.thunkify(fn)

Returns a new function that would return a child thunk function

Transform a fn function which is in Node.js style into a new function. This new function does not accept a callback as an argument, but accepts child thunk functions.

const thunk = require('thunks')()
const fs = require('fs')
const fsStat = thunk.thunkify(fs.stat)

fsStat('thunks.js')(function (error, result) {
  console.log('thunks.js: ', result)
})
fsStat('.gitignore')(function (error, result) {
  console.log('.gitignore: ', result)
})

You may also write code with this:

let obj = {a: 8}
function run (x, callback) {
  //...
  callback(null, this.a * x)
}

let run = thunk.thunkify.call(obj, run)

run(1)(function (error, result) {
  console.log('run 1: ', result)
})
run(2)(function (error, result) {
  console.log('run 2: ', result)
})

thunk.lift(fn)

lift comes from Haskell, it transforms a synchronous function fn into a new async function. This new function will accept thunkable arguments, evaluate them, then run as the original function fn. The new function returns a child thunk function.

const thunk = require('thunks')()

function calculator (a, b, c) {
  return (a + b + c) * 10
}

const calculatorT = thunk.lift(calculator)

let value1 = thunk(2)
let value2 = Promise.resolve(3)

calculatorT(value1, value2, 5)(function (error, result) {
  console.log(result) // 100
})

You may also write code with this:

const calculatorT = thunk.lift.call(context, calculator)

thunk.promise(thunkable)

it transforms thunkable value to a promise.

const thunk = require('thunks').thunk

thunk.promise(function * () {
  return yield Promise.resolve('Hello')
}).then(function (res) {
  console.log(res)
})

thunk.persist(thunkable)

it transforms thunkable value to a persist thunk function, which can be called more than once with the same result(like a promise). The new function returns a child thunk function.

const thunk = require('thunks')()

let persistThunk = thunk.persist(thunk(x))

persistThunk(function (error, result) {
  console.log(1, result) // x
  return persistThunk(function (error, result) {
    console.log(2, result) // x
    return persistThunk
  })
})(function (error, result) {
  console.log(3, result) // x
})

You may also write code with this:

const persistThunk = thunk.persist.call(context, thunkable)

thunk.delay(delay)

Return a child thunk function, this child thunk function will be called after delay milliseconds.

console.log('thunk.delay 500: ', Date.now())
thunk.delay(500)(function () {
  console.log('thunk.delay 1000: ', Date.now())
  return thunk.delay(1000)
})(function () {
  console.log('thunk.delay end: ', Date.now())
})

You may also write code with this:

console.log('thunk.delay start: ', Date.now())
thunk.delay.call(this, 1000)(function () {
  console.log('thunk.delay end: ', Date.now())
})

thunk.stop([message])

This will stop control flow process with a message similar to Promise's cancelable(not implemented yet). It will throw a stop signal object. Stop signal is an object with a message and status === 19(POSIX signal SIGSTOP) and a special code. Stop signal can be caught by onstop, and aslo can be caught by try catch, in this case it will not trigger onstop.

const thunk = require('thunks')({
  onstop: function (res) {
    if (res) console.log(res.code, res.status, res) // SIGSTOP 19 { message: 'Stop now!' }
  }
})

thunk(function (callback) {
  thunk.stop('Stop now!')
  console.log('It will not run!')
})(function (error, value) {
  console.log('It will not run!', error)
})
thunk.delay(100)(function () {
  console.log('Hello')
  return thunk.delay(100)(function () {
    thunk.stop('Stop now!')
    console.log('It will not run!')
  })
})(function (error, value) {
  console.log('It will not run!')
})

thunk.cancel()

This will cancel all control flow process in the current thunk's scope.

TypeScript Typings

import * as assert from 'assert'
import { thunk, thunks, isGeneratorFn } from 'thunks'
// or: import * as thunks from 'thunks'

thunk(function * () {
  assert.strictEqual(yield thunks()(1), 1)
  assert.ok(isGeneratorFn(function * () {}))

  while (true) {
    yield function (done) { setTimeout(done, 1000) }
    console.log('Dang!')
  }
})()

What functions are thunkable

thunks supports so many thunkable objects. There are three kind of functions:

  • thunk-like function function (callback) { callback(err, someValue) }
  • generator function function * () { yield something }
  • async/await function async function () { await somePromise }

thunks can't support common functions (non-thunk-like functions). thunks uses fn.length === 1 to recognize thunk-like functions.

Using a common function in this way will throw an error:

thunk(function () {})(function (err) {
  console.log(1, err) // 1 [Error: Not thunkable function: function () {}]
})

thunk(function (a, b) {})(function (err) {
  console.log(2, err) // 2 [Error: Not thunkable function: function (a, b) {}]
})

thunk(function () { let callback = arguments[0]; callback() })(function (err) {
  console.log(3, err) // 3 [Error: Not thunkable function: function () { let callback = arguments[0]; callback() }]
})

thunk()(function () {
  return function () {} // can't return a non-thunkable function.
})(function (err) {
  console.log(4, err) // 4 [Error: Not thunkable function: function () {}]
})

So pay attention to that. We can't return a non-thunkable function in thunk. If we return a thunkable function, thunk will evaluate it as an asynchronous task.

License

thunks is licensed under the MIT license. Copyright © 2014-2020 thunks.

thunks's People

Contributors

brooooooklyn avatar dependabot[bot] avatar duzun avatar gyson avatar mdvorscak avatar monkerek avatar tiye avatar zensh avatar znetstar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

thunks's Issues

Can not return function in thunk function

Since we can not return a function as a result in a thunk function and the author has no plan to solve the problem. It should be mentioned in README or leave it as a issue.

some demo about `Maximum call stack size exceeded`

'use strict';
/*global console*/

var Thunk = require('thunks')();

// demo 1
Thunk(0)(function(err, res) {
  console.log(err, res);
});

function next(err, res) {
  return res + Math.random();
}

// demo 2
var thunk = Thunk(0);

for (var i = 0; i < 10000000; i++) thunk = thunk(next);

thunk(function(err, res) {
  console.log(err, res);
});

// demo 3
Thunk(function(callback) {
  setTimeout(function() {
    callback(null, 100);
  }, 100);
})(function(err, res) {
  console.log(err, res);
});

// 只有这种深层嵌套才会 `Maximum call stack size exceeded`,但显然这种写法是有问题的,应该报错警告
// function testThunk() {
//   var i = 0;
//
//   console.time('testThunk');
//   Thunk(0)(nested)(function() {
//     console.timeEnd('testThunk');
//   });
//
//   function nested() {
//     if (++i <= 50000) return Thunk(0)(nested);
//   }
// }
//
// testThunk();

thunks 的作用域和异常处理设计

thunks 的作用域和异常处理设计

我认为,thunks 的作用域和异常处理设计是完美的,如果你发现不完美,那一定是 bug,请告知我来修复。

作用域

thunks 引入了作用域的概念,目前作用域中可以注册两个方法 debugonerror

var thunks = require('thunks');
var thunk = thunks({
  onerror: function (error) {
    console.error(error);
  },
  debug: function () {
    console.log.apply(console, arguments);
  }
});

thunk(function (callback) {
  // do some thing ...
  callback(error, value);
})(function (error, res) {
  console.log(error, res);
});

thunks 母函数

母函数,用于生成带作用域的 thunk

thunk 生成器

thunk 函数生成器,凡是使用同一个 thunk 生成器生成的任何 thunk 函数以及其派生的 thunk 链,都在同一个作用域内。(是否可以访问作用域?抱歉,为了代码安全,作用域是无形的存在,你访问不到~)

debug 方法

可以捕捉该作用域内任何 thunk 函数的任何输出值,用于调试,Promise 没有这个能力。

onerror 方法

可以捕捉作用域内任何异常,且异常一旦发生就会被捕捉,并且停止执行后续逻辑,除非 onerror 返回 true

Promise 是在 promise 链上注册 catch 捕捉异常,然而实际场景中我们并不需要单独处理链上的异常,而是有任何异常都跳到同一个处理函数,这就是 onerror。即使有异常需要特别处理的 thunk 函数链,也可通过以下方法实现需求:

方法一:

为特殊异常业务创建新的作用域。作用域是可以平行或任意嵌套的,它们之间不会相互干涉。比如 thunk-redis 库作为第三方 API 库,不应该自己处理异常,而应该将异常抛给调用方处理:

proto.hscan = function () {
  return sendCommand(this, 'hscan', tool.slice(arguments))(function (error, res) {
    if (error) throw error;
    res[1] = toHash(null, res[1]);
    return res;
  });
};

上面是 thunk-redisHSCAN 命令实现,它的作用域上没有注册 onerror,如果发生异常,直接 throw 给下一链(调用方)处理。

而对于一个 http server,我们就需要对每一个请求创建独立作用域处理:

http.createServer(function (req, res) {
  var thunk = thunks(function (error) {
    // 发生错误则将错误响应给用户
    res.renderError(error);
    // 如果是系统错误,则做相应处理,如写入日志等
    if (error.type === 'sys') catchSysError(error, req);
  })

  // sessionAuth 也经过了 thunks 封装
  thunk(sessionAuth(req))(function (error, result) {
    // error 必定为 null,不用管
    // 其它业务逻辑等,不管同步异步都可以用 thunks 封装,你懂的
  });
}).listen(80);

所以,取代 node.js 的 domain 是完全可行的,话说 domain 也不是好东西,将被淘汰。

方法二:

由于 onerror 给了忽略异常的能力,所以,如果异常判定为可忽略,return true 就行了

方法三:

thunk 函数内部自行 try catch,操作细节见后面示例。

几种 thunk 生成器的创建方式

  1. 不注册任何方法:

    var thunk = thunks();
  2. 只注册 onerror

    var thunk = thunks(function (error) {
      console.error(error);
    });

    如果 onerror 返回 true,则会忽略错误,继续执行后续逻辑。

    var thunk = thunks(function (error) {
      console.error(error);
      return true;
    });
  3. 注册 debugonerror

    var thunk = thunks({
      onerror: function (error) {
        console.error(error);
      },
      debug: function () {
        console.log.apply(console, arguments);
      }
    });

异常处理

见识了作用域,那么如果不添加 onerror 监听是不是就会丢失异常呢?显然不是:

var thunk = require('thunks')();
thunk(function (callback) {
  noneFn();
})();
// throw error: `ReferenceError: noneFn is not defined`

如上,异常将会被抛出系统:ReferenceError: noneFn is not defined

thunk(function (callback) {
  noneFn();
})(function (error, res) {
  // catch a error
  console.log(error, res); // [ReferenceError: noneFn is not defined] undefined
  noneFn();
})();
// none function to catch error, error will be throw.
// throw error: `ReferenceError: noneFn is not defined`

如上,第一个异常被捕获,第二个被抛出系统。第一个因为给 thunk 添加了数据接收体 callback 函数,thunks 当然认为 callback 会处理异常,所以把异常丢给 callback 处理。第二个,没有数据接收体,就把异常抛出系统了。如果是封装第三方 API,不知道后面有没有接收体,那么就应该像这样处理:

var thunk = require('thunks')();

module.exports = function (arg, options)
  return Thunk(function (callback) {
    // do some thing, get error or result
    callback(error, result);
  })(function (error, res) {
    // 如果有异常,直接抛出,当然也可加工后抛出
    if (error) throw error;
    // 进一步加工 res
    return doSomeOther(res);
  });
};

更多可以参考 thunk-redis,一个用 thunks 封装的原生 redis 客户端。

前面提到,“自行 try catch 异常” 是怎么回事,其实很简单:

thunk(function (callback) {
  try {
    noneFn();
  } catch (err) {
    return callback(err);
  }
  return callback(null, 1);
})(function (error, res) {
  console.log(error, res); // [ReferenceError: noneFn is not defined] undefined
});

当然,这捕获的是同步函数的异常,如果是异步函数,那么请用 thunks 封装好再来。

最后看看 generator 函数中的异常处理,很简单的示例代码,自行理解:

var thunk = require('../thunks.js')();

thunk(function* () {
  // catch error by yourself
  try {
    yield function (callback) { noneFn(); };
  } catch (err) {
    console.log('catched a error:', err);
  }

  yield function (callback) { throw new Error('some error'); };

})(function (error, res) {
  // catch the second error by Thunk
  console.log(error, res);

})(function* () {
  yield function (callback) { throw new Error('some error2'); };
})();
// throw error to system
// Error: some error2...

以上相关代码在 examples 目录均可见,还包括更多使用示例。

为什么不加入一些流程控制相关的方法,不用借助其他库就可以畅快的完成一些简单的流程任务

我看到thunk的all方法和seq方法,参数数组里的元素都是thunk对象,为什么不可以是普通数组呢。

比如这样:

thunk
.each(arr, fn)
.seq(fn)

each处理数组返回符合seq的要求的数组,这样来处理,在一个流程中处理完一个需求

场景:处理20条异步任务,任务全部完成后返回处理结果

thunk
.each(getList(20), fn)
.seq(fn)

Promise 的及早求值 vs thunks 的惰性求值

Promise 的及早求值 vs thunks 的惰性求值

thunks 的异步编程模式模仿了 Promise,实际上 thunks 诞生之初的目标就是想用纯函数实现 Promise 的功能。随着代码的不断优化和越来越多的功能引入,thunks 已完全超越 Promise,Promise 能实现的业务逻辑,thunks 都能实现,而 thunks 能实现的很多功能,Promise 却无能为力,更重要的是,thunks 能完美处理 Promise。

thunk 是什么?在《Haskell趣學指南》中,thunk 被翻译成 保证,而在《Haskell 函数式编程入门》的最后章节(15.3.2),thunk 描述如下:

thunk 意为形实替换程序(有时候也称为延迟计算,suspended computation)。它指的是在计算的过程中,一些函数的参数或者一些结果通过一段程序来代表,这被称为 thunk。可以简单地把 thunk 看做是一个未求得完全结果的表达式与求得该表达式结果所需要的环境变量组成的函数,这个表达式与环境变量形成了一个无参数的闭包(parameterless closure),所以 thunk 中有求得这个表达式所需要的所有信息,只是在不需要的时候不求而已。

以上描述可以看出,thunk 必定是惰性求值的。而 thunks 的实现也与 haskell 中的 thunk 含义一致。

及早求值和惰性求值是 Promise 与 thunks 的根本性区别之一。那么这个区别在实际应用场合中会有影响吗?毕竟普通场合下用起来都差不多。

下面就给出一个受 Promise 的及早求值限制而难以实现的应用场景。首先,用示例看看及早求值和惰性求值的区别。

我使用了 thunk-redis 来做示例,因为它同时支持输出 thunk API 和 promise API。

Promise 的及早求值:

var redis = require('thunk-redis');
var client = redis.createClient({usePromise: true});

var infoPromise = client.info();
// 这里,info command 已经向 redis server 发出,已经开始异步求值。

infoPromise.then(function(info) {
  console.log(info);
}).catch(function(err) {
  console.error(err);
});

thunks 的惰性求值:

var redis = require('thunk-redis');
var client = redis.createClient();

var infoThunk = client.info();
// 这里,info command 请求还封装在 infoThunk 函数内,没有向 redis server 发出请求。

infoThunk(function(err, info) {
  if (err) return onsole.error(err);
  console.log(info);
});
// 执行 infoThunk 函数才会向 redis server 发出请求,进行异步求值。

可以看到,当我们获得一个 promise 的时候,它内部的异步任务已经启动。而当我们获得一个 thunk 函数的时候,
它内部的异步任务没有启动,在我们需要的时候手动执行,不需要的话就不必管了。而 promise 就由不得你了,不管你要不要,它已经执行。

我们来看看一个实际应用场景:

/**
* 更新 issue 内容(不包括置顶、阅读计数、标签、点赞、对象关联、评论),并后台同步到 mongodb
*
* @param {Object} issue
*
* @return {Boolean}
* @api public
*/
exports.updateIssue = function*(issueObj) {
  // 将 issue 更新内容转化为 redis 格式(redis 为主数据库)
  var issue = parseIssueTo(issueObj);
  // 将 redis transaction 启动命令推入任务队列
  var tasks = [client.multi()];
  if (issue.title || issue.content) {
    // 如果更新内容包含 title 或 content,则需要同时更新 issue 的活跃统计
    // 先读取原文的 title 和 content 用作修改对比
    var value = yield client.hmget(issueKey(issueObj._id), 'title', 'content');
    // 未读到 issue 则说明它不存在,请求数据有问题
    if (!value.length) tools.throw(404, 'issue "' + issueObj._id + '" is not exist');
    // 作比较,未更新则删除
    if (issue.title === value[0]) delete issue.title;
    if (issue.content === value[1]) delete issue.content;
    // 有更新则将活跃统计更新任务推入队列
    if (issue.title || issue.content) {
      var activedAt = new Date();
      issue.activedAt = JSON.stringify(activedAt);
      tasks.push(client.zadd(activeZSets, +activedAt, issueObj._id));
    }
    // 若不涉及 title 或 content,则直接判断 issue 是否存在
  } else yield isIssueExists(issueObj._id);
  // 空数据不处理(如原传入了 content 但发现没改变),直接返回
  if (_.isEmpty(issue)) return false;
  // 将数据更新任务推入队列
  tasks.push(client.hmset(issueKey(issueObj._id), issue));
  tasks.push(client.exec());
  // 如果是 promise API,上面队列中的任务都已经开始执行
  // 如果是 thunk API,则还没有执行,`yield tasks`才真正开始执行
  var res = yield tasks;
  // client.exec() 响应不为 null则表明执行成功,将更新同步到 mongodb
  res = !!res[tasks.length - 1];
  if (res) Thunk(mongoIssue.findByIdAndUpdate(issueObj))();
  return res;
};

这是从用户社区系统的 DAO 操作模块抽取的一段,我添加了一些注释。它是用 Toa 构建的系统(开发中)。

这只是一个相对简单的场景。采用 thunk API,我们可以按照逻辑获取一个一个异步任务的 thunk,按顺序推入任务队列,中间若有出错可以直接退出。当一切准备好,我们才真正开始执行这个异步任务队列。

如果采用 promise API,我们必须先把相关的一切准备好,如解析请求、读取数据、对比数据等,再根据准备结果构建不同的 promise 任务队列,最后的 yield tasks 将只是等待队列中的所有 promise 完成。我们不能提前取得任何一个任务的 promise,因为一旦取得,这个任务就开始执行了,无法取消~

虽然 promise 和 thunk 都是 “保证”,但实际应用场景需要的保证应该更像后者:先拿到一个“保证”,当需要时就可以按预期执行,不需要则直接丢弃;而不应该像前者,拿到这个“保证”时它就已经执行了,不管是不是真正需要。

关于无缝接入第三方库

比如bluebird有注明的promisifyAll,对于像mongoose, redis这种第三方库,几乎不用修改任何代码就无缝接入Promise

// The most popular redis module
var Promise = require("bluebird");
Promise.promisifyAll(require("redis"));

// Mongoose
var Promise = require("bluebird");
Promise.promisifyAll(require("mongoose"));

业务代码的就可以像这样写了:

Feed.findOneAsync().then(function(feed){
    feed.text = Math.random().toString()
    return feed.saveAsync()
}).then(function(doc){
    callback(null, doc)
})

thunk这种基于封装的模式好像写起代码来,略显冗余和繁琐。

目前我个人感觉就是如果业务比较复杂或者函数不太符合node.js callback风格的时候thunk还是挺有用的,不知道是否表达准确。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.