【解决方案】Vue3多组件异步任务队列

·  阅读 658
【解决方案】Vue3多组件异步任务队列

关于 Vue3mitt.js 的使用方法我在另一篇文章中有介绍

整理的一些Vue3知识点(550+👍)

需求介绍

最近公司有个需求,是一个移动端页面。一个页面包含多个楼层,每个楼层是一个单独的组件。每个组件内部有自己的逻辑。

微信截图_20210707080035.png

页面是类似于个人中心的福利页面,每个楼层展示对应礼包的图片,用户进入页面以后,在满足条件的前提下,自动弹出领取礼包的弹窗。

微信截图_20210707080549.png

控制每个礼包的弹窗显示隐藏的状态分别写在各自的组件中,现在的需求是

💡 每次只能展示一个弹窗

💡 无论点击确认还是取消,关闭上一个弹窗之后,自动打开第二个弹窗

💡 可以控制弹窗展示的顺序

微信截图_20210707083400.png

解决方案

技术栈

  • Vue3
  • mitt.js
  • Promise

思路

每个弹窗都视为一个异步任务,按预设顺序构建一个任务队列,然后通过点击按钮手动改变当前异步任务的状态,进入到下一个异步任务。

步骤一

先写两个组件模拟一下实际情况

父组件(页面组件)

<template>
  <div>
    <h1>我是父组件!</h1>
    <child-one></child-one>
    <child-two></child-two>
    
    <div class="popup"
     v-if="showPopp">
      <h1>我是父组件弹窗</h1>
    </div>
  </div>
  
</template>
<script>
import { defineComponent } from 'vue'

import ChildOne from './components/Child1.vue'
import ChildTwo from './components/Child2.vue'

export default defineComponent({
  name: '',
  components: {
    ChildOne,
    ChildTwo,
  },
  setup() {
    //控制弹窗显示
    const showPopp = ref(false)
    return {
      showPopp,
    }
  },
})
</script>
复制代码

子组件一

<template>
    <div>
      我是楼层一
    </div>

    <div class="popup"
       v-if="showPopp">
        <h3>我是弹窗一</h3>
        <div>
          <button @click='cancle'>取消</button>
          <button @click='confirm'>确定</button>
        </div>
    </div>
</template>
<script>
import { defineComponent, ref } from 'vue'


export default defineComponent({
  name: '',
  setup() {
    //控制弹窗显示
    const showPopp = ref(false)
    //取消的逻辑
    const cancle = () => {
      showPopp.value = false
      //do something
    }
    //确认的逻辑
    const confirm = () => {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>
复制代码

子组件二

跟子组件一基本一样,这里操作弹窗的逻辑是一模一样的,其实应该把逻辑提取到一个 hook 里,这里为了方便演示,就直接写了。

<template>
    <div>
      我是楼层二
    </div>
    
    <div class="popup"
       v-if="showPopp">
        <h3>我是弹窗二</h3>
        <div>
          <button @click='cancle'>取消</button>
          <button @click='confirm'>确定</button>
        </div>
    </div>
</template>
<script>
import { defineComponent, ref } from 'vue'


export default defineComponent({
  name: '',
  setup() {
    //控制弹窗显示
    const showPopp = ref(false)
    //取消的逻辑
    const cancle = () => {
      showPopp.value = false
      //do something
    }
    //确认的逻辑
    const confirm = () => {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>
复制代码

结果如下图

微信截图_20210707085736.png

步骤二

我们先不使用弹窗,我们通过定时器和 console.log 来模拟异步任务

父组件

//省略部分上文出现过的代码
setup() {
    .......
    //父组件要单独处理的异步任务
    const taskC = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('父组件的异步任务')
        }, 1000)
      })
    }
    
    onMounted(() => {
      taskC()
    })
    ......
  },
复制代码

子组件一

//省略部分上文出现过的代码
setup() {
    .......
    const taskA = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('子组件一的异步任务')
        }, 1000)
      })
    }
    onBeforeMount(() => {
      taskA()
    })
    ......
  },
复制代码

子组件二

//省略部分上文出现过的代码
setup() {
    .......
    const taskB = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('子组件二的异步任务')
        }, 1000)
      })
    }
    onBeforeMount(() => {
      taskB()
    })
    ......
},
复制代码

看一下结果,因为还没有构建任务队列,所有的异步任务都是同时进行的,所以同时打印出了三个组件的 log

16.gif

步骤三

使用 mitt.js 来收集异步任务

先把 mitt.js 封装成一个工具函数

//mitt.js
import mitt from 'mitt'
const emitter = mitt();

export default emitter;
复制代码

在子组件挂载之前触发 add-async-tasts 事件,通知父组件收集异步任务,在父组件监听 add-async-tasts 事件,将子组件的任务存入数组中。

父组件

//省略部分上文出现过的代码
setup() {
    .......
    // 声明一个空数组  用来存放所有的异步任务
    let asyncTasks = []
    
    //向数组中添加异步任务 收集所有的异步任务
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }
    
    // 监听add-async-tasts事件,当有异步任务触发的时候把异步任务添加到数组中
    emitter.on('add-async-tasts', addAsyncTasts)
    
    // 组件卸载的时候移除监听事件,数组重置为空
    onUnmounted(() => {
      emitter.off('add-async-tasts', addAsyncTasts)
      asyncTasks = []
    })
    .......
复制代码

子组件一

//省略部分上文出现过的代码
setup() {
    .......
    onBeforeMount(() => {
      //条件判断 if...  此处满足条件将通知父组件收集任务
      emitter.emit('add-async-tasts', taskA)
    })
    .......
复制代码

子组件二

//省略部分上文出现过的代码
setup() {
    .......
    onBeforeMount(() => {
      //条件判断 if...  此处满足条件将通知父组件收集任务
      emitter.emit('add-async-tasts', taskB)
    })
    .......
复制代码

看一下结果,我在父组件的收集函数中打了 log ,可以看见是触发了两次收集函数

1.gif

点开看一下,可以看到里面有两条数据,分别是 taskAtaskB 。说明我们的任务已经收集起来了。

image.png

步骤四

自定义任务顺序

这个我实现的方式是在收集任务的时候,多传入一个数字参数,最后再把任务队列按照数字大小排序。

父组件

//省略部分上文出现过的代码
setup() {
    .......
    //排序函数
    const compare = (property) => {
      return (a, b) => {
        let value1 = a[property]
        let value2 = b[property]
        return value1 - value2
      }
    }
    
    //向数组中添加异步任务 收集所有的异步任务
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      //根据order字段进行排序
      asyncTasks = asyncTasks.sort(compare('order'))
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }
    .......
复制代码

子组件一

//省略部分上文出现过的代码
setup() {
    .......
    onBeforeMount(() => {
      //条件判断 if...  此处满足条件将通知父组件收集任务
      emitter.emit('add-async-tasts', { fun: taskA, order: 1 })
    })
    .......
复制代码

子组件二

//省略部分上文出现过的代码
setup() {
    .......
    onBeforeMount(() => {
      //条件判断 if...  此处满足条件将通知父组件收集任务
      emitter.emit('add-async-tasts', { fun: taskB, order: 2 })
    })
    .......
复制代码

看一下结果,可以看到依然收集到了两个任务,并且按照order进行了排序

image.png

我们修改子组件一的 order3 ,再来验证一下结果是否正确

image.png

可以看到 taskA 排到了 taskB 的后面,说明我们的自定义异步任务的顺序也实现了。

步骤五

任务收集起来以后,接下里就是构建任务队列了

父组件

//省略部分上文出现过的代码
setup() {
    .......
    //实例被挂载后调用  为了保证收集完所有的任务,我们在onMounted周期中执行队列
    //mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 nextTick
    onMounted(() => {
      nextTick(() => {
        // 构建队列
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          //返回一个完成状态的promise,才可以继续链式调用
          return Promise.resolve()
        }

        // 执行队列
        queue(asyncTasks)
          .then((data) => {
            //所有子组件的任务完成后,进行父组件的任务
            //如果想先进行父组件的任务,可以把order定义为0存进任务队列
            return taskC()
          })
          .catch((e) => console.log(e))
      })
    })
 })
    .......
复制代码

看一下结果,可以看到所有的任务都按顺序进行了。

2.gif

步骤六

使用真实的弹窗场景修改代码

先来简单看一下 Promise , Promise 的使用不是本文的内容

Promise 对象的状态不受外界影响。

Promise 对象代表一个异步操作,有三种状态:

  • Pending(进行中)
  • Resolved(已完成,又称 Fulfilled
  • Rejected(已失败)

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

但是通过实践发现其实是可以在外部手动修改 Promise 的状态的

具体参考下面这篇文章👉 如何在Promise外部控制其状态

既然可以修改,那么我们就在子组件的按钮点击事件中,添加可以手动修改 Promise 状态的代码

//省略部分上文出现过的代码
setup() {
    .......
 //用来在外部改变promise的状态
    let fullfilledFn
    //异步任务
    const taskA = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //取消的逻辑
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //确认的逻辑
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
 })
    .......
复制代码

最后看一下结果

3.gif

全部代码

最后贴一下全部代码

父组件

<!--
 * @Description: 
 * @Date: 2021-06-23 09:48:13
 * @LastEditTime: 2021-07-07 10:34:04
 * @FilePath: \one\src\App.vue
-->
<template>
  <div>
    <h1>我是父组件!</h1>
    <child-one></child-one>
    <child-two></child-two>
  </div>
  <div class="popup"
       v-if="showPopp">
    <h1>我是父组件弹窗</h1>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, onUnmounted, nextTick, ref } from 'vue'

import ChildOne from './components/Child1.vue'
import ChildTwo from './components/Child2.vue'

import emitter from './mitt'

export default defineComponent({
  name: '',
  components: {
    ChildOne,
    ChildTwo,
  },
  setup() {
    //控制弹窗显示
    const showPopp = ref(false)
    //排序函数
    const compare = (property) => {
      return (a, b) => {
        let value1 = a[property]
        let value2 = b[property]
        return value1 - value2
      }
    }

    //组件要单独处理的异步任务
    const taskC = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          showPopp.value = true
          resolve()
        }, 1000)
      })
    }

    // 声明一个空数组  用来存放所有的异步任务
    let asyncTasks = []

    //向数组中添加异步任务 收集所有的异步任务
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      asyncTasks = asyncTasks.sort(compare('order'))
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }

    // 监听addAsyncTasts事件,当有异步任务触发的时候把异步任务添加到数组中
    emitter.on('add-async-tasts', addAsyncTasts)

    //实例被挂载后调用 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 nextTick
    onMounted(() => {
      nextTick(() => {
        // 构建队列
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          return Promise.resolve()
        }

        // 执行队列
        queue(asyncTasks)
          .then((data) => {
            return taskC()
          })
          .catch((e) => console.log(e))
      })
    })

    // 组件卸载的时候移除监听事件,数组重置为空
    onUnmounted(() => {
      emitter.off('add-async-tasts', addAsyncTasts)
      asyncTasks = []
    })

    return {
      showPopp,
    }
  },
})
</script>


复制代码

子组件一

<!--
 * @Description: 
 * @Date: 2021-06-23 09:48:13
 * @LastEditTime: 2021-07-07 10:32:52
 * @FilePath: \one\src\components\Child1.vue
-->
<template>
  <div>
    我是楼层一
  </div>

  <div class="popup"
       v-if="showPopp">
    <h3>我是弹窗一</h3>
    <div>
      <button @click='cancle'>取消</button>
      <button @click='confirm'>确定</button>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onBeforeMount, ref } from 'vue'
import emitter from '../mitt'

export default defineComponent({
  name: '',
  setup() {
    //控制弹窗显示
    const showPopp = ref(false)
    //用来在外部改变promise的状态
    let fullfilledFn
    //异步任务
    const taskA = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //取消的逻辑
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //确认的逻辑
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() => {
      //条件判断 if...
      emitter.emit('add-async-tasts', { fun: taskA, order: 1 })
    })
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


复制代码

子组件二

<!--
 * @Description: 
 * @Date: 2021-06-23 18:46:29
 * @LastEditTime: 2021-07-07 10:33:11
 * @FilePath: \one\src\components\Child2.vue
-->
<template>
  <div>
    我是楼层二
  </div>
  <div class="popup"
       v-if="showPopp">
    <h3>我是弹窗二</h3>
    <div>
      <button @click='cancle'>取消</button>
      <button @click='confirm'>确定</button>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onBeforeMount, ref } from 'vue'
import emitter from '../mitt'

export default defineComponent({
  name: '',
  setup() {
    //用来在外部改变promise的状态
    let fullfilledFn

    //控制弹窗显示
    const showPopp = ref(false)

    //异步任务
    const taskB = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //取消的逻辑
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //确认的逻辑
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() => {
      //条件判断 if...
      emitter.emit('add-async-tasts', { fun: taskB, order: 2 })
    })
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


复制代码

这个方案是第一次遇见这种需求,拍脑门想出来的,肯定不是最好的方案,但也算是一个招。希望各位大佬可以指点更好更简单的方案。

参考

如何在Promise外部控制其状态

分类:
前端
标签: