vue3-优雅拿捏弹窗优先级
需求描述
产品给我提了一个这样的需求:进入页面的时候,依次弹出新人引导、升级弹窗、权益弹窗以及其他。 弹窗的优先级后期可能改为后台配置,目前是前端写死。
初步方案
方案流程图
示意伪代码
<template>
<dialog1 v-if="isShowDialog1" @close="startDialog2"/>
<dialog2 v-if="isShowDialog2" @close="startDialog3"/>
<dialog3 v-if="isShowDialog3" @close="end"/>
</template>
<script lang="ts" setup>
import { dialog1, dialog2, dialog3 } from './components';
import { requestDialog1Data, requestDialog2Data, requestDialog3Data } from './service';
const isShowDialog1 = ref(false);
const isShowDialog2 = ref(false);
const isShowDialog3 = ref(false);
function startDialog1() {
const res = requestDialog1Data();
if (res.data) {
isShowDialog1.value = true
} else {
startDialog2();
}
}
function startDialog2() {
const res = requestDialog2Data();
if (res.data) {
isShowDialog2.value = true
} else {
startDialog3();
}
}
function startDialog3() {
const res = requestDialog3Data();
if (res.data) {
isShowDialog3.value = true
} else {
end();
}
}
function end() {
isShowDialog1.value = false;
isShowDialog2.value = false;
isShowDialog3.value = false;
}
startDialog1();
</script>
方案找茬
请求依赖
多个弹窗之间的请求没有依赖关系,但是为了实现优先级弹窗强制加入了依赖,性能低下。
代码冗余
三种弹窗之间本无联系,但为了实现优先级,不得不将显示代码写到一处,后续添加新弹窗或者修改优先级顺序,将比较困难。
无法配置优先级
当dialog2的优先级改为大于dialog1时,无法直接生效,需要改大量代码。
那么有没有什么方案可以解决这三个问题呢?
改良方案
方案描述
我们期待定义一个变量,在赋值为true后,自动等待其他优先级更高的变量赋值为false的时候延时赋值。如下伪代码所示。
// usePriorityRef为我们预想的方法名,它将返回一个Ref<boolean>类型的值
const dialogShow1 = usePriorityRef({
value: false,
priority: 1,
});
const dialogShow2 = usePriorityRef({
value: false,
priority: 2,
});
dialogShow1.value = true;
dialogShow2.value = true;
console.log(dialogShow2.value); // 预期打印为false,因为优先级更高的dialogShow1还没有设置为false
setTimeout(() => {
dialogShow1.value = false;
console.log(dialogShow2.value) // 预期打印为true
}, 1000) // 延时1s关闭弹窗的过程
业务伪代码进行如下改动:
<template>
- <dialog1 v-if="isShowDialog1" @close="startDialog2"/>
+ <dialog1 v-if="isShowDialog1" @close="isShowDialog1 = false"/>
- <dialog2 v-if="isShowDialog2" @close="startDialog3"/>
+ <dialog2 v-if="isShowDialog2" @close="isShowDialog2 = false"/>
- <dialog3 v-if="isShowDialog3" @close="end"/>
+ <dialog3 v-if="isShowDialog3" @close="isShowDialog3 = false"/>
</template>
<script lang="ts" setup>
import { dialog1, dialog2, dialog3 } from './components';
import { requestDialog1Data, requestDialog2Data, requestDialog3Data } from './service';
-const isShowDialog1 = ref(false);
+const isShowDialog1 = usePriorityRef({
+ value: false,
+ priority: 1,
+});
-const isShowDialog2 = ref(false);
+const isShowDialog2 = usePriorityRef({
+ value: false,
+ priority: 2,
+});
-const isShowDialog3 = ref(false);
+const isShowDialog3 = usePriorityRef({
+ value: false,
+ priority: 3,
+});
function startDialog1() {
const res = requestDialog1Data();
if (res.data) {
isShowDialog1.value = true
} else {
- startDialog2();
+ isShowDialog1.value = false
}
}
function startDialog2() {
const res = requestDialog2Data();
if (res.data) {
isShowDialog2.value = true
} else {
- startDialog3();
+ isShowDialog2.value = false
}
}
function startDialog3() {
const res = requestDialog3Data();
if (res.data) {
isShowDialog3.value = true
} else {
- end();
+ isShowDialog3.value = false
}
}
- function end() {
- isShowDialog1.value = false;
- isShowDialog2.value = false;
- isShowDialog3.value = false;
- }
startDialog1();
startDialog2();
startDialog3();
</script>
方案流程图
弹窗皆出现:
其中一个弹窗不出现
其中一个弹窗超时出现
完整的流程图
技术点拆解
vue3中如何定义一个响应式变量,赋值为x后,其值可以不等于x?
答案是computed。computed方法允许传入getter和setter方法,如下官网示例所示:
<script setup>
//官网示例
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({ // getter
get() {
return firstName.value + '-' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
现在当你再运行
fullName.value = 'John Doe'
时,setter 会被调用而firstName
和lastName
会随之更新。且fullName.value
最终为John-Doe
同理,定义两个ref,一个ref在setter方法里面使用,作为computed的接收值,另一ref在getter方法里面使用,作为computed属性的返回值
import { ref, computed, watch } from 'vue';
function usePriorityRef(task) {
const watchRef = ref(task.val);
const nextRef = ref(task.val);
const taskRef = computed({
get: () => nextRef.value,
set: (value) => {
watchRef.value = value;
},
});
return taskRef
}
<script setup>
const dialogShow1 = usePriorityRef({
value: false,
priority: 1,
});
const dialogShow2 = usePriorityRef({
value: false,
priority: 2,
});
dialogShow1.value = true;
dialogShow2.value = true;
console.log(dialogShow2.value); // 打印出false,符合效果
</script>
任务队列存储和排序
根据流程动图所示,链表非常适合这个需求。
链表初始化
对比于数组,链表不需要提前声明制定长度的空间,只需要定义一个头节点即可。
function getHeadTaskNode() {
// 定义头节点
const header = {
nextTask: null, // 定义下一个任务节点
status: 'before', // 定义总体任务的状态,
// before表示未开始、starting表示已开始、end表示已结束
}
return head;
}
按序添加任务节点
插入节点的算法示意图
function addTaskNode(task) {
let temp = header.next;
let pre = header;
if (!temp) {
header.next = task;
return task;
}
while (temp) {
const nextNodenextNode = temp.nextTask;
if (pre.priority < task.priority && temp.priority > task.priority ) {
pre.next = task;
task.next = temp;
break;
}
}
return task;
}
关键逻辑梳理
任务节点何时从'未开始'变为'等待出现'
当任务节点对应的computed属性的setter方法触发时,且为true
任务节点何时从'未开始'变为'已结束'
当任务节点对应的computed属性的setter方法触发时,且为false
任务节点何时从'未开始'变为'已超时'
当任务节点对应的computed属性的setter方法超过约定的超时时间还未触发
何时开始第一个任务
当header节点的下一个任务节点的状态为’等待出现‘时,使用vue的watch方法监听
所有任务结束后,需要做什么操作
每一个任务节点状态重置为’未开始‘,头部节点的状态重置为’未开始‘
任务可以从中间节点开始吗
可以,当头部节点的状态为’未开始‘,且中间节点的setter方法被触发,且为true,则从中间节点开始,适用于单独的状态管控。
成果
通过对上述技术方案的整合,我完成了一个npm js库(git仓库:github.com/blankzust/v…
使用方法如下所示:
Install
# with npm
npm i vue3-task-ref
# with yarn
yarn add vue3-task-ref
# with pnpm
pnpm add vue3-task-ref
Use
- 单组优先级
<template>
<div>
<button v-if="showDialog1" @click="showDialog1 = false">按钮1</button>
<button v-if="showDialog2" @click="showDialog2 = false">按钮2</button>
<button v-if="showDialog3" @click="showDialog3 = false">按钮3</button>
</div>
<template>
<script setup>
import { defaultTaskContainer } from 'vue3-task-ref';
const showDialog1 = defaultTaskContainer.taskRef({
val: false,
no: 1
});
const showDialog2 = defaultTaskContainer.taskRef({
val: false,
no: 2
});
const showDialog3 = defaultTaskContainer.taskRef({
val: false,
no: 3
});
// 用setTimeout模拟请求
setTimeout(() => {
showDialog1.value = true;
}, 100)
setTimeout(() => {
showDialog2.value = true;
}, 100)
setTimeout(() => {
showDialog3.value = true;
}, 100)
</script>
- 多组优先级
<template>
<div>
<button v-if="showDialog1" @click="showDialog1 = false">按钮1</button>
<button v-if="showDialog2" @click="showDialog2 = false">按钮2</button>
<button v-if="showDialog3" @click="showDialog3 = false">按钮3</button>
<button v-if="showDialog4" @click="showDialog4 = false">按钮4</button>
</div>
<template>
<script setup>
import { createTaskContainer } from 'vue3-task-ref';
const container1 = createTaskContainer();
const container2 = createTaskContainer();
const showDialog1 = container1.taskRef({
val: false,
no: 1
});
const showDialog2 = container1.taskRef({
val: false,
no: 2
});
const showDialog3 = container2.taskRef({
val: false,
no: 1
});
const showDialog4 = container2.taskRef({
val: false,
no: 2
});
// 用setTimeout模拟请求
setTimeout(() => {
showDialog1.value = true;
}, 100)
setTimeout(() => {
showDialog2.value = true;
}, 100)
setTimeout(() => {
showDialog3.value = true;
}, 100)
setTimeout(() => {
showDialog4.value = true;
}, 100)
</script>
- 自定义超时时间(默认为100ms)
<template>
<div>
<button v-if="showDialog1" @click="showDialog1 = false">按钮1</button>
<button v-if="showDialog2" @click="showDialog2 = false">按钮2</button>
<button v-if="showDialog3" @click="showDialog3 = false">按钮3</button>
</div>
<template>
<script setup>
import { defaultTaskContainer } from 'vue3-task-ref';
const showDialog1 = defaultTaskContainer.taskRef({
val: false,
no: 1,
});
const showDialog2 = defaultTaskContainer.taskRef({
val: false,
no: 2,
timeout: 1000
});
const showDialog3 = defaultTaskContainer.taskRef({
val: false,
no: 3
});
// 用setTimeout模拟请求
setTimeout(() => {
// 小于默认超时时间100毫秒返回,已超时
// 故按钮1不显示
showDialog1.value = true;
}, 200)
setTimeout(() => {
// 小于1000毫秒返回,未超时
showDialog2.value = true;
}, 800)
setTimeout(() => {
showDialog3.value = true;
}, 100)
</script>
小结
本篇文章,我们通过computed、ref、watch等vue3 api的使用以及链表的数据结构,完成了一个延时改变的ref封装,优雅拿捏住了弹窗优先级设计。不得不感叹,vue3的api设计可真是巧妙。我是blank,我们下篇文章见。
转载自:https://juejin.cn/post/7236295340630163514