您的位置:首页 > Web前端 > JavaScript

深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise

2017-04-07 22:22 786 查看


第一部分,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 由“等待”态转换到“拒绝”态时调用

下面挨个介绍这些规范在上一节代码中的实现,所谓理论与实践相结合。在阅读以下内容时,你要时刻准备参考上一节的代码。


状态变化

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 异步的本质

从最初的 ES3、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.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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: