关于 Vue3
和 mitt.js
的使用方法我在另一篇文章中有介绍
需求介绍
最近公司有个需求,是一个移动端页面。一个页面包含多个楼层,每个楼层是一个单独的组件。每个组件内部有自己的逻辑。
页面是类似于个人中心的福利页面,每个楼层展示对应礼包的图片,用户进入页面以后,在满足条件的前提下,自动弹出领取礼包的弹窗。
控制每个礼包的弹窗显示隐藏的状态分别写在各自的组件中,现在的需求是
💡 每次只能展示一个弹窗
💡 无论点击确认还是取消,关闭上一个弹窗之后,自动打开第二个弹窗
💡 可以控制弹窗展示的顺序
解决方案
技术栈
- 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>
结果如下图
步骤二
我们先不使用弹窗,我们通过定时器和 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
步骤三
使用 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
,可以看见是触发了两次收集函数
点开看一下,可以看到里面有两条数据,分别是 taskA
和 taskB
。说明我们的任务已经收集起来了。
步骤四
自定义任务顺序
这个我实现的方式是在收集任务的时候,多传入一个数字参数,最后再把任务队列按照数字大小排序。
父组件
//省略部分上文出现过的代码
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进行了排序
我们修改子组件一的 order
为 3
,再来验证一下结果是否正确
可以看到 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))
})
})
})
.......
看一下结果,可以看到所有的任务都按顺序进行了。
步骤六
使用真实的弹窗场景修改代码
先来简单看一下 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()
}
})
.......
最后看一下结果
全部代码
最后贴一下全部代码
父组件
<!--
* @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>
这个方案是第一次遇见这种需求,拍脑门想出来的,肯定不是最好的方案,但也算是一个招。希望各位大佬可以指点更好更简单的方案。