前言
提到 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(感谢、尊重作者、鞠躬)