0%

教你从头到尾解决小程序回调地狱

示例代码逻辑上有点小 bug ,但不影响。


由于历史原因,微信小程序的 异步API 大部分使用的都是 回调函数 或者 then() 的方法;虽然很直观,但是一旦逻辑复杂起来就会变成「屎山」。什么叫屎山?下面这个就叫屎山。

1
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
// 忽略了 this.setData()
setUserInfo(event) {
const userInfos = wx.cloud.database().collection('userInfo')
wx.getSetting({
success: (res) => {
if (!!res.authSetting["scope.userInfo"]) {
wx.getUserInfo({
lang: 'zh_CN',
success: (res) => {
wx.cloud.callFunction({
name: 'getOpenId'
}).then(res => {
userInfos.where({
_openid: res.result.openId
}).count().then(res => {
if (res.total === 0) {
userInfos.add({
data: event.detail.userInfo
}).then(() => {
wx.showToast({
title:'注册成功'
})
wx.reLaunch({
url: '/pages/my/my',
})
})
} else {
wx.showToast({
title:'登录成功'
})
wx.reLaunch({
url: '/pages/my/my',
})
}
})
})
},
})
} else {
Toast.fail('请允许')
}
},
})
},

本文将通过 Promise对象 , async/await函数 从「简单」到「高级」的化解、切割、消灭屎山。(之后简称 Promise 和 async/await)


何来:什么是「异步」

就是

刚入门的童鞋可能不清楚什么是异步,建议先看下面这篇文章初步的了解一下。

Javascript异步编程的4种方法

着重了解概念和第一,第四个方法即可

小程序有非常多的 异步API ,只要是提供了 回调函数 的,都可以视为是 异步API

在实际的业务中,经常需要根据上一个 API 的结果来执行下一个 API。与 回调函数 结合起来就形成下面这个屎山 的原因

1
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
53
setUserInfo(event) {
const userInfos = wx.cloud.database().collection('userInfo')
wx.getSetting({
success: (res) => {
if (!!res.authSetting["scope.userInfo"]) {
wx.getUserInfo({
lang: 'zh_CN',
success: (res) => {
const userInfo = res.userInfo
wx.cloud.callFunction({
name: 'getOpenId'
}).then(res => {
userInfos.where({
_openid: res.result.openId
}).get().then(res => {
if (res.total === 0) {
userInfos.add({
data: userInfo
}).then((res) => {
userInfo._id = res._id
userInfo._openid = _openid
this.setData({ userInfo })
wx.showToast({
title: '注册成功'
})
wx.reLaunch({
url: '/pages/my/my',
})
})
} else {
this.setData({ userInfo: res.data })
wx.showToast({
title: '注册成功'
})
wx.reLaunch({
url: '/pages/my/my',
})
}
})
})
},
})
} else {
Toast.fail('请允许')
}
},
})
wx.getUserInfo({
success: function (res) {
console.log(res.userInfo)
}
})
},

回调函数写起来很爽,但是哪天你需要改变某个回调后的逻辑时;你就得像手剥洋葱含着泪一层一层的「剥开」它


既然 回调函数 这么麻烦,那么把回调的结果赋值给外面,或者把逻辑用方法分离出去不行吗?

  1. 没等你异步函数执行回调,后面的代码就执行了,值都是undefined

  1. 抽象成方法「治标不治本」,改嵌套的还是嵌套了

天下苦回调久矣,所以就有了「事件监听」、「发布/订阅」、「Promises对象」等等的改良方法;但是他们都有各自的缺点。

直到五年前 Promiseasync/await 的面世,JavaScript 的异步才算迎来了它的「终极解决」办法


  • English (中文):Promise (承诺)、async (异步)、await (等待)。
  • Promises 对象可以看做 Promise 的前世,他们两个不是同一个东西 。
  • 由于 Promise (ES6加入)和 async/await (ES7加入)太「新」了,需要 Babel 等转码器转码之后才能运行到旧浏览器。
    • vue-cli 的 build 和 小程序编译就是在干这件事
  • 在 Nodejs 的 API 中使用的就是 发布/订阅 (事件)的异步方法,例如 fs
    • Node不支持 ES6 语法
      • 可以使用 Babel转换来支持 ES6

化解:async / await 函数

使用 async/await 可以简化 回调中的 then

asyncawait 是 JavaScript 中的关键字,await 顾名思义就是 「等待」。

他的使用我直接举个例子吧,虽然微信小程序新 API 都提供了 Promise 对象的调用类型,但是使用 then() 还是会出现「嵌套」问题的:

1
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
getUserInfo() {
// 调用 getOpenId 云函数 获取 openid 之后查询数据库并将结果打印出来
wx.cloud.callFunction({
name: 'getOpenId'
}).then(res => {
userinfos.where({
openid: res.result.openid
}).get().then(res => {
console.log(res.data)
})
})
},

// 使用 async / await 之后

async getUserInfo() {

// 调用云函数获取 openid
var openid = await wx.cloud.callFunction({
name: 'getOpenId'
})

// 根据 openid 查询数据库中的用户信息
var res = await userinfos.where({
openid: openid.result.openid
}).get()

// 打印用户信息
console.log(res.data)
},

使用 async / await 之前, Promise 的结果只能在 then 里取到,然后再进行下一步操作;

使用 async / await 之后会将结果赋值给前面的变量,没赋值的话 await 也会等待异步执行完成才执行下一步。是不是有点像之前 「回调函数」里幻想的「赋值给外面」,从嵌套解构变成了个线性解构。


对了,不要以为 then 就没用了,它可以这样使用:返回只需要的值,简化后续的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async getUserInfo() {

// 调用云函数获取 openid
var openid = await wx.cloud.callFunction({
name: 'getOpenId'
}).then(res => {
return res.result.openid
})

// 根据 openid 查询数据库中的用户信息
var userInfo = await userinfos.where({
openid
}).get().then(res=>{
return res.data
})

// 打印用户信息
console.log(userInfo)
},

学会使用 async/await 之后,那坨屎山就是这个样子了

  • 由于使用了async 标记了整个函数,整个函数已经是「异步」了,所以执行异步函数时页面并不会「假死」。
  • await 后面只能是 Promise ,标记使用 回调函数 的小程序的 API 是没有用的(目前来说)。

切割:用 Promise 「包装」 回调函数

await 后面只能是 Promise ,标记使用「回调函数」的小程序的 API 是没有用的。

那么,对于 回调函数 就没有办法了吗。当然不是,不然 Promise 也不会被称为「终极解决」方法了。

这是获取用户设置的例子

1
2
3
4
5
wx.getSetting({
success(res) {
console.log(res);
}
})

可以使用 Promise 来「包装」这个 回调函数

当 success 回调函数执行到 resolve (解决) 这一行时,就会调用外层的 then()

1
2
3
4
5
6
7
8
9
new Promise((resolve) => {
wx.getSetting({
success(res) {
resolve(res)
}
})
}).then(res => {
console.log(res);
})

结合上面的 async/await ,「完美」解决了嵌套问题

1
2
3
4
5
6
7
8
const res = await new Promise((resolve) => {
wx.getSetting({
success(res) {
resolve(res)
}
})
})
console.log(res);

  • 不是所有回调函数都需要「包装」,当出现两层以上回调函数嵌套的,才推荐使用 Promise 进行 「包装」。简而言之:禁止过度「包装」

消灭: 微信API 支持 Promise

在学会了使用 async/await 标记函数 和用 Promise 包装 异步API 之后,就再也没有被异步嵌套困扰过。

直到某天老师给的实验作业中出现了个意想不到的骚操作,之前需要「包装」的 API 可以直接使用 await 来等待回调,并将结果赋值给前面的变量。

原来在2月份的时候微信更新了一波。基础库 2.10.2 版本起,异步 API 支持 callback & promise 两种调用方式

也就是说,之后再也不需要「包装」异步 API 了;因为已经支持 Promise 调用了。

不过实际感受并不是特别完善,开发者工具并没有进行适配。使用 await 会有警告。


并且没有片段提示


相比之下,云开发的 API 是有提示的


当然你可以使用 JSDoc 的 @type 来强行标记类型,是会有提示的。

所以,建议基础不太好的童鞋还是老老实实用「包装」的方法来解决回调嵌套比较好;

  • 佛系更新的官方都支持 Promise 了,说明 异步API Promise 化这条路是正确的。
  • 在官方更新 Promise 调用的片段(提示)前,萌新还是建议使用 Promise 「包装」异步API。毕竟拼写错误导致的报错的问题不是一次两次了,依赖片段提示可以极大的避免拼写错误。