前端基礎進階(十三):透徹掌握Promise的使用,讀這篇就夠了

Promise:高手必備

Promise的重要性我認爲我沒有必要多講,歸納起來講就是必須得掌握,並且還要掌握透徹。這篇文章的開頭,主要跟你們分析一下,爲何會有Promise出現。html

在實際的使用當中,有很是多的應用場景咱們不能當即知道應該如何繼續往下執行。最重要也是最主要的一個場景就是ajax請求。通俗來講,因爲網速的不一樣,可能你獲得返回值的時間也是不一樣的,這個時候咱們就須要等待,結果出來了以後才知道怎麼樣繼續下去。前端

// 簡單的ajax原生實現
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);
    }
}

在ajax的原生實現中,利用了onreadystatechange事件,當該事件觸發而且符合必定條件時,才能拿到咱們想要的數據,以後咱們才能開始處理數據。react

這樣作看上去並無什麼麻煩,可是若是這個時候,咱們還須要作另一個ajax請求,這個新的ajax請求的其中一個參數,得從上一個ajax請求中獲取,這個時候咱們就不得不以下這樣作:jquery

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);

        // 僞代碼
        var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
        var XHR2 = new XMLHttpRequest();
        XHR2.open('GET', url, true);
        XHR2.send();
        XHR2.onreadystatechange = function() {
            ...
        }
    }
}

當出現第三個ajax(甚至更多)仍然依賴上一個請求的時候,咱們的代碼就變成了一場災難。這場災難,每每也被稱爲回調地獄git

所以咱們須要一個叫作Promise的東西,來解決這個問題。es6

固然,除了回調地獄以外,還有一個很是重要的需求:爲了咱們的代碼更加具備可讀性和可維護性,咱們須要將數據請求與數據處理明確的區分開來。上面的寫法,是徹底沒有區分開,當數據變得複雜時,也許咱們本身都沒法輕鬆維護本身的代碼了。這也是模塊化過程當中,必需要掌握的一個重要技能,請必定重視。github

從前面幾篇文中的知識咱們能夠知道,當咱們想要確保某代碼在誰誰以後執行時,咱們能夠利用函數調用棧,將咱們想要執行的代碼放入回調函數中。ajax

// 一個簡單的封裝
function want() {
    console.log('這是你想要執行的代碼');
}

function fn(want) {
    console.log('這裏表示執行了一大堆各類代碼');

    // 其餘代碼執行完畢,最後執行回調函數
    want && want();
}

fn(want);

利用回調函數封裝,是咱們在初學JavaScript時經常會使用的技能。segmentfault

確保咱們想要的代碼壓後執行,除了利用函數調用棧的執行順序以外,咱們還能夠利用上一篇文章所述的隊列機制。數組

function want() {
    console.log('這是你想要執行的代碼');
}

function fn(want) {
    // 將想要執行的代碼放入隊列中,根據事件循環的機制,咱們就不用非得將它放到最後面了,由你自由選擇
    want && setTimeout(want, 0);
    console.log('這裏表示執行了一大堆各類代碼');
}

fn(want);

若是瀏覽器已經支持了原生的Promise對象,那麼咱們就知道,瀏覽器的js引擎裏已經有了Promise隊列,這樣就能夠利用Promise將任務放在它的隊列中去。

function want() {
    console.log('這是你想要執行的代碼');
}

function fn(want) {
    console.log('這裏表示執行了一大堆各類代碼');

    // 返回Promise對象
    return new Promise(function(resolve, reject) {
        if (typeof want == 'function') {
            resolve(want);
        } else {
            reject('TypeError: '+ want +'不是一個函數')
        }
    })
}

fn(want).then(function(want) {
    want();
})

fn('1234').catch(function(err) {
    console.log(err);
})

看上去變得更加複雜了。但是代碼變得更加健壯,處理了錯誤輸入的狀況。

爲了更好的往下擴展Promise的應用,這裏須要先跟你們介紹一下Promsie的基礎知識。

1、 Promise對象有三種狀態,他們分別是:

  • pending: 等待中,或者進行中,表示尚未獲得結果
  • resolved(Fulfilled): 已經完成,表示獲得了咱們想要的結果,能夠繼續往下執行
  • rejected: 也表示獲得結果,可是因爲結果並不是咱們所願,所以拒絕執行

這三種狀態不受外界影響,並且狀態只能從pending改變爲resolved或者rejected,而且不可逆。在Promise對象的構造函數中,將一個函數做爲第一個參數。而這個函數,就是用來處理Promise的狀態變化。

new Promise(function(resolve, reject) {
    if(true) { resolve() };
    if(false) { reject() };
})

上面的resolve和reject都爲一個函數,他們的做用分別是將狀態修改成resolved和rejected。

2、 Promise對象中的then方法,能夠接收構造函數中處理的狀態變化,並分別對應執行。then方法有2個參數,第一個函數接收resolved狀態的執行,第二個參數接收reject狀態的執行。

function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    }).then(function() {
        console.log('參數是一個number值');
    }, function() {
        console.log('參數不是一個number值');
    })
}

fn('hahha');
fn(1234);

then方法的執行結果也會返回一個Promise對象。所以咱們能夠進行then的鏈式執行,這也是解決回調地獄的主要方式。

function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    })
    .then(function() {
        console.log('參數是一個number值');
    })
    .then(null, function() {
        console.log('參數不是一個number值');
    })
}

fn('hahha');
fn(1234);
then(null, function() {}) 就等同於catch(function() {})

3、Promise中的數據傳遞

你們自行從下面的例子中領悟吧。

var fn = function(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve(num);
        } else {
            reject('TypeError');
        }
    })
}

fn(2).then(function(num) {
    console.log('first: ' + num);
    return num + 1;
})
.then(function(num) {
    console.log('second: ' + num);
    return num + 1;
})
.then(function(num) {
    console.log('third: ' + num);
    return num + 1;
});

// 輸出結果
first: 2
second: 3
third: 4

OK,瞭解了這些基礎知識以後,咱們再回過頭,利用Promise的知識,對最開始的ajax的例子進行一個簡單的封裝。看看會是什麼樣子。

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';

// 封裝一個get請求的方法
function getJSON(url) {
    return new Promise(function(resolve, reject) {
        var XHR = new XMLHttpRequest();
        XHR.open('GET', url, true);
        XHR.send();

        XHR.onreadystatechange = function() {
            if (XHR.readyState == 4) {
                if (XHR.status == 200) {
                    try {
                        var response = JSON.parse(XHR.responseText);
                        resolve(response);
                    } catch (e) {
                        reject(e);
                    }
                } else {
                    reject(new Error(XHR.statusText));
                }
            }
        }
    })
}

getJSON(url).then(resp => console.log(resp));

爲了健壯性,處理了不少可能出現的異常,總之,就是正確的返回結果,就resolve一下,錯誤的返回結果,就reject一下。而且利用上面的參數傳遞的方式,將正確結果或者錯誤信息經過他們的參數傳遞出來。

如今全部的庫幾乎都將ajax請求利用Promise進行了封裝,所以咱們在使用jQuery等庫中的ajax請求時,均可以利用Promise來讓咱們的代碼更加優雅和簡單。這也是Promise最經常使用的一個場景,所以咱們必定要很是很是熟悉它,這樣才能在應用的時候更加靈活。

4、Promise.all

當有一個ajax請求,它的參數須要另外2個甚至更多請求都有返回結果以後才能肯定,那麼這個時候,就須要用到Promise.all來幫助咱們應對這個場景。

Promise.all接收一個Promise對象組成的數組做爲參數,當這個數組全部的Promise對象狀態都變成resolved或者rejected的時候,它纔會去調用then方法。

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10';

function renderAll() {
    return Promise.all([getJSON(url), getJSON(url1)]);
}

renderAll().then(function(value) {
    // 建議你們在瀏覽器中看看這裏的value值
    console.log(value);
})

5、 Promise.race

與Promise.all類似的是,Promise.race都是以一個Promise對象組成的數組做爲參數,不一樣的是,只要當數組中的其中一個Promsie狀態變成resolved或者rejected時,就能夠調用.then方法了。而傳遞給then方法的值也會有所不一樣,你們能夠再瀏覽器中運行下面的例子與上面的例子進行對比。

function renderRace() {
    return Promise.race([getJSON(url), getJSON(url1)]);
}

renderRace().then(function(value) {
    console.log(value);
})

嗯,我所知道的,關於Promise的基礎知識就這些了,若是還有別的,歡迎你們補充。

那麼接下來,咱們要結合三個不一樣的應用場景來讓你們感覺一下Promise在模塊系統中如何使用。

這裏選擇requirejs是由於學習成本最低,可以快速上手進行簡單的運用。接下來的這些例子,會涉及到不少其餘的知識,所以若是想要完全掌握,必定要動手實踐,本身試着完成一遍。

我在github上建立了對應的項目,你們能夠直接clone下來進行學習。這樣學習效果會更好。

項目地址: https://github.com/yangbo5207...

往下閱讀例子以前,請必定要對requirejs有一個簡單的瞭解。

requirejs中文文檔 http://www.requirejs.cn/

代碼結構

項目的代碼結果如上圖所示,全部的html文件都放在根目錄下。

  • pages: html直接引入的js
  • libs: 經常使用的庫
  • components: 針對項目自定義的模塊

首先爲了可以讓require起做用,咱們須要在html中引入require.js,寫法以下:

// index.js爲入口文件
<script data-main="./pages/index.js" src="./libs/require.js"></script>

在入口的index.js中,咱們能夠對經常使用的模塊進行映射配置,這樣在引入時就能夠少寫一些代碼。

// 具體的配置項的含義,請參閱require的中文文檔
requirejs.config({
    baseUrl: './',
    paths: {
        jquery: "./libs/jquery-3.2.0",
        API: './libs/API',
        request: './libs/request',
        calendar: './components/calendar',
        imageCenter: './components/imageCenter',
        dialog: './components/Dialog'
    }
})

配置以後,那麼咱們在其餘模塊中,引入配置過的模塊,就能夠簡單的這樣寫:

var $ = require('jquery');

若是不進行配置,也能夠這樣引入模塊:

require('./components/button');

咱們可使用define定義一個模塊:

// 其餘方式請參閱文檔
define(function(require) {

})

使用return能夠直接對外提供方法:

// 在其餘模塊經過require引入時獲得的值,就是這裏返回的值
define(function(require) {
    return {
        a: 1
    }
})

OK,瞭解上面這些,應付基礎的使用已經沒有問題了。咱們接下來重點總結第一個經常使用的應用場景:ajax。

關於ajax的簡單使用和簡單封裝,咱們在上面都已經講過了,這裏就再也不多說,直接使用jquery封裝好的方法便可。而咱們須要處理的問題在於,如何有效的將ajax的數據請求和數據處理分別放在不一樣的模塊中進行管理,這樣作的主要目的在於下降後期維護成本,便於管理。

來看看怎麼樣簡單操做的。

首先,將全部的url放在一個模塊中統一處理。

// libs/API.js
define(function() {
    return {
        dayInfo: 'https://hq.tigerbrokers.com/fundamental/finance_calendar/get_day/2017-04-03',
        typeInfo: 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-04-15'
    }
})

在實際開發中,url並非直接經過字符串就能直接確認的,某些url還須要經過參數拼接等,這個時候須要咱們靈活處理。

第二步,將全部的數據請求這個動做放在同一個模塊中統一管理。

// libs/request.js
define(function(require) {
    var API = require('API');

    // 由於jQuery中的get方法也是經過Promise進行了封裝,最終返回的是一個Promise對象,所以這樣咱們就能夠將數據請求與數據處理放在不一樣的模塊
    // 這樣咱們就可使用一個統一的模塊來管理全部的數據請求

    // 獲取當天的信息
    getDayInfo = function() {
        return $.get(API.dayInfo);
    }

    // 獲取type信息
    getTypeInfo = function() {
        return $.get(API.typeInfo);
    };

    return {
        getDayInfo: getDayInfo,
        getTypeInfo: getTypeInfo
    }
});

在這個模塊中,咱們還能夠對拿到的數據進行一些你須要的過濾處理,確保最終返回給下一個模塊的數據是可以直接使用的。

第三步:就是拿到數據而且處理數據了。

// components/calendar.js
define(function(require) {
    var request = require('request');

    // 拿到數據以後,須要處理的組件,能夠根據數據渲染出需求想要的樣式
    // 固然這裏爲了簡化,就僅僅只是輸出數據就好了,在實際中,拿到數據以後還要進行相應的處理

    request.getTypeInfo()
        .then(function(resp) {

            // 拿到數據,並執行處理操做
            console.log(resp);
        })

    // 這樣,咱們就把請求數據,與處理數據分離開來,維護起來就更加方便了,代碼結構也足夠清晰
})

這就是我所瞭解的處理ajax的比較好的一個方式,若是你有其餘更好的方式也歡迎分享。

第二個應用場景就是圖片加載的問題。
在一些實際應用中,經常會有一些圖片須要放置在某一個塊中,好比頭像,好比某些圖片列表。但是源圖片的尺寸可能很難保證長寬比例都是一致的,若是咱們直接給圖片設定寬高,就有可能致使圖片變形。變形以後高大上的頁面就直接垮掉了。

所以爲了解決這個問題,咱們須要一個定製的image組件來解決這個問題。咱們指望圖片可以根據本身的寬高比,合理的縮放,保證在這個塊中不變形的狀況下儘量的顯示更多的內容。

假若有一堆圖片,以下:

<section class="img-wrap">
    <div class="img-center">
        ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1491191204817&di=48ea9cde3319576ed6e0b6dc6c6b75b4&imgtype=0&src=http%3A%2F%2Fa.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2F342ac65c103853438b3c5f8b9613b07ecb8088ad.jpg)
    </div>

    <div class="img-center">
        ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1491191241712&di=9dbd9c614b82f0b02c92c6e60875983a&imgtype=0&src=http%3A%2F%2Fpic5.qiyipic.com%2Fcommon%2F20130524%2F7dc5679567cd4243a0a41e5bf626ad77.jpg%3Fsrc%3Dfocustat_4_20130527_7)
    </div>

    <div class="img-center">
        ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1491191271233&di=0c9dd2677413beadcccd66b9d4598c6b&imgtype=0&src=http%3A%2F%2Fb.zol-img.com.cn%2Fdesk%2Fbizhi%2Fimage%2F4%2F960x600%2F1390442684896.jpg)
    </div>

    <div class="img-center">
        ![](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1491191294538&di=6474f3b560f2c100e62f118dde7e8d6c&imgtype=0&src=http%3A%2F%2Ff.hiphotos.baidu.com%2Fzhidao%2Fpic%2Fitem%2Fc9fcc3cec3fdfc03dfdfafcad23f8794a4c22618.jpg)
    </div>
</section>

每一張圖片都有一個包裹的div,這些div的寬高,就是咱們指望圖片能保持的寬高。

當圖片寬度值過大時,咱們指望圖片的高度爲100%,而且左右居中。
當圖片高度值過大時,咱們指望圖片的寬度爲100%,而且上下居中。

根據這一點,咱們來看看具體怎麼實現。

首先是樣式的定義很重要。

.img-center {
    width: 200px;
    height: 150px;
    margin: 20px;
    overflow: hidden;
    position: relative;
}

.img-center img {
    display: block;
    position: absolute;
}

.img-center img.aspectFill-x {
    width: 100%;
    top: 50%;
    transform: translateY(-50%);
}

.img-center img.aspectFill-y {
    height: 100%;
    left: 50%;
    transform: translateX(-50%);
}

我分別定義了aspectFill-xaspectFill-y,經過判斷不一樣的寬高比,來決定將他們中的其中一個加入到img標籤的class中去便可。

獲取圖片的原始寬高,須要等到圖片加載完畢以後才能獲取。而當圖片已經存在緩存時,則有一個compete屬性變成true。那麼咱們就能夠根據這些基礎知識,定義一個模塊來處理這件事情。

// components/imageCenter.js
define(function(require) {

    // 利用Promise封裝一個加載函數,這裏也是能夠單獨放在一個功能模塊中進一步優化
    var imageLoad = function(img) {
        return new Promise(function(resolve, reject) {
            if (img.complete) {
                resolve();
            } else {
                img.onload = function(event) {
                    resolve(event);
                }

                img.onerror = function(err) {
                    reject(err);
                }
            }
        })
    }

    var imageCenter = function(domList, mode) {

        domList.forEach(function(item) {
            var img = item.children[0];
            var itemW = item.offsetWidth;
            var itemH = item.offsetHeight;
            var itemR = itemW / itemH;

            imageLoad(img).then(function() {
                var imgW = img.naturalWidth;
                var imgH = img.naturalHeight;
                var imgR = imgW / imgH;

                var resultMode = null;

                switch (mode) {
                    // 這樣寫是由於期待將來能夠擴展其餘的展現方式
                    case 'aspectFill':
                        resultMode = imgR > 1 ? 'aspectFill-x' : 'aspectFill-y';
                        break;
                    case 'wspectFill':
                        resultMode = itemR > imgR ? 'aspectFill-x' : 'aspectFill-y'
                        break;
                    default:
                }

                $(img).addClass(resultMode);
            })
        })
    }

    return imageCenter;
})

那麼在使用時,直接引入這個模塊並調用imageCenter方法便可。

// index.js
var imageCenter = require('imageCenter');
var imageWrapList = document.querySelectorAll('.img-center');
imageCenter(imageWrapList, 'wspectFill');

一堆尺寸亂七八糟的圖片就這樣被馴服了

第三個應用場景,則是自定義彈窗的處理。

這種類型的彈窗隨處可見,並且十分經常使用

所以本身專門定義一個經常使用的彈窗就變得很是有必要,這對於咱們開發效率的提升很是有幫助。固然,我這裏只是簡單的寫了一個簡陋的,僅供參考。

咱們指望的是利用Promise,當咱們點擊確認時,狀態變成resolved,點擊取消時,狀態變成rejected。這樣也方便將彈窗生成與後續的操做處理區分開來。

先定義一個Dialog模塊。使用的是最簡單的方式定義,應該不會有什麼理解上的困難。主要提供了show和hide2個方法,用於展現和隱藏。

// components/Dialog.js
define(function(require) {

    // 利用閉包的特性,判斷是否已經存在實例
    var instance;

    function Dialog(config) {

        this.title = config.title ? config.title : '這是標題';
        this.content = config.content ? config.content : '這是提示內容';

        this.html = '<div class="dialog-dropback">' +
            '<div class="container">' +
                '<div class="head">'+ this.title +'</div>' +
                '<div class="content">'+ this.content +'</div>' +
                '<div class="footer">' +
                    '<button class="cancel">取消</button>' +
                    '<button class="confirm">確認</button>' +
                '</div>' +
            '</div>' +
        '</div>'
    }

    Dialog.prototype = {
        constructor: Dialog,
        show: function() {
            var _this = this;
            if (instance) {
                this.destory();
            }
            $(this.html).appendTo($(document.body));
            instance = this;

            return new Promise(function(resolve, reject) {
                $('.dialog-dropback .cancel').on('click', function(e) {
                    _this.hide();
                    reject(e);
                })

                $('.dialog-dropback .confirm').on('click', function(e) {
                    _this.hide();
                    resolve(e);
                })
            })
        },

        destory: function() {
            instance = null;
            $('.dialog-dropback .cancel').off('click');
            $('.dialog-dropback .confirm').off('click');
            $('.dialog-dropback').remove();
        },

        hide: function() {
            this.destory();
        }
    }

    return function(config) {
        return new Dialog(config);
    }
})

那麼在另一個模塊中須要使用它時:

define(function(require) {
    var Dialog = require('dialog');

    $('button.aspect').on('click', function() {
        Dialog({
            title: '友情提示',
            content: '外面空氣不太好,你肯定你要出門逛逛嗎?'
        }).show().then(function() {
            console.log('你點擊了確認按鈕.');
        }).catch(function() {
            console.log('你點擊了取消按鈕.');
        })
    })
})

這三種場景就介紹完了,主要是須要你們經過源碼來慢慢理解和揣摩。真正掌握以後,相信你們對於Promise在另外的場景中的使用也會變得駕輕就熟。

最後總結一下,這篇文章,涉及到的東西,有點多。大概包括Promise基礎知識,ajax基礎知識,如何利用Promise封裝ajax,如何使用require模塊系統,如何在模塊中使用Promise,而且對應的三個應用場景又各自有許多須要瞭解的知識,所以對於基礎稍差的朋友來講,理解透徹了確定會有一個比較大的進步。固然也會花費你更多的時間。

另外在咱們的工做中還有一件很是重要的事情是須要咱們持續去作的。那就是將經常使用的場景封裝成爲能夠共用的模塊,等到下次使用時,就能夠直接拿來使用而節省很是多的開發時間。好比我這裏對於img的處理,對於彈窗的處理,都是能夠擴展成爲一個通用的模塊的。慢慢積累多了,你的開發效率就能夠獲得明顯的提升,這些積累,也將會變成你的優點所在。

後續的文章我會分享如何利用react與es6模塊系統封裝的共用組件,你們也能夠學習了以後,根據本身的需求,封裝最適合你本身的一套組件。

最後,最近問我怎麼學習的人愈來愈多,我真的有點回答不過來了,我想把我這些文章裏的知識都掌握了,應付畢業以後的第一份工做應該不是什麼問題的吧?並且爲了大家可以掌握Promise的使用,我還專門給讀者老爺們建立了一個項目,列舉了整整三個實例,還有源代碼供大家學習,我學Promise的時候,找很久都沒找到一個稍微接近實際應用的案例,學了很久才知道怎麼使用,效率之低可想而知。因此靜下心來慢慢學習吧,花點時間是值得的 ~ ~ 。

前端基礎進階系列目錄

clipboard.png

相關文章
相關標籤/搜索