天天看点

深入理解 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源码解读》

继续阅读