JavaScript 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只是一種規范,從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!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM