Promises are about making asynchronous code retain most of the lost properties of synchronous code such as flat indentation and one exception channel. – Bluebird Wiki: Promise Anti Patterns
Promises 是为了让异步代码也能保持这些同步代码的属性:扁平缩进和单异常管道。
Deferred 反模式
这种反模式中,deferred
对象的创建是没有意义的,反而会增加代码的复杂度。
例如:
//Code copyright by Twisternha http://stackoverflow.com/a/19486699/995876 CC BY-SA 2.5myApp.factory('Configurations', function (Restangular, MotorRestangular, $q) { var getConfigurations = function () { var deferred = $q.defer(); MotorRestangular.all('Motors').getList().then(function (Motors) { //Group by Config var g = _.groupBy(Motors, 'configuration'); //Map values var mapped = _.map(g, function (m) { return { id: m[0].configuration, configuration: m[0].configuration, sizes: _.map(m, function (a) { return a.sizeMm }) } }); deferred.resolve(mapped); }); return deferred.promise; }; return { config: getConfigurations() } }); |
这里的 deferred
对象并没有什么意义,而且可能在出错的情况下无法捕获。
正确的写法应该为:
myApp.factory('Configurations', function (Restangular, MotorRestangular, $q) { var getConfigurations = function () { //Just return the promise we already have! return MotorRestangular.all('Motors').getList().then(function (Motors) { //Group by Cofig var g = _.groupBy(Motors, 'configuration'); //Return the mapped array as the value of this promise return _.map(g, function (m) { return { id: m[0].configuration, configuration: m[0].configuration, sizes: _.map(m, function (a) { return a.sizeMm }) } }); }); }; return { config: getConfigurations() } }); |
再举一个例子:
function applicationFunction(arg1) { var deferred = Promise.pending(); // 获取 Q.defer() libraryFunction(arg1, function(err, value) { if (err) { deferred.reject(err); } else { deferred.resolve(value); } }); return deferred.promise;} |
这就像重复造轮子,因为回调 API 的封装应该使用 promise 库的 promisification(promise 化)方法实现:
var applicationFunction = Promise.promisify(libraryFunction); |
通用 promise 化可能更快,因为可以借助 Promise 的内部操作,还能处理一些极端情况:例如 libraryFunction
同步抛异常或者用到了多个成功值。
什么时候使用 deferred?
必须用的时候。
当要封装的回调 API 和规范不一致时,例如 setTimeout
:
// 通过 setTimeout 返回 promisefunction delay(ms) { var deferred = Promise.pending(); setTimeout(function() { deferred.resolve(); }, ms); return deferred.promise;} |
.then(success, fail)
反模式
这样使用 .then
就像下面这段代码:
var t0;try { t0 = doThat();} catch(e) {} // 这样报错发生时会 catch 不到var staff = JSON.parse(t0); |
正常的同步写法是:
try { var stuff = JSON.parse(doThat());} catch(e) {} |
所以正确的 .then
用法应该是:
doThat().then(function(v) { return JSON.parse(v);}).catch(function(e) {}); |
嵌套 Promise
例如:
loadSomething().then(function(something) { loadAnothering().then(function(another) { DoSomethingOnThem(something, another); });}); |
如果只是想对两个 promise 的结果做处理,可以使用 Promise.all 方法:
Promise.all([loadSomething, loadAnothering]).then(function(something, another) { DoSomethingOnThem(something, another);}); |
断链
例如:
function anAsyncCall() { var promise = doSomethingAsync(); promise.then(function() { somethingComplicated(); }); return promise;} |
这里的问题在于加入 somethingComplicated()
出错的话不会被捕获。promise 应该链式调用。也就是说所有的 then
方法都应该返回一个新的 promise
。所以上面代码的正确写法为:
function anAsyncCall() { var promise = doSomethingAsync(); return promise.then(function() { somethingComplicated(); });} |
集合
例如需要对一个集合中的每个元素执行异步操作:
function workMyCollection(arr) { var resultArr = []; function _recursive(idx) { if (idx >= resultArr.length) return resultArr; return doSomethingAsync(arr[idx]).then(function(res) { resultArr.push(res); return _recursive(idx + 1); }); } return _recursive(0);} |
这里的问题在于需要遍历数组,其实可以用 promise.all
解决:
function workMyCollection(arr) { return q.all( arr.map(function(item) { return doSomethingAsync(item); }) ); } |
总结
最容易犯的错误,没有使用 catch
去捕获 then
里抛出的报错:
// snippet1somePromise().then(function () { throw new Error('oh noes');}).catch(function (err) { // I caught your error! :)}); // snippet2somePromise().then(function resolve() { throw new Error('oh noes');}, function reject(err) { // I didn't catch your error! :(}); |
这里的问题在于 snippet2 在 function resolve 中出错时无法捕获。而 catch 则可以。
下面的两个示例返回结果是不一样的:
// example1Promise.resolve('foo').then(Promise.resolve('bar')).then(function (result) { console.log(result); // foo});// example2Promise.resolve('foo').then(function () { return Promise.resolve('bar')}).then(function (result) { console.log(result); // bar}); |
example2 改变了返回值,因而 result 发生了变化。
更多关键词
async flow control, event loop, callback, promises, generators, async/wait, coroutines
Promises 所做的承诺是保证异步代码顺序执行,并能够链式管理异常和错误。相比使用event loop
和回调(callback)来控制异步代码的顺序执行,Promises 能够让代码更加清晰易懂。generator 更是从语言级别上提供了更好的支持。
V8 优化
V8 有两个编译器:通用编译器和优化编译器。也就是V8 中的 JavaScript 代码总是被编译成机器码后才执行的。
例如 a + b
编译成汇编代码为:
mov eax, amov ebx, bcall RuntimeAdd |
但如果 a
和 b
都是整数的话,则会被编译成:
mov eax, amov ebx, badd eax, ebx |
这样编译后的代码性能会快很多,因为跳过了 JavaScript 对不同类型数据相加的处理。
通用编译器会得到前一种汇编码,优化编译器会得到后一种汇编码。两种汇编代码性能很容易产生 100 倍的差异。但是存在一些模式,这些模式下的代码优化编译器不会去处理(称为bail out
)。Promises 属于一种被 bail out 的模式。
他山之石
Python greenlets(基于gevent)
go coroutine
参考资料
- bluebird wiki
- tao of code
- https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
- https://www.promisejs.org/patterns/
- http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript
- Node.js async exception handling
- promise nuggets
- Optimization Killers
- v8 bailout reasons
- Python greenlets
转载自:http://taobaofed.org/blog/2016/05/03/promise-anti-patterns/
作者:云翮