第一部分,Promise 加入 ES6 標準
原文地址 http://www.cnblogs.com/wangfupeng1988/p/6515855.html 未經作者允許不得轉載!
從 jquery v1.5 釋出經過若干時間之後,Promise 終於出現在了 ES6 的標準中,而當下 ES6 也正在被大規模使用。
本節展示的程式碼參考這裡
本節內容概述
- 寫一段傳統的非同步操作
- 用
Promise
進行封裝
寫一段傳統的非同步操作
還是拿之前講 jquery deferred
物件時的那段setTimeout
程式
var wait = function () { var task = function () { console.log('執行完成') } setTimeout(task, 2000) } wait()
之前我們使用 jquery 封裝的,接下來將使用 ES6 的Promise
進行封裝,大家注意看有何不同。
用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 那次簡單一些,邏輯上也更加清晰一些。
- 將之前的非同步操作那幾行程式,用
new Promise((resolve,reject) => {.....})
包裝起來,最後return
即可 - 非同步操作的內部,在
callback
中執行resolve()
(表明成功了,失敗的話執行reject
)
接著上面的程式繼續往下寫。wait()
返回的肯定是一個promise
物件,而promise
物件有then
屬性。
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
還可以進行鏈式操作。
以上就是 ES6 的Promise
的基本使用演示。看完你可能會覺得,這跟之前講述 jquery 的不差不多嗎 ———— 對了,這就是我要在之前先講 jquery 的原因,讓你感覺一篇一篇看起來如絲般順滑!
接下來,將詳細說一下 ES6 Promise
的一些比較常見的用法,敬請期待吧!
第二部分,Promise 在 ES6 中的具體應用
上一節對 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+ 規範
Promise/A 是由 CommonJS 組織制定的非同步模式程式設計規範,後來又經過一些升級,就是當前的 Promise/A+ 規範。上一節講述的Promise
的一些功能實現,就是根據這個規範來的。
本節內容概述
- 介紹規範的核心內容
- 狀態變化
then
方法- 接下來...
介紹規範的核心內容
網上有很多介紹 Promise/A+ 規範的文章,大家可以搜尋來看,但是它的核心要點有以下幾個,我也是從看了之後自己總結的
關於狀態
- promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
- promise 的狀態只可能從“等待”轉到“完成”態或者“拒絕”態,不能逆向轉換,同時“完成”態和“拒絕”態不能相互轉換
關於then
方法
- promise 必須實現
then
方法,而且then
必須返回一個 promise ,同一個 promise 的then
可以呼叫多次(鏈式),並且回撥的執行順序跟它們被定義時的順序一致 then
方法接受兩個引數,第一個引數是成功時的回撥,在 promise 由“等待”態轉換到“完成”態時呼叫,另一個是失敗時的回撥,在 promise 由“等待”態轉換到“拒絕”態時呼叫
下面挨個介紹這些規範在上一節程式碼中的實現,所謂理論與實踐相結合。在閱讀以下內容時,你要時刻準備參考上一節的程式碼。
狀態變化
promise 可能有三種狀態:等待(pending)、已完成(fulfilled)、已拒絕(rejected)
拿到上一節的readFilePromise
函式,然後執行const result = readFilePromise(someFileName)
會得到一個Promise
物件。
- 剛剛建立時,就是 等待(pending)狀態
- 如果讀取檔案成功了,
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 真的取代 callback 了嗎
Promise 雖然改變了 JS 工程師對於非同步操作的寫法,但是卻改變不了 JS 單執行緒、非同步的執行模式。
本節概述
- JS 非同步的本質
- Promise 只是表面的寫法上的改變
- Promise 中不能缺少 callback
- 接下來...
JS 非同步的本質
從最初的 ES3、4 到 ES5 再到現在的 ES6 和即將到來的 ES7,語法標準上更新很多,但是 JS 這種單執行緒、非同步的本質是沒有改變的。nodejs 中讀取檔案的程式碼一直都可以這樣寫
fs.readFile('some.json', (err, data) => {
})
既然非同步這個本質不能改變,伴隨非同步在一起的永遠都會有callback
,因為沒有callback
就無法實現非同步。因此callback
永遠存在。
Promise 只是表面的寫法上的改變
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
或者棄而不用,反而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 的效率。
第五部分,使用 Q.js 庫
如果實際專案中使用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原始碼解讀》