Callback、Promise、Generator、async/await 对比
JS 是单线程的语言。但是也有各种异步的方法(常见的就是 ajax 请求咯)。怎么处理好这些异步请求是一个非常重要的问题
假设一个场景:
现在有一个基础信息录入的表单,需要用户上传 4 张相关的图片和各种填写的资料
- 上传图片是单独的接口,上传成功后会返回对应的文件名
- 最后提交整体资料,图片的字段只要对应的图片文件名称,而不是文件的类型
# 石器时代的 Callback
既然要上传图片,还要拿对应的文件名。那就只好一张张上传,传完了才提交表单呗
// 先定义一个上传图片的方法
// 既然是callback的方式,那肯定就有回调函数传入进来了
function uploadImage(data, cb) {
$.ajax({
url: 'xxx',
data: data,
success: function(res) {
// 假设图片文件名就在 res.data.file
cb({
status: 200,
data: res.data.file
})
}
})
}
// 定义一个提交数据的方法
// file 就是对应的图片文件,就忽略不写了~
function submit() {
uploadImage({ name: 'img1', data: file }, function(img1_data) {
uploadImage({ name: 'img2', data: file }, function(img2_data) {
uploadImage({ name: 'img3', data: file }, function(img3_data) {
uploadImage({ name: 'img4', data: file }, function(img4_data) {
// 最后拿到所有的图片信息
console.log(img1_data, img2_data, img3_data, img4_data)
// 处理剩下的业务逻辑... 图片信息字段放入formData中
// ajax 提交 formData数据
})
})
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 优缺点
优点
快
这个快,并不是说开发效率的快。只是代码执行效率的快,可以看到中间基本没有中间环节,拿到数据,就调用回调函数返回对应的数据,JS 执行效率高逻辑简单,兼容性好
这兼容性好到什么底部?IE 老大哥都可以识别的代码,你说兼容性多好
缺点
回调地狱 Callback hell
上面的代码就有点回调地狱的味道,一层套一层,现在还只是上传 4 张图片,万一搞个 10 张,20 张,那嵌套的就太恐怖了- 可读性不好,毕竟代码是写给人看的。
# 铁器时代 Promise
Promise 的出现其实比较早,早在 JQ 就已经开始有实现,比如 JQ 的 1.5版本
就已经出现了 JQuery Deferred
。这可以看作是 Promise
的一个原型了,不过可能用的人比较少,JQ 后来也出了 Promise 的版本。以至于后来也出现了各种 Promise 的库,还有我们熟悉的手写 Promise A+
规范。直到近几年 Promise 终于被纳入了 JS 的规范
# JQuery Deferred
JQuery Deferred 也可以看做是 Promise 的前生了。语法其实和 Promise 还是略有相似
感兴趣的可以看下 `JQuery Deferred` 的用法
// 定义一个 Deferred
function waitHandle() {
var dtd = $.Deferred() //创建一个deferred 对象
var wait = function(dtd) {
// 要求传入一个deferred 对象
var task = function() {
console.log('执行完成')
dtd.resolve() // 表示异步任务已经完成
// dtd.reject() //表示异步任务失败或出错
setTimeout(task, 2000)
return dtd // 要求返回deferred 对象
}
//注意,这里一定要有返回值
return wait(dtd)
}
}
// 使用的时候
var w = waitHandle()
w.then(
function() {
console.log('ok 1')
},
function() {
console.log('error 1')
}
).then(
function() {
console.log('ok 2')
},
function() {
console.log('error 2')
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 定义 Promise
还是根据一开始讲的案例继续写代码(上传 4 张图片然后提交表单)
来看下 Promise 对于这个的实现:
// 注意少了callback了。只传入了要上传图片的内咯
function uploadImage(data) {
return new Promise(function(resove, reject) {
$.ajax({
url: 'xxx',
data: data,
success: function(res) {
// 用 resolve 代表成功
resolve({
status: 200,
data: res.data.file
})
},
fail: function() {
// reject 代表失败
reject({
status: 400,
data: ''
})
}
})
})
}
// 定义一个提交数据的方法
// file 就是对应的图片文件,就忽略不写了~
function submit() {
uploadImage({ name: 'img1', data: file })
.then(res => {
formData.img1 = res.data
return uploadImage({ name: 'img2', data: file })
})
.then(res => {
formData.img2 = res.data
return uploadImage({ name: 'img3', data: file })
})
.then(res => {
formData.img3 = res.data
return uploadImage({ name: 'img4', data: file })
})
.then(res => {
formData.img4 = res.data
// 最后就是提交 formData 了
})
.catch(res => {
// 当然也可以单独处理,写法很多
console.log('上传图片失败后的统一处理')
})
.finally(res => {
console.log('最后全部执行完成,不管有没有错误都会来这里')
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
小结
Promise 好像的确解决了回调地狱的问题,可是那么 then
还是看的眼花缭乱。明明都是差不多的方法,解决了回调地狱,可是又引来了新的链式调用过长的问题
而且,.then
方法中还需要特别注意,如果上一个方法没有 return
新的 Promise 对象,那.then
还是拿的上一个方法的值(总的来说可能编码错误率相对高一些)。需要时刻注意 链式调用 的规则,返回新的对象~
# Promise.all
基于刚才说的 Promise 方法,其实我们上传图片的动作都是一致的,甚至不一致的方法也可以
。为了避免写那么多的 .then
。Promise.all
就出现了
作用 Promise.all 主要用于一次性执行多个异步方法,并且按传入顺序执行,用法如下
function uploadImage(data) {
// 上传方法和Promise一样的了。就不重复写
}
Promise.all([
uploadImage({ name: 'img1', data: file }),
uploadImage({ name: 'img2', data: file }),
uploadImage({ name: 'img3', data: file }),
uploadImage({ name: 'img4', data: file })
])
.then(res => {
console.log(res) // 打印出来的res是个数组
// res = [
// { data: 'xxx', code: 200 },
// { data: 'xxx', code: 200 },
// { data: 'xxx', code: 200 },
// { data: 'xxx', code: 200 }
// ]
})
.catch(res => {
console.log('如果有一个图片上传失败,都会进到.catch 方法中')
})
.finally(res => {
console.log('最后全部执行完成,不管有没有错误都会来这里')
})
// Promise.all 接受的是一个数组。所以上面的代码还能这样写:
// 如果是一致的操作。可以通过循环来调用方法。用 map 返回一个数组
var uploadList = [
{ name: 'img1', data: file },
{ name: 'img2', data: file },
{ name: 'img3', data: file },
{ name: 'img4', data: file }
]
Promise.all(uploadList.map(item => uploadImage(item))).then(res => {
// ... 后面都一样了
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
uploadImage
部分可以换成其他的 Promise 封装的方法,他会执行这些 Promise 方法,统一拿到回调值。
# Promise.race
Promise.all 是把数组内的方法全部都执行完成才回调,而 Promise.race 则是哪个函数先执行完成,就会进回调,其他还没执行完成的函数则会被销毁
var fun1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 3000, 'fun1')
})
var fun2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 200, 'fun2')
})
// *注:这里使用的示例和promise.all用的示例类似,
// 可是没有用()执行,因为fun1就是一个promise对象,不需要执行
Promise.race([fun1, fun2]).then(function(result) {
console.log(result) // 'fun2' 因为fun2比较早执行结束
})
2
3
4
5
6
7
8
9
10
11
12
13
# 优缺点
- 优点
- 代码可读性稍微提高(依旧还是一串的
then
) - 如果使用 Promise.all 方法都是同步执行的(比如 4 张图片都是同时上传,而无需等待其他图片)
- Promise.race 更是添加了非常多的场景,例如同时烧两壶水,
那壶先开就用那一壶
,这就是典现的 Promise.race 应用场景。
- 代码可读性稍微提高(依旧还是一串的
- 缺点
- 刚才也说了,Promise.all 方法都是同步执行的。如果希望方法可以按顺序执行,我们还是要写很多的.then 回调,就像刚开始的 Promise 的例子一样
- 而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦。
# 火器时代 Generator
Generator 函数是一种可以中途暂停、异步执行的函数,它的语法很简单
function uploadImage(data) {
// 上传方法和Promise一样的了。就不重复写
return new Promise()
}
function* upload() {
yield uploadImage({ name: 'img1', data: file })
yield uploadImage({ name: 'img2', data: file })
yield uploadImage({ name: 'img3', data: file })
yield uploadImage({ name: 'img4', data: file })
}
function submit() {
var uploadFn = upload() // 执行后返回的是一个 Generator 对象
formData.img1 = uploadFn.next().data
formData.img2 = uploadFn.next().data
formData.img3 = uploadFn.next().data
formData.img4 = uploadFn.next().data
// 最后提交formData表单
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 优缺点
- 优点
- 和 Promise 相比。Generator 在异常处理方向会显得更加又好,如果出错了,可以直接就把
submit
方法return
掉。而不用等待其他方法执行回调
- 和 Promise 相比。Generator 在异常处理方向会显得更加又好,如果出错了,可以直接就把
- 缺点
- 就是每次都要执行
next()
显得很麻烦
- 就是每次都要执行
# 认知时代 async/await
Generator
的出现,并不是异步解决的最终方案。并且由于 Generator
比较特殊的语法,*
,yield
,.next
以至于其实很多人都没用过 Generator
。直接转向了 async/await
async/await 只是 generator 的语法糖。但是使用 async
的函数会返回 Promise
对象。所以在 await
后面的方法,只要是 Promise 对象都可以~
function uploadImage(data) {
// 上传方法和Promise一样的了。就不重复写
return new Promise()
}
// 按循序执行的例子
async function submit() {
// 最后提交formData表单
formData.img1 = await uploadImage({ name: 'img1', data: file }).data
formData.img2 = await uploadImage({ name: 'img2', data: file }).data
formData.img3 = await uploadImage({ name: 'img3', data: file }).data
formData.img4 = await uploadImage({ name: 'img4', data: file }).data
}
// 既然 await 后面只要是Promise函数就可以,那我们可以改成这样
async function submit2() {
var uploadList = [
{ name: 'img1', data: file },
{ name: 'img2', data: file },
{ name: 'img3', data: file },
{ name: 'img4', data: file }
]
var imgList = await Promise.all(uploadList.map(item => uploadImage(item)))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这样就万事大吉了吗?我们还忽略了异常处理的情况。既然 await
返回的是 Promise
对象,那就还得考虑错误的情况。这时候成功或者失败,都会返回到对应的对象/imgList
中。这并不是我们想看到的。
所以我们在定义一个 await 包装的方法
const awaitWrap = promise => {
return promise.then(data => [data, null]).catch(err => [null, err])
}
// 然后基于上面的代码可以写出
async function submit() {
const [img1, err1_msg] = await awaitWrap(uploadImage({ name: 'img1', data: file }))
const [img2, err2_msg] = await awaitWrap(uploadImage({ name: 'img2', data: file }))
const [img3, err3_msg] = await awaitWrap(uploadImage({ name: 'img3', data: file }))
const [img4, err4_msg] = await awaitWrap(uploadImage({ name: 'img4', data: file }))
}
2
3
4
5
6
7
8
9
10
11
awaitWrap
接收 Promise
对象,也返回 Promise
对象(还记得一开始 Promise 的时候说的嘛,如果.then
中没返回新的Promise
,那就还是当前自己的对象)
返回的新对象里面,返回了一个数组,直接把成功的参数和错误的参数分为 2 部分,这样我们就不会把成功/失败混淆在一起了
# 优缺点
- 优点
- 语法更加清晰,基于
Generator
的语法糖,可以按步骤执行,也可以配合Promise.all
多个函数同时执行
- 语法更加清晰,基于
- 缺点
- 兼容性 其实从 Promise 开始,IE 就开始不好兼容,当然网上有非常多的
pollyfill
去完善这个兼容性 - await 如果默认不处理的话成功和失败默认是不分开的,都在返回值里面,想优雅的处理还得自己封装下
- 兼容性 其实从 Promise 开始,IE 就开始不好兼容,当然网上有非常多的
# 总结
每个方法各有优缺点和用处。需要根据自身的场景,选择最合适的方法才是最好的
- 如果只是简单的一些回调,那肯定用
callbakc
是最方便,最快的 - 考虑到程序可能要按顺序加载/某个方法依赖另外一个异步方法执行的时候,优先使用
Promise
封装。这样拓展性目前是最好的(支持 async 和 Generator) - 最后如果要考虑到异步的执行顺序,我还是优先推荐
async/await
。这个可 Promise.all 并发,也可以一个个等待执行。不过后面的几个方法兼容性目前还不太好,还需要引入非常多的pollyfill
。可是很大的提升了可读性和维护性