Promise详解

Promise 是一种处理异步的思路。Promise 也是一个类。当我们说一个 promise 的时候,一般指一个 Promise 对象。

快速感受

传统使用回调的写法:

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)

优点

这里先提几个关键点,具体优点还需要边学边做体会。

使用 Promise 代替回调等机制,处于两类目的。一类是 Promise 比相应写法更好,简练方便,清晰,表达力强等等。第二类是相应机制的一些功能,如重复回调、错误处理、回调注册时机、组合多个异步操作等,非常容易出问题或非常难写,而 Promise 可以规避这类问题。

最后一点是 Promise 统一了标准化的写法?;氐鞑⒚挥型骋坏谋曜?,Node.js 的回调,XMLHttpRequest 的回调并不统一。但 Promise 是统一的。

状态

一个 Promise 会处于下面三种状态中的一种:

  • 异步结果就绪前,Promise 处于 pending 状态
  • 获得结果后,Promise 处于 fulfilled 状态
  • 如果发生了错误,Promise 处于 rejected 状态。

Promise 落定(settled),指它已获得了结果或发生了错误(fulfilled 或 rejected)。Promise 一旦落定,状态不会再改变。

Promise 提供两种接口使 Promise 从进行中的状态(pending)达到落定状态(settled):

  • 一类是“拒绝”(reject)方法,让 Promise 进入 rejected 状态。
  • 一类是“解决”(resolve)方法。如果解决的值不是一个 Promise,则进入 fulfilled 状态;否则如果解决的值本身又是一个 Promise,则要等这个 Promise 达到落定状态。

创建 Promise

通过构造器

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

并且如果在函数中抛出了异常,p 自动被该异常拒绝。

const p = new Promise(function (resolve, reject) {
    throw new Error("Bad")
})
p.catch(function(e) { // 将执行这里
    console.error(e)
})

thenable

thenable 是一个对象,具有类似与 Promise 的 then() 方法。thenable 不是一个 Promise。它一般是 Promise 标准化之前出现的采用 Promise 思想的自定义的对象。通过 Promise.resolve(x) 可以将一个非标准化的 thenable 转换为一个 Promise,见下一节。

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 在指定时间内获取到了结果(落定),则通知这个结果,否则以超时异常拒绝:

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 解决或拒绝时调用。

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

使用回调的一个常见问题时,还来不及注册回调函数,异步执行就结束了。但 Promise 没有这个问题。如果一个 Promise 对象落定了,会保持住状态以及解决的值或拒绝的异常。此时再使用 thencatch 注册处理方法仍可以得到结果。

then 还有一个两参数的写法:

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

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

传递

then() 方法会返回一个新的 Promise,于是你可以链接调用:

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
})

上述代码其实先后会产生 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 的值取决于它的回调函数的执行。

1、如果 onFulfilledonRejected 返回一个 Promise,该 Promise 的结论传递给 P。例子:

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

特别注意如果 onRejected 中正常返回了值,则 then() 的结果是解决(fulfilled)而不是拒绝状态??梢岳谜庵痔匦曰指创砦螅峁┠现档?。

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

2、如果 onFulfilledonRejected 返回了一个值,值传给 P。

该机制的主要作用是展平嵌套的 then() 调用,例子:

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
    })

链式调用有一个好处,可以最后统一处理错误。中间环节的任何错误可以被最终统一处理:

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

串行与并行

通过 then 链式调用异步函数,这些函数的执行是串行的:

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

如果不使用 then 连接,它们是并行执行的,但是你拿不到结果,也不知道什么时候他们全部完成。

asyncFunc1()
asyncFunc2()

解决方法是使用 Promise.all()。它的参数是一个数组,数组元素是 Promise。它返回一个 Promise,解析的结果是一个数组。

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

如果 map 的映射函数返回一个 Promise,map 产生的数组由 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() 类似,但只要数组中一个 Promise 落定(不管解决还是拒绝),该 Promise 的结果作为 Promise.race() 的结果。

注意如果数组为空,Promise.race() 永远不会落定(settled)。

例子,通过 Promise.race() 实现超时:

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( 也会产生一个结果,甚至可能抛出异常,或返回一个异步结果(一个新的 Promise)。

捕获全部异常

前面提过可以通过链式调用,最后统一处理异常,这么做不但省事,而且可以避免遗漏错误:

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

有时不管成功还是失败都要执行一些方法,例如确认对话框,不管确定还是取消都要关闭对话框。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 })
    )
}

调用:

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

Promise 库

原生 Promise 库够用但不够强大,对于更复杂的功能,可以使用某个第三方 Promise 库。比如 bluebird。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容