开发日记(02) - js 异步任务队列

是不是恍然大悟,还可以这样???😲😲😲,是的就是这么简单。当然如果你足够牛逼,可以加个异步任务出错重试,超时啥啥的,现在这样我们的项目够用了。

开发日记(02) - js 异步任务队列

2021-01-31 20:40:22

0️⃣ 问题 ❓

算是之前项目遗留下来的一个问题。一直困扰着我。

还是关于 uni-app 以及 Vue 项目的网路请求,有这么一个需求,项目内有一个全局使用到数据,我们称为**“数据字典”**。需要在项目一打开就加载进来,存入到 Vuex,后续使用就不需要再请求网络了,使用的时候先判断 Vuex 内有没有数据,没有就去请求,有就用现成的。一般像这样的数据不需要经常更新。 有点类似于项目的全局配置,一打开就需要请求,然后在特定的页面需要用到。

因为是全局需要用到,所以我们在 App.vueonLaunch 应用生命周期(uni-app 的应用生命周期,与 Vuemounted 生命周期类似)进行一次请求

数据字典就是用来格式化类似性别订单状态的,比如后台返回一个订单列表,订单状态为 1,2,3,4,5...

前端不可能显示为纯数字,这个时候可以根据后台给到的解释进行判断显示。不过这种方法有一个缺点,就是每增加一种状态前端都得改代码。

数据字典就是一种比较好的解决方式,订单状态全部放在后端维护,前端使用数据字典配置好的状态说明进行格式化显示就很灵活了。

${app}/src/App.vue

import { getDictByKey } from '@/store/util'
export default {
  onLaunch() {
    console.log('App Launch')
    getDictByKey()
  },
}

在页面中这么用

${app}/src/pages/foo/foo.vue

import { getDictByKey } from '@/store/util'
export default {
  async onLoad() {
    // 请求类型信息
    const typeList = await getDictByKey('title_type', 'all')

    if (typeList && typeList.length) {
      this.typeList = typeList
    }
  },
}

这样用效果是可以实现的,但是我在 foo.vue 页面刷新页面的时候,就会触发两次相同的网络请求,拉了两遍请求数据字典的接口,因为 App.vue 有一次请求,foo.vue 也有一次网络请求。

这个接口数据量相对来说比较大,就会卡顿一下,拉取两次本来也是不正确的,虽然需求完成了,但是出于码农的强迫症和“职业道德”,这个问题不能蒙混过关,一定要解决它。

1️⃣ 解决方案

本地存一份数据

这是我们最开始用的解决方案,因为数据不经常更新,我们就把这个接口返回的 json 存为文件,然后在项目直接引入,只在 App.vue 进行一次请求更新。在页面内使用不请求。

一开始倒是没什么问题,项目经过迭代之后,数据字典模块也随之更新了,这样就造成了,假如我在 foo.vue 页面刷新数据,网络请求还没回来,本地没有的数据就显示空白。

异步任务队列

有看过关于任务队列的介绍,像 mqkafka,都是用来做消息队列的。 mysql 的事务隔离模式也有类似的。异步任务队列有点类似于 “串行化”,画张图大家感受下

flow.png

不管有多少个人问我要数据,我都把你们的请求存起来,我去拿数据,等我拿到了,我自己存起来,再一个个给你们。这样网络请求只发送一次,但是项目内同时可以有多个请求,类似的操作不仅仅在请求网络的时候能用到。

2️⃣ 代码实现异步任务队列

老规矩,直接上完整代码,代码不多,已经在项目内用上了,没有发现问题。拷贝需要修改成你自己的业务逻辑。

下面来慢慢分析代码,先说实话,点子是我自己想的,我实现不出来,就去网上找了蛮久,找到了一个看起来不错的优雅实现(其实就是代码比较少,改起来简单点 😁)

import store from '@/store/index'
import { handleApiRequestException } from '@/util/handle-error'

// 任务队列
const queue = []

/**
 * @name 通过字典的key值获取字典的value(添加任务)
 * @param {string} key 数据字典的 key 值
 * @param {*} value 用来标识请求所有还是单个值
 */
export function getDictByKey(key, value) {
  return new Promise((resolve) => {
    const task = { resolve, key, value }
    queue.push(task)
    if (queue.length === 1) {
      _next(task, true)
    }
  })
}

/**
 * @name 执行任务
 * @param {object} nextPromise 任务对象
 * @param {boolean} first 是否是第一个任务
 */
async function _next(nextPromise, first) {
  const { resolve, key, value } = nextPromise

  if (!store.state.dict.length) {
    try {
      await store.dispatch('getDictList')
      resolve(store.getters.filterDict(key, value))
    } catch (error) {
      handleApiRequestException(error)
      resolve(value === 'all' ? [] : '')
    }
  } else {
    resolve(store.getters.filterDict(key, value))
  }
  let task = queue.shift()
  if (first) {
    task = queue.shift()
  }
  task && _next(task)
}

我们从第一行开始看起

import 进来了两个东西,一个是 Vuex 实例,一个是错误处理。

queue 这个就是我们要的任务队列了。我们需要数据的请求,一个个往里面添加。后面执行完成了的任务会通过 shift 弹出去。

getDictByKey 就是请求,来看看页面怎么用的

import { getDictByKey } from '@/store/util'
// 获取时间单位类型
this.dateTypeList = await getDictByKey('time_type', 'all')

这里用到了 Promise 对象的一个特性,没有 resolve 就会一直阻塞。

_next 方法用来启动执行任务,传入一个任务和是否为第一个任务的标识,

  1. 取出任务

  2. 判断 Vuex 数据源是否已经有值

    如果有

    1. 直接 resolve 同步函数执行的结果

    如果没有

    1. 执行网络请求
    2. 请求成功存入 Vuex 数据仓库
    3. resolve 同步函数执行的结果
    4. 请求失败, resolve 空数据,同是进行错误处理(toast 提示)
  3. 拿到下一次任务

  4. 判断是不是第一次任务,如果是需要弹出,因为第一次任务已经执行过了,并且 resolve

  5. 判断任务是否存在,存在就继续使用 _next 任务执行函数执行第一步,没有任务就执行完毕了

3️⃣ 总结

是不是恍然大悟,还可以这样???😲😲😲,是的就是这么简单。当然如果你足够牛逼,可以加个异步任务出错重试,超时啥啥的,现在这样我们的项目够用了。


还有啊,如果你认真看了我的文章,解决了你的问题,我建议你关注下我,至少给我点个赞。你不要“不知好歹”,毕竟我还有很多问题的解决方案。

你们要是但凡有一个人给我提个问题,也不至于我王者荣耀周末“五连跪”!😤

📔 开发日记系列

只记录些平时开发觉得有用的东西,有问题请务必斧正,拜托了 🙏🙏🙏

  1. 开发日记(01) - uni-app 使用等宽字体对其数字显示
  2. 开发日记(02) - js 异步任务队列
  3. 开发日记(03) - uni-app 打包为 app

关于我

SunSeekerX,

全栈开发、区块链开发、移动端开发、前后端开发、NodeJS 开发、小程序、uni-app 开发、等

喜欢探讨技术实现方案和细节,完美主义者,见不得 bug

Github:https://github.com/SunSeekerX

个人博客:https://yoouu.cn/

个人在线笔记:https://doc.yoouu.cn/