原文地址 http://www.cnblogs.com/wangfupeng1988/p/6515855.html 未經做者容許不得轉載!html
從 jquery v1.5 發佈通過若干時間以後,Promise 終於出如今了 ES6 的標準中,而當下 ES6 也正在被大規模使用。前端
本節展現的代碼參考這裏node
Promise
進行封裝仍是拿以前講 jquery deferred
對象時的那段setTimeout
程序jquery
var wait = function () { var task = function () { console.log('執行完成') } setTimeout(task, 2000) } wait()
以前咱們使用 jquery 封裝的,接下來將使用 ES6 的Promise
進行封裝,你們注意看有何不一樣。git
Promise
進行封裝const wait = function () { // 定義一個 promise 對象 const promise = new Promise((resolve, reject) => { // 將以前的異步操做,包括到這個 new Promise 函數以內 const task = function () { console.log('執行完成') resolve() // callback 中去執行 resolve 或者 reject } setTimeout(task, 2000) }) // 返回 promise 對象 return promise }
注意看看程序中的註釋,那都是重點部分。從總體看來,感受此次比用 jquery 那次簡單一些,邏輯上也更加清晰一些。es6
new Promise((resolve,reject) => {.....})
包裝起來,最後return
便可callback
中執行resolve()
(代表成功了,失敗的話執行reject
)接着上面的程序繼續往下寫。wait()
返回的確定是一個promise
對象,而promise
對象有then
屬性。github
const w = wait() w.then(() => { console.log('ok 1') }, () => { console.log('err 1') }).then(() => { console.log('ok 2') }, () => { console.log('err 2') })
then
仍是和以前同樣,接收兩個參數(函數),第一個在成功時(觸發resolve
)執行,第二個在失敗時(觸發reject
)時執行。並且,then
還能夠進行鏈式操做。web
以上就是 ES6 的Promise
的基本使用演示。看完你可能會以爲,這跟以前講述 jquery 的不差很少嗎 ———— 對了,這就是我要在以前先講 jquery 的緣由,讓你感受一篇一篇看起來如絲般順滑!面試
接下來,將詳細說一下 ES6 Promise
的一些比較常見的用法,敬請期待吧!ajax
上一節對 ES6 的 Promise 有了一個最簡單的介紹,這一節詳細說一下 Promise 那些最多見的功能
本節展現的代碼參考這裏
Promise.all
和Promise.race
的應用Promise.resolve
的應用由於如下全部的代碼都會用到Promise
,所以乾脆在全部介紹以前,先封裝一個Promise
,封裝一次,爲下面屢次應用。
const fs = require('fs') const path = require('path') // 後面獲取文件路徑時候會用到 const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) // 注意,這裏執行 reject 是傳遞了參數,後面會有地方接收到這個參數 } else { resolve(data.toString()) // 注意,這裏執行 resolve 時傳遞了參數,後面會有地方接收到這個參數 } }) }) }
以上代碼一個一段 nodejs 代碼,將讀取文件的函數fs.readFile
封裝爲一個Promise
。通過上一節的學習,我想你們確定都能看明白代碼的含義,要是看不明白,你就須要回爐重造了!
咱們要使用上面封裝的readFilePromise
讀取一個 json 文件../data/data2.json
,這個文件內容很是簡單:{"a":100, "b":200}
先將文件內容打印出來,代碼以下。你們須要注意,readFilePromise
函數中,執行resolve(data.toString())
傳遞的參數內容,會被下面代碼中的data
參數所接收到。
const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) })
再加一個需求,在打印出文件內容以後,我還想看看a
屬性的值,代碼以下。以前咱們已經知道then
能夠執行鏈式操做,若是then
有多步驟的操做,那麼前面步驟return
的值會被當作參數傳遞給後面步驟的函數,以下面代碼中的a
就接收到了return JSON.parse(data).a
的值
const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { // 第一步操做 console.log(data) return JSON.parse(data).a // 這裏將 a 屬性的值 return }).then(a => { // 第二步操做 console.log(a) // 這裏能夠獲取上一步 return 過來的值 })
總結一下,這一段內容提到的「參數傳遞」其實有兩個方面:
resolve
傳遞的值,會被第一個then
處理時接收到then
有鏈式操做,前面步驟返回的值,會被後面的步驟獲取到咱們知道then
會接收兩個參數(函數),第一個參數會在執行resolve
以後觸發(還能傳遞參數),第二個參數會在執行reject
以後觸發(其實也能夠傳遞參數,和resolve
傳遞參數同樣),可是上面的例子中,咱們沒有用到then
的第二個參數。這是爲什麼呢 ———— 由於不建議這麼用。
對於Promise
中的異常處理,咱們建議用catch
方法,而不是then
的第二個參數。請看下面的代碼,以及註釋。
const fullFileName = path.resolve(__dirname, '../data/data2.json') const result = readFilePromise(fullFileName) result.then(data => { console.log(data) return JSON.parse(data).a }).then(a => { console.log(a) }).catch(err => { console.log(err.stack) // 這裏的 catch 就能捕獲 readFilePromise 中觸發的 reject ,並且能接收 reject 傳遞的參數 })
在若干個then
串聯以後,咱們通常會在最後跟一個.catch
來捕獲異常,並且執行reject
時傳遞的參數也會在catch
中獲取到。這樣作的好處是:
then
的兩個參數,就會出現分支,影響閱讀)try - catch
的樣子,更易理解若是如今有一個需求:先讀取data2.json
的內容,當成功以後,再去讀取data1.json
。這樣的需求,若是用傳統的callback
去實現,會變得很麻煩。並且,如今只是兩個文件,若是是十幾個文件這樣作,寫出來的代碼就無法看了(臭名昭著的callback-hell
)。可是用剛剛學到的Promise
就能夠輕鬆勝任這項工做
const fullFileName2 = path.resolve(__dirname, '../data/data2.json') const result2 = readFilePromise(fullFileName2) const fullFileName1 = path.resolve(__dirname, '../data/data1.json') const result1 = readFilePromise(fullFileName1) result2.then(data => { console.log('data2.json', data) return result1 // 此處只需返回讀取 data1.json 的 Promise 便可 }).then(data => { console.log('data1.json', data) // data 便可接收到 data1.json 的內容 })
上文「參數傳遞」提到過,若是then
有鏈式操做,前面步驟返回的值,會被後面的步驟獲取到。可是,若是前面步驟返回值是一個Promise
的話,狀況就不同了 ———— 若是前面返回的是Promise
對象,後面的then
將會被當作這個返回的Promise
的第一個then
來對待 ———— 若是你這句話看不懂,你須要將「參數傳遞」的示例代碼和這裏的示例代碼聯合起來對比着看,而後體會這句話的意思。
Promise.all
和Promise.race
的應用我還得繼續提出更加奇葩的需求,以演示Promise
的各個經常使用功能。以下需求:
讀取兩個文件data1.json
和data2.json
,如今我須要一塊兒讀取這兩個文件,等待它們所有都被讀取完,再作下一步的操做。此時須要用到Promise.all
// Promise.all 接收一個包含多個 promise 對象的數組 Promise.all([result1, result2]).then(datas => { // 接收到的 datas 是一個數組,依次包含了多個 promise 返回的內容 console.log(datas[0]) console.log(datas[1]) })
讀取兩個文件data1.json
和data2.json
,如今我須要一塊兒讀取這兩個文件,可是隻要有一個已經讀取了,就能夠進行下一步的操做。此時須要用到Promise.race
// Promise.race 接收一個包含多個 promise 對象的數組 Promise.race([result1, result2]).then(data => { // data 即最早執行完成的 promise 的返回值 console.log(data) })
Promise.resolve
的應用從 jquery 引出,到此即將介紹完 ES6 的Promise
,如今咱們再回歸到 jquery 。
你們都是到 jquery v1.5 以後$.ajax()
返回的是一個deferred
對象,而這個deferred
對象和咱們如今正在學習的Promise
對象已經很接近了,可是還不同。那麼 ———— deferred
對象可否轉換成 ES6 的Promise
對象來使用??
答案是能!須要使用Promise.resolve
來實現這一功能,請看如下代碼:
// 在瀏覽器環境下運行,而非 node 環境 cosnt jsPromise = Promise.resolve($.ajax('/whatever.json')) jsPromise.then(data => { // ... })
注意:這裏的Promise.resolve
和文章最初readFilePromise
函數內部的resolve
函數可千萬不要混了,徹底是兩碼事兒。JS 基礎好的同窗一看就明白,而這裏看不明白的同窗,要特別注意。
實際上,並非Promise.resolve
對 jquery 的deferred
對象作了特殊處理,而是Promise.resolve
可以將thenable
對象轉換爲Promise
對象。什麼是thenable
對象?———— 看個例子
// 定義一個 thenable 對象 const thenable = { // 所謂 thenable 對象,就是具備 then 屬性,並且屬性值是以下格式函數的對象 then: (resolve, reject) => { resolve(200) } } // thenable 對象能夠轉換爲 Promise 對象 const promise = Promise.resolve(thenable) promise.then(data => { // ... })
上面的代碼就將一個thenalbe
對象轉換爲一個Promise
對象,只不過這裏沒有異步操做,全部的都會同步執行,可是不會報錯的。
其實,在咱們的平常開發中,這種將thenable
轉換爲Promise
的需求並很少。真正須要的是,將一些異步操做函數(如fs.readFile
)轉換爲Promise
(就像文章一開始readFilePromise
作的那樣)。這塊,咱們後面會在介紹Q.js
庫時,告訴你們一個簡單的方法。
以上都是一些平常開發中很是經常使用的功能,其餘詳細的介紹,請參考阮一峯老師的 ES6 教程 Promise 篇
最後,本節咱們只是介紹了Promise
的一些應用,通俗易懂拿來就用的東西,可是沒有提高到理論和標準的高度。有人可能會不屑 ———— 我會用就好了,要那麼空談的理論幹嗎?———— 你只會使用卻上升不到理論高度,永遠都是個搬磚的,搬一塊磚掙一毛錢,不搬就不掙錢! 在我看來,全部的知識應該都須要上升到理論高度,將實際應用和標準對接,知道真正的出處,才能走的長遠。
下一節咱們介紹 Promise/A+ 規範
Promise/A 是由 CommonJS 組織制定的異步模式編程規範,後來又通過一些升級,就是當前的 Promise/A+ 規範。上一節講述的Promise
的一些功能實現,就是根據這個規範來的。
then
方法網上有不少介紹 Promise/A+ 規範的文章,你們能夠搜索來看,可是它的核心要點有如下幾個,我也是從看了以後本身總結的
關於狀態
關於then
方法
then
方法,並且then
必須返回一個 promise ,同一個 promise 的then
能夠調用屢次(鏈式),而且回調的執行順序跟它們被定義時的順序一致then
方法接受兩個參數,第一個參數是成功時的回調,在 promise 由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在 promise 由「等待」態轉換到「拒絕」態時調用下面挨個介紹這些規範在上一節代碼中的實現,所謂理論與實踐相結合。在閱讀如下內容時,你要時刻準備參考上一節的代碼。
promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
拿到上一節的readFilePromise
函數,而後執行const result = readFilePromise(someFileName)
會獲得一個Promise
對象。
readFilePromise
函數內部的callback
中會自定調用resolve()
,這樣就變爲 已完成(fulfilled)狀態readFilePromise
函數內部的callback
中會自定調用reject()
,這樣就變爲 已拒絕(rejeced)狀態promise 的狀態只可能從「等待」轉到「完成」態或者「拒絕」態,不能逆向轉換,同時「完成」態和「拒絕」態不能相互轉換
這個規則仍是能夠參考讀取文件的這個例子。從一開始準備讀取,到最後不管是讀取成功或是讀取失敗,都是不可逆的。另外,讀取成功和讀取失敗之間,也是不能互換的。這個邏輯沒有任何問題,很好理解。
then
方法promise 必須實現
then
方法,並且then
必須返回一個 promise ,同一個 promise 的then
能夠調用屢次(鏈式),而且回調的執行順序跟它們被定義時的順序一致
promise
對象必須實現then
方法這個無需解釋,沒有then
那就不叫promise
then
必須返回一個promise
,同一個 promise 的then
能夠調用屢次(鏈式)」 ———— 這兩句話說明了一個意思 ———— then
確定要再返回一個promise
,要否則then
後面怎麼能再鏈式的跟一個then
呢?
then
方法接受兩個參數,第一個參數是成功時的回調,在 promise 由「等待」態轉換到「完成」態時調用,另外一個是失敗時的回調,在 promise 由「等待」態轉換到「拒絕」態時調用
這句話比較好理解了,咱們從一開始就在 demo 中演示。
Promise
的應用、規範都介紹完了,看起來挺牛的,也解決了異步操做中使用callback
帶來的不少問題。可是Promise
本質上究竟是一種什麼樣的存在,它是真的把callback
棄而不用了嗎,仍是二者有什麼合做關係?它究竟是真的神通廣大,仍是使用了障眼法?
這些問題,你們學完Promise
以後應該去思考,不能光學會怎麼用就中止了。下一節咱們一塊兒來探討~
Promise 雖然改變了 JS 工程師對於異步操做的寫法,可是卻改變不了 JS 單線程、異步的執行模式。
從最初的 ES三、4 到 ES5 再到如今的 ES6 和即將到來的 ES7,語法標準上更新不少,可是 JS 這種單線程、異步的本質是沒有改變的。nodejs 中讀取文件的代碼一直均可以這樣寫
fs.readFile('some.json', (err, data) => {
})
既然異步這個本質不能改變,伴隨異步在一塊兒的永遠都會有callback
,由於沒有callback
就沒法實現異步。所以callback
永遠存在。
JS 工程師不會討厭 JS 異步的本質,可是很討厭 JS 異步操做中callback
的書寫方式,特別是遇到萬惡的callback-hell
(嵌套callback
)時。
計算機的抽象思惟和人的具象思惟是徹底不同的,人永遠喜歡看起來更加符合邏輯、更加易於閱讀的程序,所以如今特別強調代碼可讀性。而Promise
就是一種代碼可讀性的變化。你們感覺一下這兩種不一樣(這其中還包括異常處理,加上異常處理會更加複雜)
第一種,傳統的callback
方式
fs.readFile('some1.json', (err, data) => { fs.readFile('some2.json', (err, data) => { fs.readFile('some3.json', (err, data) => { fs.readFile('some4.json', (err, data) => { }) }) }) })
第二種,Promise
方式
readFilePromise('some1.json').then(data => { return readFilePromise('some2.json') }).then(data => { return readFilePromise('some3.json') }).then(data => { return readFilePromise('some4.json') })
這兩種方式對於代碼可讀性的對比,很是明顯。可是最後再次強調,Promise
只是對於異步操做代碼可讀性的一種變化,它並無改變 JS 異步執行的本質,也沒有改變 JS 中存在callback
的現象。
上文已經基本給出了上一節提問的答案,可是這裏還須要再加一個補充:Promise
不只僅是沒有取代callback
或者棄而不用,反而Promise
中要使用到callback
。由於,JS 異步執行的本質,必須有callback
存在,不然沒法實現。
再次粘貼處以前章節的封裝好的一個Promise
函數(進行了一點點簡化)
const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { resolve(data.toString()) }) }) }
上面的代碼中,promise
對象的狀態要從pending
變化爲fulfilled
,就須要去執行resolve()
函數。那麼是從哪裏執行的 ———— 還得從callback
中執行resolve
函數 ———— 這就是Promise
也須要callback
的最直接體現。
一塊技術「火」的程度和第三方開源軟件的數量、質量以及使用狀況有很大的正比關係。例如爲了簡化 DOM 操做,jquery 風靡全世界。Promise 用的比較多,第三方庫固然就必不可少,它們極大程度的簡化了 Promise 的代碼。
接下來咱們一塊兒看看Q.js
這個庫的使用,學會了它,將極大程度提升你寫 Promise 的效率。
若是實際項目中使用Promise
,仍是強烈建議使用比較靠譜的第三方插件,會極大增長你的開發效率。除了將要介紹的Q.js
,還有bluebird
也推薦使用,去 github 自行搜索吧。
另外,使用第三方庫不只僅是提升效率,它還讓你在瀏覽器端(不支持Promise
的環境中)使用promise
。
本節展現的代碼參考這裏
Q.nfcall
和Q.nfapply
Q.defer
Q.denodeify
Q.all
和Q.any
Q.delay
能夠直接去它的 github 地址 (近 1.3W 的 star 數量說明其用戶羣很大)查看文檔。
若是項目使用 CommonJS 規範直接 npm i q --save
,若是是網頁外鏈可尋找可用的 cdn 地址,或者乾脆下載到本地。
如下我將要演示的代碼,都是使用 CommonJS 規範的,所以我要演示代碼以前加上引用,之後的代碼演示就不重複加了。
const Q = require('q')
Q.nfcall
和Q.nfapply
要使用這兩個函數,你得首先了解 JS 的call
和apply
,若是不瞭解,先去看看。熟悉了這兩個函數以後,再回來看。
Q.nfcall
就是使用call
的語法來返回一個promise
對象,例如
const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfcall(fs.readFile, fullFileName, 'utf-8') // 使用 Q.nfcall 返回一個 promise result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })
Q.nfapply
就是使用apply
的語法返回一個promise
對象,例如
const fullFileName = path.resolve(__dirname, '../data/data1.json') const result = Q.nfapply(fs.readFile, [fullFileName, 'utf-8']) // 使用 Q.nfapply 返回一個 promise result.then(data => { console.log(data) }).catch(err => { console.log(err.stack) })
怎麼樣,體驗了一把,是否是比直接本身寫Promise
簡單多了?
Q.defer
Q.defer
算是一個比較偏底層一點的 API ,用於本身定義一個promise
生成器,若是你須要在瀏覽器端編寫,並且瀏覽器不支持Promise
,這個就有用處了。
function readFile(fileName) { const defer = Q.defer() fs.readFile(fileName, (err, data) => { if (err) { defer.reject(err) } else { defer.resolve(data.toString()) } }) return defer.promise } readFile('data1.json') .then(data => { console.log(data) }) .catch(err => { console.log(err.stack) })
Q.denodeify
咱們在很早以前的一節中本身封裝了一個fs.readFile
的promise
生成器,這裏再次回顧一下
const readFilePromise = function (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) { reject(err) } else { resolve(data.toString()) } }) }) }
雖然看着不麻煩,可是仍是須要不少行代碼來實現,若是使用Q.denodeify
,一行代碼就搞定了!
const readFilePromise = Q.denodeify(fs.readFile)
Q.denodeif
就是一鍵將fs.readFile
這種有回調函數做爲參數的異步操做封裝成一個promise
生成器,很是方便!
Q.all
和Q.any
這兩個其實就是對應了以前講過的Promise.all
和Promise.race
,並且應用起來如出一轍,很少贅述。
const r1 = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') const r2 = Q.nfcall(fs.readFile, 'data2.json', 'utf-8') Q.all([r1, r2]).then(arr => { console.log(arr) }).catch(err => { console.log(err) })
Q.delay
Q.delay
,顧名思義,就是延遲的意思。例如,讀取一個文件成功以後,再過五秒鐘以後,再去作xxxx。這個若是是本身寫的話,也挺費勁的,可是Q.delay
就直接給咱們分裝好了。
const result = Q.nfcall(fs.readFile, 'data1.json', 'utf-8') result.delay(5000).then(data => { // 獲得結果 console.log(data.toString()) }).catch(err => { // 捕獲錯誤 console.log(err.stack) })
以上就是Q.js
一些最經常使用的操做,其餘的一些很是用技巧,你們能夠去搜索或者去官網查看文檔。
至此,ES6 Promise
的全部內容就已經講完了。可是異步操做的優化到這裏沒有結束,更加精彩的內容還在後面 ———— Generator
若是你看完了,感受還不錯,歡迎給我打賞 ———— 以激勵我更多輸出優質內容
最後,github地址是 https://github.com/wangfupeng1988/js-async-tutorial 歡迎 star 和 pr
-----------------
學習做者教程:《前端JS高級面試》《前端JS基礎面試題》《React.js模擬大衆點評webapp》《zepto設計與源碼分析》《json2.js源碼解讀》