likes
comments
collection
share

vue3-优雅拿捏弹窗优先级

作者站长头像
站长
· 阅读数 14

需求描述

产品给我提了一个这样的需求:进入页面的时候,依次弹出新人引导、升级弹窗、权益弹窗以及其他。 弹窗的优先级后期可能改为后台配置,目前是前端写死。

初步方案

方案流程图

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-优雅拿捏弹窗优先级

其中一个弹窗不出现

vue3-优雅拿捏弹窗优先级

其中一个弹窗超时出现

vue3-优雅拿捏弹窗优先级

完整的流程图

vue3-优雅拿捏弹窗优先级

技术点拆解

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;
}

按序添加任务节点

插入节点的算法示意图

vue3-优雅拿捏弹窗优先级

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

  • 单组优先级

vue3-优雅拿捏弹窗优先级

<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>
  • 多组优先级

vue3-优雅拿捏弹窗优先级

<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)

vue3-优雅拿捏弹窗优先级

<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,我们下篇文章见。