when.js与Promises
上篇文章介绍了JavaScript异步机制,请看这里。
JavaScript异步机制带来的问题
JavaScript异步机制的主要目的是处理非阻塞,在交互的过程中,会需要一些IO操作(比如Ajax请求,文件加载,Node.js中的文件读取等),如果这些操作是同步的,就会阻塞其它操作。
异步机制虽然带来了许多好处,但同时也存在一些不如意的地方。
代码可读性
这样的代码读起来简直累觉不爱啊~~~
operation1(function(err, result) { operation2(function(err, result) { operation3(function(err, result) { operation4(function(err, result) { operation5(function(err, result) { // do sth. }) }) }) }) })
流程控制
异步机制使得流程控制变的有些困难,比如,在N个for循环中的回调函数执行完成之后再做某些事情:
var data = []; fs.readdir(path, function (err, files) { if (err) { console.log(err) } else { for (var i in files) { (function (i) { fs.stat(path + ‘/‘ + files[i], function (err, stats) { if (err) { console.log(err) } else { o = { "fileName": files[i].slice(0, -5), "fileTime": stats.mtime.toString().slice(4, 15) }; data.push(o); } }) })(i); } } }); var html = template(‘templates/main‘, data); res.writeHead(200, {‘Content-Type‘: ‘text/html; charset="UTF-8"‘}); res.write(html); res.end();
上面的代码不能获得预期的结果,因为for循环中所有的fs.stat执行结束后,data才会获得预期的值。可是,怎么知道for循环全部执行结束了呢?
异常处理
再看看上面的代码,如果多几个需要处理异常的地方,代码可谓支离破碎了。
Promises
Promise是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。
Promises全称叫Promises/A+,是一个开放的JavaScript规范,已经被加入ES6中。Promises只是一种规范,现在已经有浏览器部分的实现了Promises。
虽然现在少有浏览器原生支持Promises,但既然Promises提供了一个规范、一个标准,那么就一定有实现这个标准的库,比如Q、when.js、Dojo.deferred、jQuery.deferred等,本文通过when.js来说明Promises。
先看下面一段代码:
function a (num) { var deferred = when.defer(), count = num; setTimeout(function () { count ++; console.log(count); deferred.resolve(count); }, 1000); return deferred.promise; } function b (count) { var deferred = when.defer(); setTimeout(function () { count *= 10; console.log(count); deferred.resolve(count); }, 1000); return deferred.promise; } function c () { console.log(‘done‘); } a(10) .then(b) .then(c);
上面这段代码做的事情是:输入num,1s后输出num + 1,2s后输出(num + 1) * 10,最后输出done。
在每个需要执行的方法体a、b、c里面多了这些东西:
定义一个defer对象。
var deferred = when.defer();
完成方法的功能时,调用resolve(data)。
deferred.resolve(count);
最后返回promise。
return deferred.promise;
以上promise的写法是参考屈屈写的异步编程:When.js快速上手,写的非常好。
后来查阅when.js官方API时,发现作者不建议直接使用when.defer()来初始化一个promise,而是使用when.promise(resolver)。resolver是一个方法,里面包含三个参数resolve、reject和notify,分别对应了上面代码中的deferred.resolve、deferred.reject以及deferred.notify。
Note: The use of when.defer is discouraged. In most cases, using when.promise, when.try, or when.lift provides better separation of concerns.
when.promise()的官方示例:
var promise = when.promise(function(resolve, reject, notify) { // Do some work, possibly asynchronously, and then // resolve or reject. You can notify of progress events // along the way if you want/need. resolve(awesomeResult); // or resolve(anotherPromise); // or reject(nastyError); });
按照官方的用法,将上面a->b->c的代码改造一下,并在a()中加入错误处理,代码如下:
function a (num) { var promise = when.promise(function (resolve, reject, notify) { var count = num; if (count >= 10) { setTimeout(function () { count ++; console.log(count); resolve(count); }, 1000); } else { reject(count); } }); return promise; } function b (count) { var promise = when.promise(function (resolve, reject, notify) { setTimeout(function () { count *= 10; console.log(count); resolve(count); }, 1000); }); return promise; } function c () { console.log(‘done‘); } function alert(num) { console.log(‘您输入的数字为‘ + num + ‘,不能小于10!‘); } a(1) .then(b, alert) .then(c);
先来熟悉一下Promises是怎样定义promise状态的。Promises/A+是这样规定的:
- 一个promise必须是下面三种状态之一:pending, fulfilled, rejected
- 当一个promise是pending状态:
- 可以转变到fulfilled状态或者rejected状态
- 当一个promise是fulfilled状态:
- 不可以转变到其他任何状态
- 必须有一个不能改变的value
- 当一个promise是rejected状态:
- 不可以转变到其他任何状态
- 必须有一个不可改变的reason
上面代码在a()中使用when.promise()实例化了一个promise后,这个promise默认状态是pending。
promise.then()方法有三个参数,分别是onFulfilled、onRejected和onProgress,这三个参数都是方法名(也就是回调函数),通过这三个参数可以对应promise的fulfilled、rejected以及pending三种状态。
通过上面的代码来解释就是:
-
当a()正常执行结束时,调用resolve(data)将promise的状态改变为fulfilled,并且通过then()的第一个参数onFulfilled将参数data传递给下一个方法b()。
-
当a()非正常结束,这里认为a()在执行过程中出现了异常时,调用reject(reason)将promise的状态改变为rejected,并且通过then()的第二个参数onRejected将参数reason传递给下一个方法alert()。
-
当a()或者b()正在执行,需要更新状态时,可以调用notify(update),在then()的第三个参数onProgress中可以获得参数update的值。onFulfilled()和onRejected()在每个promise中只会被调用一次,但是onProgress()是在每次notify的时候都会被调用。
notify与onProgress的用法:
function a (num) { var promise = when.promise(function (resolve, reject, notify) { var count = num; if (count >= 10) { setTimeout(function () { notify(count); count ++; notify(count); resolve(count); }, 1000); } else { reject(count); } }); return promise; } function b (count) { var promise = when.promise(function (resolve, reject, notify) { setTimeout(function () { count *= 10; resolve(count); }, 1000); }); return promise; } function c () { console.log(‘done‘); } function alert(num) { console.log(‘您输入的数字为‘ + num + ‘,不能小于10!‘); } function progress(num) { console.log(‘当前的数字变为‘ + num); } a(12) .then(b, alert, progress) .then(c);
在前面说到流程控制的时候提到的for循环的问题还没有解决:如果想等N个for循环中的回调函数执行结束之后做某些事情,该怎么办?
这时候该用到when.all()方法了,比如前面提到的一段代码,需求是这样:
在Node.js中,创建http服务器,读取当前目录下articles目录中的所有文件,遍历所有文件,并根据“目录+文件名”读取文件的最后修改时间,最终返回[{文件名,文件修改时间}, {文件名,文件修改时间}, ...]
这样一个列表到客户端。
这里存在的问题是,读取目录的操作是异步的,for循环读取文件状态的操作也是异步的,而在for循环中的所有异步操作都执行结束后,需要调用response.writeHead()与response.write()将所有异步数据返回到客户端。在使用when.js之前,我能想到的就是把for循环中的异步操作变为同步操作,最后再返回数据,但是就会阻塞其他的同步操作,显然这违背了异步机制。
利用when.all()改造过后的代码:
var http = require(‘http‘), fs =require(‘fs‘), connect = require(‘connect‘), when = require(‘when‘); function readDir () { var path = ‘articles‘; var promise = when.promise(function (resolve, reject, notify) { fs.readdir(path, function (err, files) { if (err) { reject(err); } else { resolve({ "path": path, "files": files }); } }); }); return promise; } function getFileStats (data) { var files = data.files, promiseList = []; for (var i in files) { (function (i) { var promise = when.promise(function (resolve, reject, notify) { fs.stat(data.path + ‘/‘ + files[i], function (err, stats) { if (err) { reject(err) } else { o = { "fileName": files[i].slice(0, -5), "fileTime": stats.mtime.toString().slice(4, 15) }; resolve(o); } }) }); promiseList.push(promise); })(i); } return promiseList; } function printErr (err) { console.log(err); } var app = connect() .use(function(req, res) { if (req.url === ‘/favicon.ico‘) { return; } else { when.all( readDir().then(getFileStats, printErr) ).then(function (o) { res.writeHead(200, {‘Content-Type‘: ‘text/html; charset="UTF-8"‘}); res.write(JSON.stringify(o)); res.end(); }, printErr); } }) .listen(8080);
浏览器上的结果:
when.all(array)需要传入一个promise数组,其中数组中的每一个promise在fulfill时都会执行resolve(data),这里的data就是前面for循环中每一次异步操作中获得的数据。在when.all()执行过后,会将每次resolve(data)中的data拼成一个数组,通过then()传递给下一个promise。拼成数组这个操作不需要手动执行,when.js会替我们做的。
when.race()
when.race()为异步任务提供了竞争机制。比如在N个异步任务中,在最快获得结果的任务之后做某些事情,可以使用when.race()。
使用when.race()同when.all()类似,传入的参数都是promise数组,返回promise数组中最早fulfill的promise,或者返回最早reject的promise。
function a () { var promise = when.promise(function (resolve, reject, notify) { setTimeout(function () { resolve(‘a‘); }, 1002); }); return promise; } function b () { var promise = when.promise(function (resolve, reject, notify) { setTimeout(function () { resolve(‘b‘); }, 1001); }); return promise; } function c (data) { console.log(data + ‘ first!‘); } when.race([a(), b()]).then(c);
执行结果:b first!
跟when.race()类似的方法是when.any(),具体的区别可以异步API。
when.js还有其他用法,本文只列举了一些常用的用法。再次强调的是,when.js的实现规范是Promises/A+,使用其他实现Promises/A+的库也可以达到一样的效果。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。