上篇文章介紹了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只是一種規范,從Chrome32起開始支持Promises。
已經實現這個標准的庫有Q、when.js、Dojo.deferred、jQuery.deferred等。
鑒於瀏覽器已經支持Promises,所以盡量使用原生的Promises,對於不支持Promises的瀏覽器,建議使用這個polyfill: promise-5.0.0.js,Promises其實很簡單:
function a (num) { var promise = new Promise(function (resolve, reject) { 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 = new Promise(function (resolve, reject) { 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(11) .then(b, alert) .then(c);
上面這段代碼做的事情是:輸入num,1s后輸出num + 1,2s后輸出(num + 1) * 10,最后輸出done。
相比a、b兩個方法本身,通過Promises定義的方法多了這些東西:
定義一個promise。
var promise = new Promise(function (resolve, reject) {));
完成方法的功能時,調用resolve(data)。
resolve(count);
最后返回promise。
return promise;
首先熟悉一下Promises是怎樣定義promise狀態的。Promises/A+是這樣規定的:
- 一個promise必須是下面三種狀態之一:pending, fulfilled, rejected
- 當一個promise是pending狀態:
- 可以轉變到fulfilled狀態或者rejected狀態
- 當一個promise是fulfilled狀態:
- 不可以轉變到其他任何狀態
- 必須有一個不能改變的value
- 當一個promise是rejected狀態:
- 不可以轉變到其他任何狀態
- 必須有一個不可改變的reason
上面代碼在a()中使用new Promise()實例化了一個promise后,這個promise默認狀態是pending。
promise.then()方法有2個參數,分別是onFulfilled、onRejected,這2個參數都是方法名(也就是回調函數),通過這2個參數可以對應promise的fulfilled和rejected2種狀態。
通過上面的代碼來解釋就是:
-
當a()正常執行結束時,調用resolve(data)將promise的狀態改變為fulfilled,並且通過then()的第一個參數onFulfilled將參數data傳遞給下一個方法b()。
-
當a()非正常結束,這里認為a()在執行過程中出現了異常時,調用reject(reason)將promise的狀態改變為rejected,並且通過then()的第二個參數onRejected將參數reason傳遞給下一個方法alert()。
在前面說到流程控制的時候提到的for循環的問題還沒有解決:如果想等N個for循環中的回調函數執行結束之后做某些事情,該怎么辦?
這時候該用到Promise.all()方法了,比如前面提到的一段代碼,需求是這樣:
在Node.js中,創建http服務器,讀取當前目錄下articles目錄中的所有文件,遍歷所有文件,並根據“目錄+文件名”讀取文件的最后修改時間,最終返回[{文件名,文件修改時間}, {文件名,文件修改時間}, ...]
這樣一個列表到客戶端。
這里存在的問題是,讀取目錄的操作是異步的,for循環讀取文件狀態的操作也是異步的,而在for循環中的所有異步操作都執行結束后,需要調用response.writeHead()與response.write()將所有異步數據返回到客戶端。在使用when.js之前,我能想到的就是把for循環中的異步操作變為同步操作,最后再返回數據,但是就會阻塞其他的同步操作,顯然這違背了異步機制。
利用Promise.all()改造過后的代碼:
var http = require('http'), fs =require('fs'), connect = require('connect'), Promise = require('promise'); function readDir (path) { return new Promise(function (resolve, reject) { fs.readdir(path, function (err, files) { if (err) { reject(err); } else { resolve({ "path": path, "files": files }); } }); }); } function getFileStats (data) { var files = data.files, promiseList = []; for (var i in files) { (function (i) { var promise = new Promise(function (resolve, reject) { fs.stat(data.path + '/' + files[i], function (err, stats) { if (err) { reject(err) } else { var o = { "fileName": files[i].slice(0, -5), "fileTime": stats.mtime.toString().slice(4, 15) }; resolve(o); } }) }); promiseList.push(promise); })(i); } return Promise.all(promiseList); } var app = connect() .use(function(req, res) { if (req.url === '/favicon.ico') { return; } else { readDir('articles/fe').then(getFileStats) .then(function (o) { res.writeHead(200, {'Content-Type': 'text/html; charset="UTF-8"'}); res.write(JSON.stringify(o)); res.end(); }); } }) .listen(8080);
瀏覽器上的結果:
Promise.all(array)需要傳入一個promise數組,其中數組中的每一個promise在fulfill時都會執行resolve(data),這里的data就是前面for循環中每一次異步操作中獲得的數據。在promise.all()執行過后,會將每次resolve(data)中的data拼成一個數組,通過then()傳遞給下一個promise。
Promise.race()
Promise.race()為異步任務提供了競爭機制。比如在N個異步任務中,在最快獲得結果的任務之后做某些事情,可以使用Promise.race()。
使用Promise.race()同Promise.all()類似,傳入的參數都是promise數組,返回promise數組中最早fulfill的promise,或者返回最早reject的promise。
function a () { var promise = new Promise(function (resolve, reject) { setTimeout(function () { resolve('a'); }, 1002); }); return promise; } function b () { var promise = new Promise(function (resolve, reject) { setTimeout(function () { resolve('b'); }, 1001); }); return promise; } function c (data) { console.log(data + ' first!'); } Promise.race([a(), b()]).then(c);
執行結果:b first!