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。

你可能感兴趣的:(Promise详解)