深刻理解 JavaScript 異步系列(3)—— ES6 中的 Promise

第一部分,Promise 加入 ES6 標準

原文地址 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

 

第二部分,Promise 在 ES6 中的具體應用

上一節對 ES6 的 Promise 有了一個最簡單的介紹,這一節詳細說一下 Promise 那些最多見的功能

本節展現的代碼參考這裏

本節課程概述

  • 準備工做
  • 參數傳遞
  • 異常捕獲
  • 串聯多個異步操做
  • Promise.allPromise.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.allPromise.race的應用

我還得繼續提出更加奇葩的需求,以演示Promise的各個經常使用功能。以下需求:

讀取兩個文件data1.jsondata2.json,如今我須要一塊兒讀取這兩個文件,等待它們所有都被讀取完,再作下一步的操做。此時須要用到Promise.all

// Promise.all 接收一個包含多個 promise 對象的數組
Promise.all([result1, result2]).then(datas => {
    // 接收到的 datas 是一個數組,依次包含了多個 promise 返回的內容
    console.log(datas[0])
    console.log(datas[1])
})

讀取兩個文件data1.jsondata2.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 異步的本質

從最初的 ES三、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.nfcallQ.nfapply
  • 使用Q.defer
  • 使用Q.denodeify
  • 使用Q.allQ.any
  • 使用Q.delay
  • 其餘

下載和安裝

能夠直接去它的 github 地址 (近 1.3W 的 star 數量說明其用戶羣很大)查看文檔。

若是項目使用 CommonJS 規範直接 npm i q --save,若是是網頁外鏈可尋找可用的 cdn 地址,或者乾脆下載到本地。

如下我將要演示的代碼,都是使用 CommonJS 規範的,所以我要演示代碼以前加上引用,之後的代碼演示就不重複加了。

const Q = require('q')

使用Q.nfcallQ.nfapply

要使用這兩個函數,你得首先了解 JS 的callapply,若是不瞭解,先去看看。熟悉了這兩個函數以後,再回來看。

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.readFilepromise生成器,這裏再次回顧一下

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.allQ.any

這兩個其實就是對應了以前講過的Promise.allPromise.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源碼解讀

相關文章
相關標籤/搜索