小程序框架提供丰富的原生API,可以方便调起微信提供的能力,如获取用户信息,本地存储,支付功能等。但大多数API为异步调用,需要传递成功或失败回调函数,例如wx.request
发起https请求需要在成功或失败回调中书写业务逻辑,这时就很容易会遇到回调地狱问题。另外一方面,错误或异常处理会和业务代码写在一起,代码耦合高。
具体业务场景示例,小程序登录流程需要先去调用微信登录接口wx.login
获取code
值,登录成功后再去调用获取用户信息接口wx.getUserInfo
获取用户相关信息,拿到code
和userInfo
后调用业务登录接口换取登录信息。在这种多层异步嵌套场景中,假如使用回调处理逻辑,就会写出一层嵌一层的代码,例如下面伪代码:
// 登录流程
wxLogin((res) => {
let code = res.code
// 登录后获取用户信息
wxGetUserInfo((res) => {
let someUserData = res
let data = {
code,
...someUserData
}
// 请求业务接口,换取登录信息
requestDjLogin(data, (res) => {
// 保存登录信息
// 业务处理...
}, (err) => {
// 异常处理
})
}, (err) => {
// 异常处理
})
}, (err) => {
// 异常处理
})
如上所示,采用回调函数解决异步问题。三个异步操作两个回调,代码已经开始变得不方便维护。或许三层异步操作还没有达到忍无可忍极限,但如果业务场景需要五层嵌套或更多情况下,就需要采用新的方式书写异步代码。
ES6中提出Promise对象语法。Promise对象是一个代理对象,允许为异步代码执行结果的成功和失败分别绑定相应的处理方法
语法
new Promise((resolve, reject) => {
// 异步操作
})
Promise有以下几种状态:
pending 状态的 Promise 对象可能以 fulfilled 状态返回了一个值,也可能reject返回一个值。then方法包含两个函数类型参数:onfulfilled 和 onrejected。当fulfilled状态时,调用 then 的 onfulfilled 方法,当Promise被拒绝时,调用 then 的 onrejected 方法。同时then方法返回 promise对象自身支持链式调用。
结合promise语法我们可以将上面的代码修改为更容易维护的代码。将上面三个异步操作封装为promise对象。
// 微信登录API promise封装
function wxLogin () {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => resolve(res),
fail: (err) => reject(err)
})
})
}
// 微信获取用户信息API 封装
function wxGetUserInfo () {
// return new Promise ...
}
// 请求业务登录接口封装
function requestDjLogin () {
// return new Promise ...
}
// 登录流程代码
let code = null
let someUserData = null
// 获取微信登录code
wxLogin().then((res) => {
code = res.code
// 微信登录成功后,
// 返回获取用户信息 promise对象
return wxGetUserInfo()
}).then((res) => {
someUserData = res
let data = {
code,
...someUserData
}
// 获取用户信息成功后,
// 返回请求业务登录接口 promise对象
return requestDjLogin(data)
}).then((res) => {
// 保存登录信息
}).catch((err) => {
// 异常处理
console.log(`err = ${err}`)
})
上面代码大致实现将异步代码从函数嵌套转为同步书写风格,避免回调地狱问题。如果业务场景需要更深层的嵌套异步操作,只需要在 then
函数的成功回调内继续 返回接下来的异步操作的 promise
对象,支持链式调用。这种书写方式更容易维护。异常处理可以在then
函数第二个参数内写,也可以统一在catch()
里面做处理。
接着说,如果链式调用里then函数成功回调内,我们需要逻辑判断是否返回异步操作的 promise
对象,或同步返回具体结果。但同时又希望链式调用能够维持。这时我们可以采用Promise.resolve(value)
语法,示例:
wxLogin().then((res) => {
code = res.code
// 微信登录成功后,判断之前是否缓存过用户信息
// 如果缓存过直接去本地缓存信息
// 如果未缓存,则需要异步请求用户信息
let userInfo = getStorageUserInfo()
if (userInfo) {
return Promise.resolve(userInfo)
} else {
return wxGetUserInfo()
}
}).then((res) => {
// ... 后续业务处理
}).catch((err) => {
// 异常处理
console.log(`err = ${err}`)
})
再有,假如我们在promise的链式调用中的遇到在 then函数成功回调内进行逻辑判断,需要根据接口返回的数据进行异常处理。假如接口数据返回有问题,不希望后面promise继续执行。则可以采用Promise.reject(reason)
语法。比如通常接口返回数据会携带code
字段,code
非0表示接口异常需要异常处理,那么可以采用下面的写法:
getIndexinfo(app).then((res) => {
// code 不为 0,返回错误异常
if (res.code == 0) {
// 接口数据成功处理
this.setData({
info: res.data
})
} else {
return Promise.reject('接口返回异常')
}
}).catch((err) => {
console.log(`err = ${err}`)
})
更复杂的场景,如果我们在链式调用的过程中有一个then
函数内部需要同时做多个异步操作,后面异步操作需要在前面同时进行的异步操作结束返回结果后执行。那么可以使用Promise.all(iterable)
语法,then函数的成功回调会拿到由所有promise返回数据组成的数组,顺序与promise.all
传递数组顺序一致。业务场景示例,在小程序中我们需要首先获取商品的基本信息,通过商品的基本信息获取服务时间和商品服务项。
// 获取商品信息
// 根据商品信息获取,商品服务时间以及服务项
getProductinfo(app).then((res) => {
// code 不为 0,返回错误异常
if (res.code == 0) {
// 接口数据成功处理
this.setData({
info: res.data
})
// 同时获取服务时间和服务项
// getServiceTime 获取服务时间 promise 接口
// getServiceType 获取商品服务项 promise 接口
return Promise.all([getServiceTime, getServiceType])
} else {
return Promise.reject('接口返回异常')
}
}).then((res) => {
// res 是包含Promise.all里所有promise返回值的数组
// ...业务处理
}).catch((err) => {
console.log(`err = ${err}`)
})
同时也存在Promise.race(iterable)
语法,当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
以上简单将 Promise 语法和实际场景中业务逻辑结合在一起进行演示。采用promise 语法替代回调函数处理异步代码更直观。
虽然目前 promise 已经可以将嵌套函数进行展平,但是写代码和阅读依然有额外的负担。在ES7中有了更加标准的解决方案,新增 async/await 两个关键词。async 声明一个异步函数,await 操作符用来等待 promise 或任何值。
await 表达式会造成异步表达式停止执行并且等待 promise 的完成,当值被 resolved,异步函数会恢复执行以及返回 resolved 值。如果该值不是一个 promise,它将会被转换成一个 resolved 后的 promise。如果 promise 被 rejected,await 表达式会抛出异常值。
// 登录流程
(async () => {
try {
// 异步调用微信登录
let code = await wxLogin()
// 异步获取用户信息
let userInfo = await wxGetUserInfo()
// 异步请求业务登录接口
let data = {
code,
...userInfo
}
let djLoginInfo = await requestDjLogin(data)
// 存储业务登录信息
// 后续处理...
} catch (err) {
// 异常处理
console.log(`err = ${err} `)
}
})()
wxLogin、wxGetUserInfo、requestDjLogin 函数同样为返回 promise 对象的异步操作。可以看出,async/awit 语法相对于 promise().then()更加简洁清晰。上面三个异步请求代码书写方式变成顺序书写,不存在回调函数嵌套问题。如果遇到同时执行多个异步操作的场景需要使用前面提到的 Promise.all([]) 语法。
let info = await Promise.all([getServiceTime, getServiceType])
await 异步操作 promise 函数中,reject异常可以通过外部 try catch 语法获取。另外如果需要在逻辑判断的地方手动抛出异常,可以采用 await Promise.reject('接口异常') 语法。外部使用 try catch语句内进行处理。
在前端可能不会遇到太深的嵌套回调问题,在小程序场景下api大部分为异步调用,异步代码嵌套使用场景也更丰富。es6、es7语法对这个问题提出新的解决方式,promise、async/await语法。通过新语法可以将异步嵌套代码变得顺序执行,书写方便更容易维护和理解。
本文分享自 交互设计前端开发与后端程序设计 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!