原文地址 http://www.cnblogs.com/wangfupeng1988/p/6515855.html 未经做者容许不得转载!html
从 jquery v1.5 发布通过若干时间以后,Promise 终于出如今了 ES6 的标准中,而当下 ES6 也正在被大规模使用。前端
本节展现的代码参考这里node
Promise
进行封装仍是拿以前讲 jquery deferred
对象时的那段setTimeout
程序jquery
var wait = function () { var task = function () { console.log('执行完成') } setTimeout(task, 2000) } wait()
以前咱们使用 jquery 封装的,接下来将使用 ES6 的Promise
进行封装,你们注意看有何不一样。git
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 那次简单一些,逻辑上也更加清晰一些。es6
new Promise((resolve,reject) => {.....})
包装起来,最后return
便可callback
中执行resolve()
(代表成功了,失败的话执行reject
)接着上面的程序继续往下写。wait()
返回的确定是一个promise
对象,而promise
对象有then
属性。github
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
还能够进行链式操做。web
以上就是 ES6 的Promise
的基本使用演示。看完你可能会以为,这跟以前讲述 jquery 的不差很少吗 ———— 对了,这就是我要在以前先讲 jquery 的缘由,让你感受一篇一篇看起来如丝般顺滑!面试
接下来,将详细说一下 ES6 Promise
的一些比较常见的用法,敬请期待吧!ajax
上一节对 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 是由 CommonJS 组织制定的异步模式编程规范,后来又通过一些升级,就是当前的 Promise/A+ 规范。上一节讲述的Promise
的一些功能实现,就是根据这个规范来的。
then
方法网上有不少介绍 Promise/A+ 规范的文章,你们能够搜索来看,可是它的核心要点有如下几个,我也是从看了以后本身总结的
关于状态
关于then
方法
then
方法,并且then
必须返回一个 promise ,同一个 promise 的then
能够调用屡次(链式),而且回调的执行顺序跟它们被定义时的顺序一致then
方法接受两个参数,第一个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另外一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用下面挨个介绍这些规范在上一节代码中的实现,所谓理论与实践相结合。在阅读如下内容时,你要时刻准备参考上一节的代码。
promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
拿到上一节的readFilePromise
函数,而后执行const result = readFilePromise(someFileName)
会获得一个Promise
对象。
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 虽然改变了 JS 工程师对于异步操做的写法,可是却改变不了 JS 单线程、异步的执行模式。
从最初的 ES三、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 的效率。
若是实际项目中使用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
若是你看完了,感受还不错,欢迎给我打赏 ———— 以激励我更多输出优质内容
最后,github地址是 https://github.com/wangfupeng1988/js-async-tutorial 欢迎 star 和 pr
-----------------
学习做者教程:《前端JS高级面试》《前端JS基础面试题》《React.js模拟大众点评webapp》《zepto设计与源码分析》《json2.js源码解读》