深入理解 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
相关文章推荐
- 深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise
- 深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise
- 深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise
- 深入理解 JavaScript 异步系列(5)—— async await
- 深入理解 JavaScript 异步系列(5)—— async await
- 深入理解 JavaScript 异步系列(2)—— jquery的解决方案
- 《深入理解ES6》阅读笔记 --- Promise与异步编程
- 深入理解 JavaScript 异步系列(1)——基础
- 深入理解 JavaScript 异步系列(1)——基础
- 深入理解 JavaScript 异步系列(1)——基础
- 深入理解 JavaScript 异步系列(2)—— jquery的解决方案
- 深入理解 JavaScript 异步系列(2)—— jquery的解决方案
- 深入理解 JavaScript 异步系列(1)——基础
- 深入理解 JavaScript 异步系列(5)—— async await
- 深入理解JavaScript系列(2) 揭秘命名函数表达式
- 深入理解JavaScript系列(9) 根本没有“JSON对象”这回事!
- 深入理解JavaScript系列(10) JavaScript核心(晋级高手必读篇)