透徹掌握Promise的使用,讀這篇就夠了


透徹掌握Promise的使用,讀這篇就夠了

 

Promise的重要性我認為我沒有必要多講,概括起來說就是必須得掌握,而且還要掌握透徹。這篇文章的開頭,主要跟大家分析一下,為什么會有Promise出現。

在實際的使用當中,有非常多的應用場景我們不能立即知道應該如何繼續往下執行。最重要也是最主要的一個場景就是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事件,當該事件觸發並且符合一定條件時,才能拿到我們想要的數據,之后我們才能開始處理數據。

這樣做看上去並沒有什么麻煩,但是如果這個時候,我們還需要做另外一個ajax請求,這個新的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); // 偽代碼 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(甚至更多)仍然依賴上一個請求的時候,我們的代碼就變成了一場災難。這場災難,往往也被稱為回調地獄。

因此我們需要一個叫做Promise的東西,來解決這個問題。

當然,除了回調地獄之外,還有一個非常重要的需求:為了我們的代碼更加具有可讀性和可維護性,我們需要將數據請求與數據處理明確的區分開來。上面的寫法,是完全沒有區分開,當數據變得復雜時,也許我們自己都無法輕松維護自己的代碼了。這也是模塊化過程中,必須要掌握的一個重要技能,請一定重視。

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

// 一個簡單的封裝 function want() { console.log('這是你想要執行的代碼'); } function fn(want) { console.log('這里表示執行了一大堆各種代碼'); // 其他代碼執行完畢,最后執行回調函數 want && want(); } fn(want);

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

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

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的基礎知識。

一、 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。

二、 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() {})

三、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最常用的一個場景,因此我們一定要非常非常熟悉它,這樣才能在應用的時候更加靈活。

四、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); })

五、 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/promiseApps

往下閱讀例子之前,請一定要對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的處理,對於彈窗的處理,都是可以擴展成為一個通用的模塊的。慢慢積累多了,你的開發效率就可以得到明顯的提高,這些積累,也將會變成你的優勢所在。

 


免責聲明!

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



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