用Promise組織程序

jopen 9年前發布 | 32K 次閱讀 Promise JavaScript開發

一、Promise基本用法

很多文章介紹Promise給的例子是這樣的:

new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', location.href, true);
    xhr.send(null);
    xhr.addEventListener('readystatechange', function(e){
       if(xhr.readyState === 4) {
           if(xhr.status === 200) {
               resolve(xhr.responseText);
           } else {
               reject(xhr);
           }
       }
    })
}).then(function(txt){
    console.log();
})

一定會有小朋友好奇,說尼瑪,這不是比回調還惡心?

這種寫法的確是能跑得起來啦……不過,按照Promise的設計初衷,我們編程需要使用的概念并非"Promise對象",而是promise函數,凡是以Promise作為返回值的函數,稱為promise函數(我暫且取了這個名字)。所以應該是這樣的:

function doSth() {
    return new Promise(function(resolve, reject) {
        //做點什么異步的事情
        //結束的時候調用 resolve,比如:
        setTimeout(function(){
            resolve(); //這里才是真的返回
        },1000)
    })
}

如果你不喜歡這樣的寫法,還可以使用defer風格的promise

function doSth2() {
    var defer = Promise.defer();
    //做點什么異步的事情
    //結束的時候調用 defer.resolve,比如:
    setTimeout(function(){
        defer.resolve(); //這里才是真的返回
    },1000)

    return defer.promise;
}

總之兩種是沒什么區別啦。

然后你就可以這么干:

doSth().then(doSth2).then(doSth);

這樣看起來就順眼多了吧。

其實說簡單點,promise最大的意義在于把嵌套的回調變成了鏈式調用(詳見第三節,順序執行),比如以下

//回調風格
loadImage(img1,function(){
    loadImage(img2,function(){
        loadImage(img3,function(){

        });
    });
});

//promise風格
Promise.resolve().then(function(){
    return loadImage(img1);
}).then(function(){
    return loadImage(img2);
}).then(function(){
    return loadImage(img3);
});

后者嵌套關系更少,在多數人眼中會更易于維護一些。

二、Promise風格的API

在去完cssconf回杭州的火車上,我順手把一些常見的JS和API寫成了promise方式:

function get(uri){
    return http(uri, 'GET', null);
}

function post(uri,data){
    if(typeof data === 'object' && !(data instanceof String || (FormData && data instanceof FormData))) {
        var params = [];
        for(var p in data) {
            if(data[p] instanceof Array) {
                for(var i = 0; i < data[p].length; i++) {
                    params.push(encodeURIComponent(p) + '[]=' + encodeURIComponent(data[p][i]));
                }
            } else {
                params.push(encodeURIComponent(p) + '=' + encodeURIComponent(data[p]));
            }
        }
        data = params.join('&');
    }


    return http(uri, 'POST', data || null, {
        "Content-type":"application/x-www-form-urlencoded"
    });
}

function http(uri,method,data,headers){
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open(method,uri,true);
        if(headers) {
            for(var p in headers) {
                xhr.setRequestHeader(p, headers[p]);
            }
        }
        xhr.addEventListener('readystatechange',function(e){
            if(xhr.readyState === 4) {
                if(String(xhr.status).match(/^2\d\d$/)) {
                    resolve(xhr.responseText);
                } else {
                    reject(xhr);
                }
            }
        });
        xhr.send(data);
    })
}

function wait(duration){
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}

function waitFor(element,event,useCapture){
    return new Promise(function(resolve, reject) {
        element.addEventListener(event,function listener(event){
            resolve(event)
            this.removeEventListener(event, listener, useCapture);
        },useCapture)
    })
}

function loadImage(src) {
    return new Promise(function(resolve, reject) {
        var image = new Image;
        image.addEventListener('load',function listener() {
            resolve(image);
            this.removeEventListener('load', listener, useCapture);
        });
        image.src = src;
        image.addEventListener('error',reject);
    })
}

function runScript(src) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.src = src;
        script.addEventListener('load',resolve);
        script.addEventListener('error',reject);
        (document.getElementsByTagName('head')[0] || document.body || document.documentElement).appendChild(script);
    })
}

function domReady() {
    return new Promise(function(resolve, reject) {
        if(document.readyState === 'complete') {
            resolve();
        } else {
            document.addEventListener('DOMContentLoaded',resolve);
        }
    })
}

看到了嗎,Promise風格API跟回調風格的API不同,它的參數跟同步的API是一致的,但是它的返回值是個Promise對象,要想得到真正的結果,需要在then的回調里面拿到。

值得一提的是,下面這樣的寫法:

waitFor(document.documentElement,'click').then(function(){
    console.log('document clicked!')
})

這樣的事件響應思路上是比較新穎的,不同于事件機制,它的事件處理代碼僅會執行一次。

通過這些函數的組合,我們可以更優雅地組織異步代碼,請繼續往下看。

三、使用Promise組織異步代碼

函數調用/并行

Promise.all跟then的配合,可以視為調用部分參數為Promise提供的函數。譬如,我們現在有一個接受三個參數的函數

function print(a, b, c) {
    console.log(a + b + c);
}

現在我們調用print函數,其中a和b是需要異步獲取的:

var c = 10;

print(geta(), getb(), 10); //這是同步的寫法

Promise.all([geta(), getb(), 10]).then(print); //這是 primise 的異步寫法

競爭

如果說Primise.all是promise對象之間的“與”關系,那么Promise.race就是promise對象之間的“或”關系。

比如,我要實現“點擊按鈕或者5秒鐘之后執行”

var btn = document.getElementsByTagName('button');

Promise.race(wait(5000), waitFor(btn, click)).then(function(){
    console.log('run!')
})

異常處理

異常處理一直是回調的難題,而promise提供了非常方便的catch方法:

在一次promise調用中,任何的環節發生reject,都可以在最終的catch中捕獲到

Promise.resolve().then(function(){
    return loadImage(img1);
}).then(function(){
    return loadImage(img2);
}).then(function(){
    return loadImage(img3);
}).catch(function(err){
    //錯誤處理
})

這非常類似于JS的try catch功能。

復雜流程

接下來,我們來看比較復雜的情況。

promise有一種非常重要的特性:then的參數,理論上應該是一個promise函數,而如果你傳遞的是普通函數,那么默認會把它當做已經resolve了的promise函數。

這樣的特性讓我們非常容易把promise風格的函數跟已有代碼結合起來。

為了方便傳參數,我們編寫一個currying函數,這是函數式編程里面的基本特性,在這里跟promise非常搭,所以就實現一下:

function currying(){
    var f = arguments[0];
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
        args.push.apply(args,arguments);
        return f.apply(this,args);
    }
}

currying會給某個函數"固化"幾個參數,并且返回接受剩余參數的函數。比如之前的函數,可以這么玩:

var print2 = currying(print,11);

print2(2, 3); //得到 11 + 2 + 3 的結果,16

var wait1s = currying(wait,1000);

wait1s().then(function(){
    console.log('after 1s!');
})

有了currying,我們就可以愉快地來玩鏈式調用了,比如以下代碼:

Promise.race([
    domReady().then(currying(wait,5000)), 
    waitFor(btn, click)])
    .then(currying(runScript,'a.js'))
    .then(function(){
        console.log('loaded');
        return Promise.resolve();
    });

表示“domReady發生5秒或者點擊按鈕后,加載腳本并執行,完畢后打印loaded”。

四、總結

promise作為一個新的API,是幾乎完全可polyfill的,這在JS發展中這是很少見的情況,它的API本身沒有什么特別的功能,但是它背后代表的編程思路是很有價值的。

來自:http://www.w3ctech.com/topic/721

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!