天天看點

深入了解 JavaScript 異步系列(3)—— ES6 中的 Promise

第一部分,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 由“等待”态轉換到“拒絕”态時調用

下面挨個介紹這些規範在上一節代碼中的實作,所謂理論與實踐相結合。在閱讀以下内容時,你要時刻準備參考上一節的代碼。

拿到上一節的

readFilePromise

函數,然後執行

const result = readFilePromise(someFileName)

會得到一個

Promise

對象。

  • 剛剛建立時,就是 等待(pending)狀态
  • 如果讀取檔案成功了,

    readFilePromise

    callback

    中會自定調用

    resolve()

    ,這樣就變為 已完成(fulfilled)狀态
  • 如果很不幸讀取檔案失敗了(例如檔案名寫錯了,找不到檔案),

    readFilePromise

    callback

    reject()

    ,這樣就變為 已拒絕(rejeced)狀态

這個規則還是可以參考讀取檔案的這個例子。從一開始準備讀取,到最後無論是讀取成功或是讀取失敗,都是不可逆的。另外,讀取成功和讀取失敗之間,也是不能互換的。這個邏輯沒有任何問題,很好了解。

then

then

then

then

  • promise

    對象必須實作

    then

    方法這個無需解釋,沒有

    then

    那就不叫

    promise

  • “而且

    then

    必須傳回一個

    promise

    ,同一個 promise 的

    then

    可以調用多次(鍊式)” ———— 這兩句話說明了一個意思 ———— 

    then

    肯定要再傳回一個

    promise

    ,要不然

    then

    後面怎麼能再鍊式的跟一個

    then

    呢?

then

這句話比較好了解了,我們從一開始就在 demo 中示範。

Promise

的應用、規範都介紹完了,看起來挺牛的,也解決了異步操作中使用

callback

帶來的很多問題。但是

Promise

本質上到底是一種什麼樣的存在,它是真的把

callback

棄而不用了嗎,還是兩者有什麼合作關系?它到底是真的神通廣大,還是使用了障眼法?

這些問題,大家學完

Promise

之後應該去思考,不能光學會怎麼用就停止了。下一節我們一起來探讨~

第四部分,Promise 真的取代 callback 了嗎

Promise 雖然改變了 JS 工程師對于異步操作的寫法,但是卻改變不了 JS 單線程、異步的執行模式。

本節概述

  • JS 異步的本質
  • Promise 隻是表面的寫法上的改變
  • Promise 中不能缺少 callback

從最初的 ES3、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 的效率。

第五部分,使用 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

求打賞

如果你看完了,感覺還不錯,歡迎給我打賞 ———— 以激勵我更多輸出優質内容

深入了解 JavaScript 異步系列(3)—— ES6 中的 Promise

最後,github位址是 https://github.com/wangfupeng1988/js-async-tutorial 歡迎 star 和 pr

-----------------

學習作者教程:《前端JS進階面試》《前端JS基礎面試題》《React.js模拟大衆點評webapp》《zepto設計與源碼分析》《json2.js源碼解讀》

繼續閱讀