提到 JavaScript 异步编程,不少小伙伴都很迷茫,本人花费大约一周的业余时间来对 JS 异步作一个完整的总结,和各位同窗共勉共进步!html
part1 基础部分node
part2 jQuery的解决方案jquery
part3 ES6-Promisegit
part4 Generatores6
part5 async-awaitgithub
v6.x
版本中使用 async-awaitpart6 总结ajax
提醒:若是你是初学 js 的同窗,还没有有太多项目经验和基础知识,请就此打住,不要看这篇教程chrome
我思考问题、写文章通常都不按讨论出牌,别人写过的东西我不会再照着抄一遍。所以,后面全部的内容,都是我看了许多资料以后,我的从新思考提炼总结出来的,这确定不能算是初级教程。npm
若是你是已有 js 开发经验,并了解异步的基础知识,到这里来想深刻了解一下Promise
Generator
和async-await
,那就太好了,很是欢迎。编程
首先记住一句话 —— JS 是单线程的语言,所谓“单线程”就是一根筋,对于拿到的程序,一行一行的执行,上面的执行为完成,就傻傻的等着。例如
var i, t = Date.now() for (i = 0; i < 100000000; i++) { } console.log(Date.now() - t) // 250 (chrome浏览器)
上面的程序花费 250ms 的时间执行完成,执行过程当中就会有卡顿,其余的事儿就先撂一边无论了。
执行程序这样没有问题,可是对于 JS 最初使用的环境 ———— 浏览器客户端 ———— 就不同了。所以在浏览器端运行的 js ,可能会有大量的网络请求,而一个网络资源啥时候返回,这个时间是不可预估的。这种状况也要傻傻的等着、卡顿着、啥都不作吗?———— 那确定不行。
所以,JS 对于这种场景就设计了异步 ———— 即,发起一个网络请求,就先无论这边了,先干其余事儿,网络请求啥时候返回结果,到时候再说。这样就能保证一个网页的流程运行。
先看一段比较常见的代码
var ajax = $.ajax({ url: '/data/data1.json', success: function () { console.log('success') } })
上面代码中$.ajax()
须要传入两个参数进去,url
和success
,其中url
是请求的路由,success
是一个函数。这个函数传递过去不会当即执行,而是等着请求成功以后才能执行。对于这种传递过去不执行,等出来结果以后再执行的函数,叫作callback
,即回调函数
再看一段更加能说明回调函数的 nodejs 代码。和上面代码基本同样,惟一区别就是:上面代码时网络请求,而下面代码时 IO 操做。
var fs = require('fs') fs.readFile('data1.json', (err, data) => { console.log(data.toString()) })
从上面两个 demo 看来,实现异步的最核心原理,就是将callback
做为参数传递给异步执行函数,当有结果返回以后再触发 callback
执行,就是如此简单!
开发中比较经常使用的异步操做有:
ajax
http.get
readFile
readdir
setTimeout
setInterval
最后,请思考,事件绑定是否是也是异步操做?例如$btn.on('click', function() {...})
。这个问题颇有意思,我会再后面的章节通过分析以后给出答案,各位先本身想一下。
$.ajax
这个函数各位应该都比较熟悉了,要完整的讲解 js 的异步操做,就必须先从$.ajax
这个方法提及。
想要学到全面的知识,你们就不要着急,跟随个人节奏来,而且相信我。我安排的内容,确定都是有用的,对主题无用的东西,我不会拿来占用你们的时间。
$.ajax
$.ajax
Promise
的关系$.ajax
先来一段最多见的$.ajax
的代码,固然是使用万恶的callback
方式
var ajax = $.ajax({ url: 'data.json', success: function () { console.log('success') }, error: function () { console.log('error') } }) console.log(ajax) // 返回一个 XHR 对象
至于这么作会产生什么样子的诟病,我想你们应该都很明白了。不明白的本身私下去查,可是你也能够继续往下看,你只须要记住这样作很很差就是了,要否则 jquery 也不会再后面进行改进
$.ajax
可是从v1.5
开始,以上代码就能够这样写了:能够链式的执行done
或者fail
方法
var ajax = $.ajax('data.json') ajax.done(function () { console.log('success 1') }) .fail(function () { console.log('error') }) .done(function () { console.log('success 2') }) console.log(ajax) // 返回一个 deferred 对象
你们注意看以上两段代码中都有一个console.log(ajax)
,可是返回值是彻底不同的。
v1.5
以前,返回的是一个XHR
对象,这个对象不可能有done
或者fail
的方法的v1.5
开始,返回一个deferred
对象,这个对象就带有done
和fail
的方法,而且是等着请求返回以后再去调用这是一个标志性的改造,无论这个概念是谁最早提出的,它在 jquery 中首先大量使用并让全球开发者都知道原来 ajax 请求还能够这样写。这为之后的Promise
标准制定提供了很大意义的参考,你能够觉得这就是后面Promise
的原型。
记住一句话————虽然 JS 是异步执行的语言,可是人的思惟是同步的————所以,开发者老是在寻求如何使用逻辑上看似同步的代码来完成 JS 的异步请求。而 jquery 的这一次更新,让开发者在必定程度上获得了这样的好处。
以前不管是什么操做,我都须要一股脑写到callback
中,如今不用了。如今成功了就写到done
中,失败了就写到fail
中,若是成功了有多个步骤的操做,那我就写不少个done
,而后链式链接起来就 OK 了。
Promise
的关系以上的这段代码,咱们还能够这样写。即不用done
和fail
函数,而是用then
函数。then
函数的第一个参数是成功以后执行的函数(即以前的done
),第二个参数是失败以后执行的函数(即以前的fail
)。并且then
函数还能够链式链接。
var ajax = $.ajax('data.json') ajax.then(function () { console.log('success 1') }, function () { console.log('error 1') }) .then(function () { console.log('success 2') }, function () { console.log('error 2') })
若是你对如今 ES6 的Promise
有了解,应该能看出其中的类似之处。不了解也不要紧,你只须要知道它已经和Promise
比较接近了。后面立刻会去讲Promise
明眼人都知道,jquery 不可能改变异步操做须要callback
的本质,它只不过是本身定义了一些特殊的 API,并对异步操做的callback
进行了封装而已。
那么 jquery 是如何实现这一步的呢?请听下回分解!
上一节讲到 jquery v1.5 版本开始,$.ajax
可使用相似当前Promise
的then
函数以及链式操做。那么它究竟是如何实现的呢?在此以前所用到的callback
在这其中又起到了什么做用?本节给出答案
本节内容概述
$.Deferred
封装then
方法给出一段很是简单的异步操做代码,使用setTimeout
函数。
var wait = function () { var task = function () { console.log('执行完成') } setTimeout(task, 2000) } wait()
以上这些代码执行的结果你们应该都比较明确了,即 2s 以后打印出执行完成
。可是我若是再加一个需求 ———— 要在执行完成以后进行某些特别复杂的操做,代码可能会不少,并且分好几个步骤 ———— 那该怎么办? 你们思考一下!
若是你不看下面的内容,并且目前尚未Promise
的这个思惟,那估计你会说:直接在task
函数中写就是了!不过相信你看完下面的内容以后,会放弃你如今的想法。
$.Deferred
封装好,接下来咱们让刚才简单的几行代码变得更加复杂。为什么要变得更加复杂?是由于让之后更加复杂的地方变得简单。这里咱们使用了 jquery 的$.Deferred
,至于这个是个什么鬼,你们先不用关心,只须要知道$.Deferred()
会返回一个deferred
对象,先看代码,deferred
对象的做用咱们会面会说。
function waitHandle() { var dtd = $.Deferred() // 建立一个 deferred 对象 var wait = function (dtd) { // 要求传入一个 deferred 对象 var task = function () { console.log('执行完成') dtd.resolve() // 表示异步任务已经完成 } setTimeout(task, 2000) return dtd // 要求返回 deferred 对象 } // 注意,这里必定要有返回值 return wait(dtd) }
以上代码中,又使用一个waitHandle
方法对wait
方法进行再次的封装。waitHandle
内部代码,咱们分步骤来分析。跟着个人节奏慢慢来,保证你不会乱。
var dtd = $.Deferred()
建立deferred
对象。经过上一节咱们知道,一个deferred
对象会有done
fail
和then
方法(不明白的去看上一节)wait
函数,可是:第一,要传入一个deferred
对象(dtd
参数);第二,当task
函数(即callback
)执行完成以后,要执行dtd.resolve()
告诉传入的deferred
对象,革命已经成功。第三;将这个deferred
对象返回。wait(dtd)
的执行结果。由于wait
函数中返回的是一个deferred
对象(dtd
参数),所以wait(dtd)
返回的就是dtd
————若是你感受这里很乱,不要紧,慢慢捋,一行一行看,相信两三分钟就能捋顺!最后总结一下,waitHandle
函数最终return wait(dtd)
即最终返回dtd
(一个deferred
)对象。针对一个deferred
对象,它有done
fail
和then
方法(上一节说过),它还有resolve()
方法(其实和resolve
相对的还有一个reject
方法,后面会提到)
then
方法接着上面的代码继续写
var w = waitHandle() w.then(function () { console.log('ok 1') }, function () { console.log('err 1') }).then(function () { console.log('ok 2') }, function () { console.log('err 2') })
上面已经说过,waitHandle
函数最终返回一个deferred
对象,而deferred
对象具备done
fail
then
方法,如今咱们正在使用的是then
方法。至于then
方法的做用,咱们上一节已经讲过了,不明白的同窗抓紧回去补课。
执行这段代码,咱们打印出来如下结果。能够将结果对标如下代码时哪一行。
执行完成 ok 1 ok 2
此时,你再回头想一想我刚才说提出的需求(要在执行完成以后进行某些特别复杂的操做,代码可能会不少,并且分好几个步骤),是否是有更好的解决方案了?
有同窗确定发现了,代码中console.log('err 1')
和console.log('err 2')
何时会执行呢 ———— 你本身把waitHandle
函数中的dtd.resolve()
改为dtd.reject()
试一下就知道了。
dtd.resolve()
表示革命已经成功,会触发then
中第一个参数(函数)的执行,dtd.reject()
表示革命失败了,会触发then
中第二个参数(函数)执行总结一下一个deferred
对象具备的函数属性,并分为两组:
dtd.resolve
dtd.reject
dtd.then
dtd.done
dtd.fail
我为什么要分红两组 ———— 这两组函数,从设计到执行以后的效果是彻底不同的。第一组是主动触发用来改变状态(成功或者失败),第二组是状态变化以后才会触发的监听函数。
既然是彻底不一样的两组函数,就应该完全的分开,不然很容易出现问题。例如,你在刚才执行代码的最后加上这么一行试试。
w.reject()
那么如何解决这一个问题?请听下回分解!
上一节经过一些代码演示,知道了 jquery 的deferred
对象是解决了异步中callback
函数的问题,可是
promise
promise
的好处promise
咱们对上一节的的代码作一点小小的改动,只改动了一行,下面注释。
function waitHandle() { var dtd = $.Deferred() var wait = function (dtd) { var task = function () { console.log('执行完成') dtd.resolve() } setTimeout(task, 2000) return dtd.promise() // 注意,这里返回的是 primise 而不是直接返回 deferred 对象 } return wait(dtd) } var w = waitHandle() // 通过上面的改动,w 接收的就是一个 promise 对象 $.when(w) .then(function () { console.log('ok 1') }) .then(function () { console.log('ok 2') })
改动的一行在这里return dtd.promise()
,以前是return dtd
。dtd
是一个deferred
对象,而dtd.promise
就是一个promise
对象。
promise
对象和deferred
对象最重要的区别,记住了————promise
对象相比于deferred
对象,缺乏了.resolve
和.reject
这俩函数属性。这么一来,可就彻底不同了。
上一节咱们提到一个问题,就是在程序的最后一行加一句w.reject()
会致使乱套,你如今再在最后一行加w.reject()
试试 ———— 保证乱套不了 ———— 而是你的程序不能执行,直接报错。由于,w
是promise
对象,不具有.reject
属性。
promise
的好处上一节提到deferred
对象有两组属性函数,并且提到应该把这两组完全分开。如今经过上面一行代码的改动,就分开了。
waitHandle
函数内部,使用dtd.resolve()
来该表状态,作主动的修改操做waitHandle
最终返回promise
对象,只能去被动监听变化(then
函数),而不能去主动修改操做一个“主动”一个“被动”,彻底分开了。
jquery v1.5 版本发布时间距离如今(2018年)已经老早以前了,那会儿你们网页标配都是 jquery 。不管里面的deferred
和promise
这个概念和想法最先是哪位提出来的,可是最先展现给全世界开发者的是 jquery ,这算是Promise
这一律念最早的提出者。
其实本次课程主要是给你们分析 ES6 的Promise
Generator
和async-await
,可是为什么要从 jquery 开始(你们如今用 jquery 愈来愈少)?就是要给你们展现一下这段历史的一些起点和发展的知识。有了这些基础,你再去接受最新的概念会很是容易,由于全部的东西都是从最初顺其天然发展进化而来的,咱们要去用一个发展进化的眼光学习知识,而不是死记硬背。
从 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
的一些比较常见的用法,敬请期待吧!
上一节对 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.denodeify
就是一键将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
文章转载:https://blog.csdn.net/sinat_17775997/article/details/70307956(感谢、尊重做者、鞠躬)