JavaScript 异步编程的前世今生(上)
前言
提到 JavaScript 异步编程,很多小伙伴都很迷茫,本人花费大约一周的业余时间来对 JS 异步做一个完整的总结,和各位同学共勉共进步!
目录
part1 基础部分
- 什么是异步
part2 jQuery的解决方案
- jQuery-1.5 之后的 ajax
- jQuery deferred
- jQuery promise
part3 ES6-Promise
- Promise 加入 ES6 标准
- Promise 在 ES6 中的具体应用
- 对标一下 Promise/A+ 规范
- Promise 真的取代 callback 了吗?
- 用 Q.js 库
part4 Generator
- ES6 中的 Generator
- Iterator 遍历器
- Generator 的具体应用
- Thunk 函数
- Generator 与异步操作
- koa 中使用 Generator
- Generator 的本质是什么?是否取代了 callback
part5 async-await
- ES7 中引入 async-await
- 如何在 nodejs
v6.x
版本中使用 async-await
part6 总结
- 总结
什么是异步
提醒:如果你是初学 js 的同学,尚未有太多项目经验和基础知识,请就此打住,不要看这篇教程
我思考问题、写文章一般都不按讨论出牌,别人写过的东西我不会再照着抄一遍。因此,后面所有的内容,都是我看了许多资料之后,个人重新思考提炼总结出来的,这肯定不能算是初级教程。
如果你是已有 js 开发经验,并了解异步的基础知识,到这里来想深入了解一下Promise
Generator
和async-await
,那就太好了,非常欢迎。
本节内容概述
- JS 为何会有异步
- 异步的实现原理是什么
- 常用的异步操作有哪些
JS 为何会有异步
首先记住一句话 —— 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
- IO 操作,如
readFile
readdir
- 定时函数,如
setTimeout
setInterval
最后,请思考,事件绑定是不是也是异步操作?例如$btn.on('click', function() {...})
。这个问题很有意思,我会再后面的章节经过分析之后给出答案,各位先自己想一下。
jQuery-1.5 之后的 ajax
$.ajax
这个函数各位应该都比较熟悉了,要完整的讲解 js 的异步操作,就必须先从$.ajax
这个方法说起。
想要学到全面的知识,大家就不要着急,跟随我的节奏来,并且相信我。我安排的内容,肯定都是有用的,对主题无用的东西,我不会拿来占用大家的时间。
本节内容概述
- 传统的
$.ajax
- 1.5 版本之后的
$.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 也不会再后面进行改进
1.5 版本之后的$.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 deferred
上一节讲到 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 promise
上一节通过一些代码演示,知道了 jquery 的deferred
对象是解决了异步中callback
函数的问题,但是
本节内容概述
- 返回
promise
- 返回
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
函数),而不能去主动修改操作
一个“主动”一个“被动”,完全分开了。
promise 的概念
jquery v1.5 版本发布时间距离现在(2018年)已经老早之前了,那会儿大家网页标配都是 jquery 。无论里面的deferred
和promise
这个概念和想法最早是哪位提出来的,但是最早展示给全世界开发者的是 jquery ,这算是Promise
这一概念最先的提出者。
其实本次课程主要是给大家分析 ES6 的Promise
Generator
和async-await
,但是为何要从 jquery 开始(大家现在用 jquery 越来越少)?就是要给大家展示一下这段历史的一些起点和发展的知识。有了这些基础,你再去接受最新的概念会非常容易,因为所有的东西都是从最初顺其自然发展进化而来的,我们要去用一个发展进化的眼光学习知识,而不是死记硬背。
Promise 加入 ES6 标准
从 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.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(感谢、尊重作者、鞠躬)