Promise詳解

Promise 是一種處理異步的思路逾礁。Promise 也是一個類敞斋。當我們說一個 promise 的時候植捎,一般指一個 Promise 對象焰枢。

快速感受

傳統(tǒng)使用回調(diào)的寫法:

fs.readFile('config.json', function (error, text) {
    if (error) {
        console.error('Error while reading config file')
    } else {
        try {
            const obj = JSON.parse(text)
            console.log(JSON.stringify(obj, null, 4))
        } catch (e) {
            console.error('Invalid JSON in file')
        }
    }
})

使用 Promise 的寫法:

function readFilePromisified(filename) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, { encoding: 'utf8' }, (error, data) => {
            if (error) {
                reject(error)
            } else {
                resolve(data)
            }
        })
    })
}

readFilePromisified('config.json').then(function (text) { // (A)
    const obj = JSON.parse(text)
    console.log(JSON.stringify(obj, null, 4))
}).catch(function (error) { // (B)
    // File read error or JSON SyntaxError
    console.error('An error occurred', error)
})

其中 readFilePromisified 方法可以通過庫快速改寫,如通過 bluebird 庫:

const readFilePromisified = bluebird.promisify(fs. fs.readFile)

優(yōu)點

這里先提幾個關鍵點荐绝,具體優(yōu)點還需要邊學邊做體會避消。

使用 Promise 代替回調(diào)等機制,處于兩類目的监憎。一類是 Promise 比相應寫法更好鲸阔,簡練方便褐筛,清晰叙身,表達力強等等曲梗。第二類是相應機制的一些功能虏两,如重復回調(diào)定罢、錯誤處理祖凫、回調(diào)注冊時機惠况、組合多個異步操作等稠屠,非常容易出問題或非常難寫权埠,而 Promise 可以規(guī)避這類問題攘蔽。

最后一點是 Promise 統(tǒng)一了標準化的寫法满俗。回調(diào)并沒有統(tǒng)一的標準瓜富,Node.js 的回調(diào)与柑,XMLHttpRequest 的回調(diào)并不統(tǒng)一价捧。但 Promise 是統(tǒng)一的结蟋。

狀態(tài)

一個 Promise 會處于下面三種狀態(tài)中的一種:

  • 異步結果就緒前渔彰,Promise 處于 pending 狀態(tài)
  • 獲得結果后恍涂,Promise 處于 fulfilled 狀態(tài)
  • 如果發(fā)生了錯誤再沧,Promise 處于 rejected 狀態(tài)。

Promise 落定(settled)淤堵,指它已獲得了結果或發(fā)生了錯誤(fulfilled 或 rejected)拐邪。Promise 一旦落定隘截,狀態(tài)不會再改變技俐。

Promise 提供兩種接口使 Promise 從進行中的狀態(tài)(pending)達到落定狀態(tài)(settled):

  • 一類是“拒絕”(reject)方法雕擂,讓 Promise 進入 rejected 狀態(tài)井赌。
  • 一類是“解決”(resolve)方法贵扰。如果解決的值不是一個 Promise戚绕,則進入 fulfilled 狀態(tài)舞丛;否則如果解決的值本身又是一個 Promise球切,則要等這個 Promise 達到落定狀態(tài)吨凑。

創(chuàng)建 Promise

通過構造器

const p = new Promise(function (resolve, reject) { // (A)
    ···
    if (···) {
        resolve(value) // success
    } else {
        reject(reason) // failure
    }
})

并且如果在函數(shù)中拋出了異常鸵钝,p 自動被該異常拒絕恩商。

const p = new Promise(function (resolve, reject) {
    throw new Error("Bad")
})
p.catch(function(e) { // 將執(zhí)行這里
    console.error(e)
})

thenable

thenable 是一個對象,具有類似與 Promise 的 then() 方法韧献。thenable 不是一個 Promise锤窑。它一般是 Promise 標準化之前出現(xiàn)的采用 Promise 思想的自定義的對象渊啰。通過 Promise.resolve(x) 可以將一個非標準化的 thenable 轉換為一個 Promise绘证,見下一節(jié)嚷那。

Promise.resolve(x)

對于 x 不是一個 Promise,Promise.resolve(x) 返回一個 Promise腐泻,該 Promise 立即以 x 的值解決派桩。

Promise.resolve('abc')
    .then(x => console.log(x)) // abc

如果 x 是一個 Promise,則 Promise.resolve(x) 原樣返回 x范嘱。

const p = new Promise(() => null)
console.log(Promise.resolve(p) === p) // true

如果 x 是一個 thenable彤侍,則把它轉換為一個 Promise逆趋。

const fulfilledThenable = {
    then(reaction) {
        reaction('hello')
    }
}
const promise = Promise.resolve(fulfilledThenable)
console.log(promise instanceof Promise) // true
promise.then(x => console.log(x)) // hello

總結:通過 Promise.resolve() 將任何值轉換為一個 Promise名斟。

Promise.reject(err)

Promise.reject(err) 返回一個 Promise魄眉,以 err 拒絕:

const myError = new Error('Problem!')
Promise.reject(myError)
    .catch(err => console.log(err === myError)) // true

例子

1岩梳、fs.readFile()

import {readFile} from 'fs'

function readFilePromisified(filename) {
    return new Promise(function (resolve, reject) {
        readFile(filename, { encoding: 'utf8' }, (error, data) => {
            if (error) {
                reject(error)
            } else {
                resolve(data)
            }
        })
    })
}

2晃择、XMLHttpRequest

function httpGet(url) {
    return new Promise(function (resolve, reject) {
        const request = new XMLHttpRequest()
        request.onload = function () {
            if (this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
        request.onerror = function () {
            reject(new Error('XMLHttpRequest Error: '+this.statusText))
        }
        request.open('GET', url)
        request.send()
    })
}

3列疗、延時

Let’s implement setTimeout() as the Promise-based function delay() (similar to Q.delay()).

function delay(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, ms)
    })
}

// Using delay():
delay(5000).then(function () {
    console.log('5 seconds have passed!')
})

4抵栈、超時

如果一個 Promise 在指定時間內(nèi)獲取到了結果(落定)古劲,則通知這個結果绢慢,否則以超時異常拒絕:

function timeout(ms, promise) {
    return new Promise(function (resolve, reject) {
        promise.then(resolve)
        setTimeout(function () {
            reject(new Error('Timeout after '+ms+' ms'))
        }, ms)
    })
}

消費一個 Promise

通過 thencatch 注冊處理方法骚露,在 Promise 解決或拒絕時調(diào)用棘幸。

promise
    .then(value => { /* fulfillment */ })
    .catch(error => { /* rejection */ })

使用回調(diào)的一個常見問題時误续,還來不及注冊回調(diào)函數(shù)蹋嵌,異步執(zhí)行就結束了栽烂。但 Promise 沒有這個問題恋脚。如果一個 Promise 對象落定了,會保持住狀態(tài)以及解決的值或拒絕的異常怀喉。此時再使用 thencatch 注冊處理方法仍可以得到結果躬拢。

then 還有一個兩參數(shù)的寫法:

promise.then(
    null,
    error => { /* rejection */ })

promise.catch(
    error => { /* rejection */ })

傳遞

then() 方法會返回一個新的 Promise聊闯,于是你可以鏈接調(diào)用:

const q = Promise.resolve(true).then(function() {
    return new Promise(function(resolve) {
        setTimeout(function() {
            resolve("Good")
        }, 1000)
    })
})
q.then(function(result) {
    console.log(result) // Good
})

上述代碼其實先后會產(chǎn)生 4 個 Promise:

const q1 = Promise.resolve(true)
const q2 = q1.then(function() {
    const q3 = new Promise(function(resolve) {
        setTimeout(function() {
            resolve("Good")
        }, 1000)
    })
    return q3
})
const q4 = q2.then(function(result) {
    console.log(result)
})

具體來說域慷,.then(onFulfilled, onRejected) 返回的 Promise P 的值取決于它的回調(diào)函數(shù)的執(zhí)行犹褒。

1叠骑、如果 onFulfilledonRejected 返回一個 Promise,該 Promise 的結論傳遞給 P茧跋。例子:

Promise.resolve(true)
    .then(function (value1) {
        return 123
    })
    .then(function (value2) {
        console.log(value2) // 123
    })

特別注意如果 onRejected 中正常返回了值瘾杭,則 then() 的結果是解決(fulfilled)而不是拒絕狀態(tài)√肿瑁可以利用這種特性恢復錯誤钝吮,提供默認值等搀绣。

Promise.reject("Bad")
    .catch(function () {
        return "I Know BAD"
    })
    .then(function (result) {
        console.log(result)
    })

2链患、如果 onFulfilledonRejected 返回了一個值麻捻,值傳給 P贸毕。

該機制的主要作用是展平嵌套的 then() 調(diào)用,例子:

asyncFunc1()
    .then(function (value1) {
        asyncFunc2()
            .then(function (value2) {
                ···
        })
})

展平了的版本:

asyncFunc1()
    .then(function (value1) {
        return asyncFunc2()
    })
    .then(function (value2) {
        ···
    })

3摊腋、如果 onFulfilledonRejected 拋出了異常兴蒸,則 P 以該異常拒絕。

asyncFunc()
    .then(function (value) {
        throw new Error()
    })
    .catch(function (reason) {
        // Handle error here
    })

鏈式調(diào)用有一個好處,可以最后統(tǒng)一處理錯誤钓觉。中間環(huán)節(jié)的任何錯誤可以被最終統(tǒng)一處理:

asyncFunc1()
    .then(asyncFunc2)
    .then(asyncFunc3)
    .catch(function (reason) {
        // Something went wrong above
    })

串行與并行

通過 then 鏈式調(diào)用異步函數(shù),這些函數(shù)的執(zhí)行是串行的:

asyncFunc1()
    .then(() => asyncFunc2())

如果不使用 then 連接卧晓,它們是并行執(zhí)行的逼裆,但是你拿不到結果胜宇,也不知道什么時候他們?nèi)客瓿伞?/p>

asyncFunc1()
asyncFunc2()

解決方法是使用 Promise.all()。它的參數(shù)是一個數(shù)組恢着,數(shù)組元素是 Promise桐愉。它返回一個 Promise,解析的結果是一個數(shù)組掰派。

Promise.all([ asyncFunc1(), asyncFunc2() ])
    .then(([result1, result2]) => {
        ···
    })
    .catch(err => {
        // Receives first rejection among the Promises
        ···
    })

如果 map 的映射函數(shù)返回一個 Promise从诲,map 產(chǎn)生的數(shù)組由 Promise.all() 處理:

const fileUrls = [
    'http://example.com/file1.txt',
    'http://example.com/file2.txt',
]
const promisedTexts = fileUrls.map(httpGet)

Promise.all(promisedTexts)
    .then(texts => {
        for (const text of texts) {
            console.log(text)
        }
    })
    .catch(reason => {
        // Receives first rejection among the Promises
    })

Promise.race()Promise.all() 類似,但只要數(shù)組中一個 Promise 落定(不管解決還是拒絕)靡羡,該 Promise 的結果作為 Promise.race() 的結果系洛。

注意如果數(shù)組為空,Promise.race() 永遠不會落定(settled)略步。

例子描扯,通過 Promise.race() 實現(xiàn)超時:

Promise.race([
    httpGet('http://example.com/file.txt'),
    delay(5000).then(function () {
        throw new Error('Timed out')
    })
])
.then(function (text) { ··· })
.catch(function (reason) { ··· })

常見錯誤

丟失 then 的結果

先看錯誤的代碼:

function foo() {
    const promise = asyncFunc()
    promise.then(result => {
        ···
    })

    return promise
}

再看正確的代碼:

function foo() {
    const promise = asyncFunc()
    return promise.then(result => {
        ···
    })
}

甚至再簡化為:

function foo() {
    return asyncFunc()
        .then(result => {
            ···
        })
}

不要忘了 promise.then( 也會產(chǎn)生一個結果,甚至可能拋出異常玫鸟,或返回一個異步結果(一個新的 Promise)贾费。

捕獲全部異常

前面提過可以通過鏈式調(diào)用,最后統(tǒng)一處理異常,這么做不但省事,而且可以避免遺漏錯誤:

asyncFunc1()
    .then(
        value => { // (A)
            doSomething() // (B)
            return asyncFunc2() // (C)
        },
        error => { // (D)
            ···
        })

上面的代碼票从,D 處的錯誤處理只能處理 asyncFunc1() 的錯誤吟榴,但無法處理 B 處拋出的移除和 C 處返回的拒絕的 Promise宪拥。正確的寫法:

asyncFunc1()
    .then(value => {
        doSomething()
        return asyncFunc2()
    })
    .catch(error => {
        ···
    })

忽視非異步代碼的錯誤

有時我們編寫的異步方法中 —— 這里的異步方法指返回 Promise 的方法 —— 在異步前存在部分非異步的代碼。如果這些代碼拋出異常,異常將直接從方法拋出,而不是進入返回 Promise 并拒絕襟己,例如下面代碼的 A 處:

function asyncFunc() {
    doSomethingSync() // (A)
    return doSomethingAsync()
        .then(result => {
            ···
        })
}

解決方法一毒涧,捕獲并顯式拒絕:

function asyncFunc() {
    try {
        doSomethingSync()
        return doSomethingAsync()
            .then(result => {
                ···
            })
    } catch (err) {
        return Promise.reject(err)
    }
}

解決方法二,通過 Promise.resolve().then() 開始一段 Promise 鏈,將同步代碼包裹進 then 的方法中:

function asyncFunc() {
    return Promise.resolve().then(() => {
        doSomethingSync()
        return doSomethingAsync()
    }).then(result => {
        ···
    })
}

方法三:

function asyncFunc() {
    return new Promise((resolve, reject) => {
        doSomethingSync()
        resolve(doSomethingAsync())
    })
    .then(result => {
        ···
    })
}

This approach saves you a tick (the synchronous code is executed right away), but it makes your code less regular.

finally

有時不管成功還是失敗都要執(zhí)行一些方法枣申,例如確認對話框,不管確定還是取消都要關閉對話框榨咐。Promise 原生 API 沒有提供 finally 的功能,但我們可以模擬:

Promise.prototype.finally = function (callback) {
    const P = this.constructor
    // We don’t invoke the callback in here,
    // because we want then() to handle its exceptions
    return this.then(
        // Callback fulfills => continue with receiver’s fulfillment or rejection
        // Callback rejects => pass on that rejection (then() has no 2nd parameter!)
        value  => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
    )
}

調(diào)用:

createResource(···)
.then(function (value1) {
    // Use resource
})
.then(function (value2) {
    // Use resource
})
.finally(function () {
    // Clean up
})

Promise 庫

原生 Promise 庫夠用但不夠強大遂蛀,對于更復雜的功能,可以使用某個第三方 Promise 庫裕坊。比如 bluebird

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彼乌,一起剝皮案震驚了整個濱河市毒租,隨后出現(xiàn)的幾起案子算色,更是在濱河造成了極大的恐慌斥废,老刑警劉巖统锤,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡钾麸,警方通過查閱死者的電腦和手機芋肠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門原在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事璧坟。” “怎么了先鱼?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵肾请,是天一觀的道長彪标。 經(jīng)常有香客問我,道長抱婉,這世上最難降的妖魔是什么步藕? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拷肌,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己昼捍,他們只是感情好蛛株,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盾沫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仪吧。 梳的紋絲不亂的頭發(fā)上哗戈,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天厚柳,我揣著相機與錄音,去河邊找鬼螟左。 笑死,一個胖子當著我的面吹牛坝茎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播次酌,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼贫母,長吁一口氣:“原來是場噩夢啊……” “哼因块!你這毒婦竟也來了吩愧?” 一聲冷哼從身側響起糖权,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎登淘,沒想到半個月后箫老,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡黔州,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年耍鬓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片流妻。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡牲蜀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绅这,到底是詐尸還是另有隱情涣达,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布证薇,位于F島的核電站度苔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏浑度。R本人自食惡果不足惜寇窑,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望箩张。 院中可真熱鬧甩骏,春花似錦完残、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缎浇,卻和暖如春扎拣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背素跺。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工二蓝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人指厌。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓刊愚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親踩验。 傳聞我的和親對象是個殘疾皇子鸥诽,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券箕憾,享受所有官網(wǎng)優(yōu)惠牡借,并抽取幸運大...
    HetfieldJoe閱讀 11,026評論 26 95
  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,707評論 1 56
  • 在ES6當中添加了很多新的API其中很值得一提的當然少不了Promise袭异,因為Promise的出現(xiàn)钠龙,很輕松的就給開...
    嘿_那個誰閱讀 3,669評論 2 3
  • 你不知道JS:異步 第三章:Promises 在第二章,我們指出了采用回調(diào)來表達異步和管理并發(fā)時的兩種主要不足:缺...
    purple_force閱讀 2,068評論 0 4
  • 特別說明御铃,為便于查閱碴里,文章轉自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 890評論 0 2