前端异步代码解决方案实践(一)

小程序框架提供丰富的原生API,可以方便调起微信提供的能力,如获取用户信息,本地存储,支付功能等。但大多数API为异步调用,需要传递成功或失败回调函数,例如wx.request发起https请求需要在成功或失败回调中书写业务逻辑,这时就很容易会遇到回调地狱问题。另外一方面,错误或异常处理会和业务代码写在一起,代码耦合高。

具体业务场景示例,小程序登录流程需要先去调用微信登录接口wx.login获取code值,登录成功后再去调用获取用户信息接口wx.getUserInfo获取用户相关信息,拿到codeuserInfo后调用业务登录接口换取登录信息。在这种多层异步嵌套场景中,假如使用回调处理逻辑,就会写出一层嵌一层的代码,例如下面伪代码:

// 登录流程
wxLogin((res) => {
  let code = res.code

  // 登录后获取用户信息
  wxGetUserInfo((res) => {
    let someUserData = res
    let data = {
      code,
      ...someUserData
    }

    // 请求业务接口,换取登录信息
    requestDjLogin(data, (res) => {
      // 保存登录信息
      // 业务处理...
    }, (err) => {
      // 异常处理 
    })
  }, (err) => {
      // 异常处理
  })
}, (err) => {
  // 异常处理
})

如上所示,采用回调函数解决异步问题。三个异步操作两个回调,代码已经开始变得不方便维护。或许三层异步操作还没有达到忍无可忍极限,但如果业务场景需要五层嵌套或更多情况下,就需要采用新的方式书写异步代码。

Promise对象

ES6中提出Promise对象语法。Promise对象是一个代理对象,允许为异步代码执行结果的成功和失败分别绑定相应的处理方法

语法

new Promise((resolve, reject) => {
  // 异步操作
})

Promise有以下几种状态:

  • pending: 初始状态,未履行或拒绝
  • fulfilled: 意味着操作成功完成
  • rejected: 意味着操作失败

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()里面做处理。

Promise.resolve(value)

接着说,如果链式调用里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.reject(reason)

再有,假如我们在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}`)
})

Promise.all(iterable)

更复杂的场景,如果我们在链式调用的过程中有一个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)

同时也存在Promise.race(iterable)语法,当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

以上简单将 Promise 语法和实际场景中业务逻辑结合在一起进行演示。采用promise 语法替代回调函数处理异步代码更直观。

ES7 async/await语法

虽然目前 promise 已经可以将嵌套函数进行展平,但是写代码和阅读依然有额外的负担。在ES7中有了更加标准的解决方案,新增 async/await 两个关键词。async 声明一个异步函数,await 操作符用来等待 promise 或任何值。

await 表达式会造成异步表达式停止执行并且等待 promise 的完成,当值被 resolved,异步函数会恢复执行以及返回 resolved 值。如果该值不是一个 promise,它将会被转换成一个 resolved 后的 promise。如果 promise 被 rejected,await 表达式会抛出异常值。

async/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 异常处理

await 异步操作 promise 函数中,reject异常可以通过外部 try catch 语法获取。另外如果需要在逻辑判断的地方手动抛出异常,可以采用 await Promise.reject('接口异常') 语法。外部使用 try catch语句内进行处理。

总结

在前端可能不会遇到太深的嵌套回调问题,在小程序场景下api大部分为异步调用,异步代码嵌套使用场景也更丰富。es6、es7语法对这个问题提出新的解决方式,promise、async/await语法。通过新语法可以将异步嵌套代码变得顺序执行,书写方便更容易维护和理解。

原文发布于微信公众号 - 交互设计前端开发与后端程序设计(interaction_Designer)

原文发表时间:2018-07-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术之路

名称空间

using 声明和using 编译指令 using 声明将特定的名称添加到它所属的声明区域中。 using std::cout; 将cout添加到声明区中,声明...

1867
来自专栏chenssy

【死磕Java并发】-----Java内存模型之happens-before

在上篇博客(【死磕Java并发】—–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性...

1202
来自专栏云计算教程系列

如何使用Grep

Grep是一个命令行实用程序,可以使用常见的正则表达式语法搜索和过滤文本。它无处不在,动词“to grep”已经成为“搜索”的同义词。它grep是一个有用的工具...

1153
来自专栏行者常至

008.多线程-synchronized锁

为了解决线程安全问题, 我们的做法是:不要让多个线程同时对一个全局变量作写的操作。

1012
来自专栏IMWeb前端团队

json-schema入门

json-schema(一) 相关知识点 它是什么 描述json的数据格式 有什么优点 描述自定义的数据格式 清晰,对人和机器友好 完整的结构校验 自动化测试 ...

4560
来自专栏开发与安全

X86 寻址方式、AT&T 汇编语言相关知识、AT&T 与 Intel 汇编语言的比较、gcc 嵌入式汇编

注:本分类下文章大多整理自《深入分析linux内核源代码》一书,另有参考其他一些资料如《linux内核完全剖析》、《linux c 编程一站式学习》等,只是为了...

4280
来自专栏Python专栏

python如何定时异步执行任务

2953
来自专栏青青天空树

mfc学习之路--如何删除通过控件新增的变量

   刚刚学校mfc的人都会遇到这样一个问题(比如我),在照做书做一个mfc程序,给控件新增变量时变量类型错了,但是变量名对了,然后想要加个正确的时候提示"已经...

975
来自专栏北京马哥教育

面试linux运维一定会问到Shell脚本这24个问题

虽然现在Python在运维工作中已经使用很普遍,但是很多企业在找Linux云计算工程师的时候还是会问到 shell 脚本的问题,它有助于你在工作环境中自动完成很...

2431
来自专栏技术博客

Asp.Net MVC对类HtmlHelper的自定义扩展方法以及如何调用

c# 扩展方法出来已久,介绍扩展方法的文章也很多,此处就不多介绍,如有不懂当然可以百度,google一下,园子当中也有超级多的讲解。

2061

扫码关注云+社区

领取腾讯云代金券