解决uni.showToast被uni.hideLoading关闭的问题

1,631 阅读3分钟

1、主要问题

在使用uniapp进行小程序开发的时候发现一个问题,当调用uni.showToast后立即调用uni.hideLoading,uni.showToast会被立即关闭,导致弹出的提示一闪即逝,猜测小程序的toast和loading可能使用的是同一个组件

/**
 * 1.前面一个任务执行完成,后面的 'toast' | 'showLoading' | 'hideLoading' 才能执行
 * 2.hideLoading 执行逻辑   ->  当有提示弹窗(toast)的时候,hideLoading必须等到toast完成才能执行,没有toast可以立即执行关闭showLoading,
 * 3.toast 执行逻辑   ->  当有提示弹窗(toast)的时候,toast必须等到上一个toast完成才能执行
 * 4.showLoading 执行逻辑   ->  当有提示弹窗(toast)的时候,showLoading必须等到上一个toast完成才能执行,没有toast,就直接执行
 *
 * 注意,showLoading 不会覆盖toast, toast不会覆盖前面的toast,hideLoading会立即关闭前面的showLoading,不会对关闭toast
 */
interface TaskItem {
  fun: 'toast' | 'showLoading' | 'hideLoading'
  title?: string
  duration?: number
  resolve?: (value: any) => void // 任务执行完成后的回调
  reject?: (reason: any) => void
}

class Tip {
  // 任务队列
  taskList: Array<TaskItem>

  duration: number

  // 当前执行的任务
  curTask: TaskItem | null

  constructor() {
    // 任务队列
    this.taskList = []

    // toast弹出时间
    this.duration = 1500

    // 当前执行的任务
    this.curTask = null
  }

  // 添加任务
  pushTask(obj: TaskItem) {
    // 如果是hideLoading,需要立即执行
    if (obj.fun === 'hideLoading') {
      this._runHideLoadingTask()
    } else {
      this.taskList.push(obj)
      if (!this.curTask) this._runTask()
    }
  }

  // 添加提示弹框
  toast(title: string) {
    return new Promise((resolve, reject) => {
      this.pushTask({ fun: 'toast', title, reject, resolve })
    })
  }

  // 添加加载弹框,不手动调用hideLoading,弹窗会再6000后关闭
  showLoading(title = '加载中', duration = 6000) {
    this.pushTask({ fun: 'showLoading', title, duration })
  }

  // 添加隐藏弹框
  hideLoading() {
    this.pushTask({ fun: 'hideLoading' })
  }

  setCurTask(task: TaskItem | null) {
    this.curTask = task
  }

  // 小提示弹框
  _runToastTask() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const _this = this
    return new Promise((resolve, reject) => {
      uni.showToast({
        title: `${_this.curTask?.title}`,
        icon: 'none',
        duration: _this.duration,
        success(e) {
          setTimeout(() => {
            _this.setCurTask(null)
            _this.taskList.shift()
            resolve(e)
          }, _this.duration)
        },
        fail: reject,
      })
    })
  }

  // 加载弹窗
  _runShowLoadingTask() {
    return new Promise<void>((resolve) => {
      uni.showLoading({ title: `${this.curTask?.title}` })
      // eslint-disable-next-line @typescript-eslint/no-this-alias
      const _this = this
      setTimeout(() => {
        // 如果当前执行的任务不是showLoading,那说明已经被 hideloading 关闭,就不需要执行自动关闭
        if (this.curTask?.fun !== 'showLoading') {
          resolve()
        } else {
          // 到了指定的时间,自动关闭
          this.setCurTask(null)
          this.taskList.shift()
          resolve()
        }
      }, _this.curTask?.duration)
    })
  }

  // 关闭showLoading
  _runHideLoadingTask() {
    // 只有当当前任务为showLoading才执行关闭loading
    if (this.curTask?.fun === 'showLoading') {
      uni.hideLoading()
      this.taskList.shift()
      this.setCurTask(null)
      this._runTask()
    }
  }
  
  // 运行任务
  _runTask() {
    // 队列没有任务退出
    if (this.taskList.length === 0) return
    // 如果当前有正在执行的任务,不继续执行
    if (this.curTask) return
    // 取出第一个元素 ,shift 会改变数组长度
    this.curTask = this.taskList[0] as TaskItem
    const next = () => {
      this._runTask()
    }
    switch (this.curTask.fun) {
      case 'toast':
        this._runToastTask().then(this.curTask.resolve).catch(this.curTask.reject).finally(next)
        break
      case 'showLoading':
        this._runShowLoadingTask().finally(next)
        break
      default:
        throw new Error('提示任务异常')
    }
  }
}
// 只允许创建一个Tip实例
const getTipInstance = (() => {
  let tip: Tip
  return () => {
    if (!tip) {
      tip = new Tip()
    }
    return tip
  }
})()

const tip = getTipInstance()

/**
 * 提示
 */
export const toast = tip.toast.bind(tip)
/**
 * 弹窗默认6秒后关闭,也可以传入第二个参数控制显示时间
 */
export const showLoading = tip.showLoading.bind(tip)
/**
 * 立即关闭弹窗
 */
export const hideLoading = tip.hideLoading.bind(tip)

2、三个方法

第一个toast是弹窗提示,toast的默认执行时间为1500,返回了一个promise,提示完成后会状态会变成pedding,toast不会覆盖前面的toast,toast不会被hideLoading关闭

第二个showLoading是显示加载,showLoading如果不被hideLoading关闭,到了指定的时间(函数的第二个参数)会被关闭,函数默认的6是秒,showLoading 不会覆盖toast

第三个hideLoading是关闭加载弹框,会立即关闭前面的showLoading,对toast不会有操作

3、测试代码

// 测试弹窗
setTimeout(() => {
  toast('你好1')
  toast('你好2')
  showLoading('加载', 3000) // 加载出现后1.5秒弹窗hideLoading()被关闭,没有hideloading,三秒后关闭
  toast('1')
  toast('2')
  // hideLoading()
}, 1000)

setTimeout(() => {
  hideLoading()
}, 4500)