2024年11月

与关系型数据库事务的区别

Redis事务是指将多条命令加入队列,一次批量执行多条命令,每条命令会按顺序执行,事务执行过程中不会被其他客户端发来的命令所打断。也就是说,Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务和关系型数据库的事务不太一样,它
不保证原子性,也没有隔离级别
的概念。

事务不保证原子性,但是Redis命令本身是原子性的

  1. Redis事务没有隔离级别的概念
    批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务里的查询要看到本事务的更新或其它事务的修改更新操作的问题。(Mysql里的事务的语句不是放入队列,而是直接执行)
  2. Redis不保证原子性
    Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的运行流程

Redis事务相关命令

  • Multi :开始事务
  • Exec :执行事务中的所有命令,即提交;
  • discard :放弃事务;和回滚不一样,Redis事务不支持回滚。
  • WATCH:监视Key改变,用于实现乐观锁。如果监视的Key的值改变,事务最终会执行失败。
  • UNWATCH:放弃监视。

没有隔离级别

当事务开启时,事务期间的命令并没有执行,而是加入队列,只有执行EXEC命令时,事务中的命令才会按照顺序执行,也就是说事务间就不会存在数据脏读、不可重复读、幻读的问题,因此就没有隔离级别。

事务不保证原子性

如上图所示,在通过EXEC执行事务时,其中命令执行失败不会影响到其他命令的执行,因此并没有保证同时成功和同时失败的原子操作,尽管这样,Redis事务中也
没有提供回滚
的支持

官方理由为:保证Redis的性能

  • 事实上如果使用Redis命令语法错误,或是将命令运用在错误的数据类型键上(如对字符串进行加减乘除等),从而导致业务数据有问题,这种情况认为是编程导致的错误,应该在开发过程中解决,避免在生产环境中发生;
  • 由于不用支持回滚功能,Redis内部简单化,而且还比较快;

多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。Redis之所以保持这样简易的事务,完全是为了保证高并发下的核心问题——性能。

语法错误(编译器错误)

在开启事务后,A的转出操作命令打成了
DECRBYa
,最终会导致事务提交失败,所有命令都不会执行,A、B保留原值。

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> DECRBYa A 500
(error) ERR unknown command 'DECRBYa', with args beginning with: 'A' '500'
127.0.0.1:6379(TX)> INCRBY B 500
QUEUED
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> mget A B
1) "1000"
2) "100"
127.0.0.1:6379>

类型错误(运行时错误)

在运行时检测类型错误,此时事务并没有回滚,而是跳过错误命令继续执行, 结果B值改变、A保留原值。

小结

  • 当事务中命令语法使用错误时,最终会导致事务执行不成功,即事务内所有命令都不执行;
  • 当事务中命令知识逻辑错误,就比如给字符串做加减乘除操作时,只能在执行过程中发现错误,这种事务执行中失败的命令不影响其他命令的执行。

使用WATCH实现乐观锁

WATCH通过监视指定Redis Key,如果没有改变,就执行成功,如果发现对应值发生改变,事务就会执行失败,如下图:

三种方式可以取消监视:

  • 事务执行之后,不管是否执行成功还好是失败,都会取消对应的监视;
  • 当监视的客户端断开连接时,也会取消监视;
  • 可以手动UNWATCH取消所有Key的监视;

面试题专栏

Java面试题专栏
已上线,欢迎访问。

  • 如果你不知道简历怎么写,简历项目不知道怎么包装;
  • 如果简历中有些内容你不知道该不该写上去;
  • 如果有些综合性问题你不知道怎么答;

那么可以私信我,我会尽我所能帮助你。

开心一刻

上午一好哥们微信我
哥们:哥们在干嘛,晚上出来吃饭
我:就我俩吗
哥们:对啊
我:那多没意思,我叫俩女的出来
哥们:好啊,哈哈哈
晚上吃完饭到家后,我给哥们发消息
我:今天吃的真开心,下次继续
哥们:开心尼玛呢!下次再叫你老婆和你女儿来,我特么踢死你

开心一刻

写在前面

正文开始之前了,我们先来正确审视下文章标题:
不依赖 Spring,你会如何自实现 RabbitMQ 消息的消费
,主要从两点来进行审视

  1. 不依赖 Spring

    作为一个
    Javaer
    ,关于
    Spring
    的重要性,我相信你们都非常清楚;回头看看你们开发的项目,是不是都是基于 Spring 的?如果不依赖 Spring,你们还能继续开发吗?不过话说回来,既然 Spring 能带来诸多便利,该用还得用,不要头铁,不要造低效轮子!


    如果能造出比 Spring 优秀的轮子,那你应该造!


    你们可能会说:不依赖 Spring 就不依赖嘛,我可以依赖
    Spring Boot
    噻;你们要是这么聊天,那就没法聊了


    这还咋聊

    Spring Boot 是不是基于 Spring 的?没有 Spring,Spring Boot 也是跑不起来的;
    不依赖 Spring
    的言外之意就是不依赖 Spring 生态,当然也包括 Spring Boot

    关于
    不依赖 Spring
    ,我就当你们审视清楚了哦

  2. 依赖 RabbitMQ Java Client

    与 RabbitMQ 服务端的交互,咱们就不要逞强去自实现了,老实点用官方提供的 Java Client 就好

    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.7.3</version>
    </dependency>
    

    注意 client 版本要与 RabbitMQ 版本兼容

所以文章标题就可以转换成

只依赖 RabbitMQ Java Client,不依赖 Spring,如何自实现 RabbitMQ 消息的消费

另外,我再带你们回顾下 RabbitMQ 的 Connection 和 Channel

  1. Connection

    Connection
    是客户端与 RabbitMQ 服务器之间的一个 TCP 连接,它是进行通信的基础,允许客户端发送命令到 RabbitMQ 服务器并接收响应;Connection 是比较重的资源,不能随意创建与关闭,一般会以

    的方式进行管理。每个 Connection 可以包含多个 Channel

  2. Channel

    Channel

    多路复用
    连接(Connection)中的一条独立的双向数据流通道。客户端与 RabbitMQ 服务端之间的大多数操作都是在 Channel 上进行的,而不是在 Connection 上直接进行。Channel 比 Connection 更轻量级,可以在同一连接中创建多个 Channel 以实现并发处理

    Channel 与 Consumer 之间的关系是一对多的,具体来说,一个 Channel 可以绑定多个 Consumer,但每个 Consumer 只能绑定到一个

自实现

我们采取主干到枝叶的实现方式,逐步实现并完善 RabbitMQ 消息的消费

主流程

依赖 RabbitMQ Java Client 来消费 RabbitMQ 的消息,代码实现非常简单,网上一搜一大把

/**
 * @author: 青石路
 */
public class RabbitTest1 {

    private static final String QUEUE_NAME = "qsl.queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = initConnectionFactory();
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));
        System.out.println(Thread.currentThread().getName() + " 线程执行完毕,消费者:" + consumerTag + "已经就绪");
    }

    public static ConnectionFactory initConnectionFactory() {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("10.5.108.226");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        factory.setVirtualHost("/");
        factory.setConnectionTimeout(30000);
        return factory;
    }

    static class QslConsumer extends DefaultConsumer {

        QslConsumer(Channel channel) {
            super(channel);
        }

        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, StandardCharsets.UTF_8);
            System.out.println(Thread.currentThread().getName()+ " 收到消息:" + message);
            this.getChannel().basicAck(envelope.getDeliveryTag(), false);
        }
    }
}

是不是很简单?

这里我得补充下,
exchange

queue
没有在代码中声明,绑定关系也没有声明,是为了简化代码,因为文章标题是
消费
;实际
exchange

queue

binding
这些已经存在,如下图所示

exchange_queue声明

上述代码,我相信你们都能看懂,主要强调下 2 点

  1. 消息是否自动 Ack

    对应代码

    channel.basicConsume(QUEUE_NAME, false, new QslConsumer(channel));
    

    basicConsume
    的第二个参数,其注释如下


    basicConsume_消息确认方式

    autoAck

    true
    表示消息在送达到 Consumer 后被 RabbitMQ 服务端确认,消息就会从队列中剔除了;
    autoAck

    false
    表示 Consumer 需要显式的向 RabbitMQ 服务端进行消息确认

    因为我们将
    autoAck
    设置成了
    true
    ,所以 main 线程存活的时间内,5 个消息被送达到 main 线程后就被 RabbitMQ 服务端确认了,也就从队列中删除了

  2. 手动确认

    如果 Consumer 的
    autoAck
    设置的是
    false
    ,那么需要显示的进行消息确认

    this.getChannel().basicAck(envelope.getDeliveryTag(), false);
    

    否则 RabbitMQ 服务端会将消息一直保留在队列中,反复投递

执行 main 方法,控制台输出如下

消费者就绪

我们去 RabbitMQ 控制台看下队列
qsl.queue
的消费者

RabbitMQ消费者

Consumer tag
值是:
amq.ctag-PxjqYiujeCvyYlgtvMz9EQ
,与控制台的输出一致;我们手动往队列中发送一条消息

发送消息

控制台输出如下

手动发送一条消息_控制台输出

自此,主流程就通了,此时已经实现 RabbitMQ 消息的消费

多消费者

单消费者肯定存在性能瓶颈,所以我们需要支持多消费者,并且是同个队列的多消费者;实现方式也很简单,只需要调整下 main 方法即可

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    QslConsumer qslConsumer = new QslConsumer(channel);
    String consumerTag1 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    String consumerTag2 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    String consumerTag3 = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
    System.out.println(Thread.currentThread().getName() + " 线程执行完毕,消费者["
            + Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已经就绪");
}

执行main 方法后 Channel 与 Consumer 关系如下

同个Channel_3个Consumer

此时是同个 Channel 绑定了 3 个不同的 Consumer;当然也可以一对一绑定,main 方法调整如下

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    Channel channel1 = connection.createChannel();
    Channel channel2 = connection.createChannel();
    Channel channel3 = connection.createChannel();
    QslConsumer qslConsumer1 = new QslConsumer(channel1);
    QslConsumer qslConsumer2 = new QslConsumer(channel2);
    QslConsumer qslConsumer3 = new QslConsumer(channel3);
    String consumerTag1 = channel1.basicConsume(QUEUE_NAME, false, qslConsumer1);
    String consumerTag2 = channel2.basicConsume(QUEUE_NAME, false, qslConsumer2);
    String consumerTag3 = channel3.basicConsume(QUEUE_NAME, false, qslConsumer3);
    System.out.println(Thread.currentThread().getName() + " 线程执行完毕,消费者["
            + Arrays.asList(consumerTag1, consumerTag2, consumerTag3) + "]已经就绪");
}

执行 main 方法后 Channel 与 Consumer 关系如下

3个Channel_3个Consumer

既然两种方式都可以实现多消费者,哪那种方式更好呢

Channel 与 Consumer 一对一绑定更好!

Channel 之间是线程安全的,同个 Channel 内非线程安全,所以同个 Channel 上同时处理多个消费者存在并发问题;另外 RabbitMQ 的消息确认机制是基于Channel 的,如果一个 Channel 上绑定多个消费者,那么消息确认会变得复杂,非常容易导致消息重复消费或丢失

也许你们会觉得
一对一
的绑定相较于
一对多
的绑定,存在资源浪费问题;确实是有这个问题,但我们要知道,Channel 是 Connection 中的一条独立的双向数据流通道,非常轻量级,相较于并发带来的一系列问题而言,这点小小的资源浪费可以忽略不计了

消费者数量能不能配置化呢,当然可以,调整非常简单

private static final int concurrency = 3;

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    for (int i = 0; i < concurrency; i++) {
        Channel channel = connection.createChannel();
        QslConsumer qslConsumer = new QslConsumer(channel);
        String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
        System.out.println("消费者:" + consumerTag + " 已经就绪");
    }
}

concurrency
的值是从数据库读取,还是从配置文件中获取,就可以发挥你们的想象呢;如果依赖 Spring 的话,往往会用配置文件的方式注入进来

消费者预取数

队列
qsl.queue
没有消费者的情况下,我们往队列中添加 5 条消息:我是消息1 ~ 我是消息5,然后调整下
handleDelivery

@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    String message = new String(body, StandardCharsets.UTF_8);
    System.out.println(consumerTag + " 收到消息:" + message);
    this.getChannel().basicAck(envelope.getDeliveryTag(), false);
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

最后执行 main,控制台输出如下

5条消息同个消费者消费

大家注意看框住的那部分,5 条消息被同一个消费者给消费了!5 条消息为什么不是负载均衡到 3 个消费者呢?这是因为消费者的
prefetch count
(即
预取数
)没有设置

prefetch count 是消费者在接收消息时,告诉 RabbitMQ 一次最多可以发送多少条消息给该消费者。默认情况下,这个值是 0,这意味着 RabbitMQ 会尽可能快地将消息分发给消费者,而不考虑消费者当前的处理能力

再回过头去看控制台的输出,是不是就能理解了?一旦某个消费者就绪,队列中的 5 条消息全部推给它了,后面就绪的 2 个消费者就没有消息可消费了;所以我们需要配置
prefetch count
以实现负载均衡,调整很简单

private static final String QUEUE_NAME = "qsl.queue";
private static final int concurrency = 3;
private static final int prefetchCount = 1;

public static void main(String[] args) throws Exception {
    ConnectionFactory factory = initConnectionFactory();
    Connection connection = factory.newConnection();
    for (int i = 0; i < concurrency; i++) {
        Channel channel = connection.createChannel();
        channel.basicQos(prefetchCount);
        QslConsumer qslConsumer = new QslConsumer(channel);
        String consumerTag = channel.basicConsume(QUEUE_NAME, false, qslConsumer);
        System.out.println("消费者:" + consumerTag + " 已经就绪");
    }
}

然后重复如上的测试,控制台输出如下

负载均衡消费

是不是实现了我们想要的
负载均衡

prefetch count 的设置需要根据实际的业务需求和消费者的处理能力进行调整;如果设置得太高,可能会导致内存占用过多;如果设置得太低,则可能无法充分利用消费者的处理能力

其他完善

限于篇幅,我就只列举几个还待完善的点

  1. 目前只支持单个队列,需要支持多个队列
  2. 目录消费逻辑单一固定,需要支持动态指定逻辑,不同的队列对应不同的消费逻辑
  3. 消费者支持停止和重启
  4. ...

关于这些点,我们下篇不见不散

总结

  1. Connection、Channel、Consumer 之间的关系需要理清楚

    Connection 是 TCP 连接;Channel 是 Connection 中的双向数据流通道;Channel 可以绑定多个 Consumer,但推荐一个 Channel 只绑定一个 Consumer

    IO 多路复用
    是网络编程中常用的技术,建议大家掌握

  2. 基于 RabbitMQ Java Client 提供的 API,实现了消息消费、多消费者以及负载均衡

    没有 Spring,我们照样可以很优雅的消费 RabbitMQ 的消息

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Parameter Competition Balancing for Model Merging

创新点


  • 重新审视了现有的模型合并方法,强调参数竞争意识的关键作用。
  • 提出了一种名为
    \({\tt Pcb-Merging}\)
    的新方法,通过平衡参数竞争有效地调整参数系数。
  • 提出的方法在各种应用场景中稳定并提升了模型合并性能,无需额外训练。

内容概述


尽管对预训练模型进行微调已成为一种常见做法,但这些模型在其特定领域之外往往表现不佳。最近模型融合技术使得能够将多个经过不同任务微调的模型直接集成到一个模型中,集成模型具备多任务能力而无需在原始数据集上进行重新训练。然而,现有方法在解决任务之间潜在冲突和复杂相关性方面存在不足,特别是在参数级别调整中,造成在不同任务间有效平衡参数竞争的挑战。

论文提出了轻量级且无需训练的创新模型融合技术
\({\tt Pcb-Merging}\)

Parameter Competition Balancing
),通过调整每个参数的系数实现有效的模型融合。
\({\tt Pcb-Merging}\)
采用内部平衡来评估各个任务中参数的重要性,并采用外部平衡来评估不同任务间的参数相似性。重要性分数较低的参数被舍弃,其余参数被重新缩放,以形成最终的融合模型。

论文在多种融合场景中评估了该方法,包括跨任务、跨领域和跨训练配置,以及领域外泛化。实验结果表明,该方法在多个模态、领域、模型大小、任务数量、微调形式以及大型语言模型中实现了显著的性能提升,超越了现有的模型融合方法。

PCB-Merging


最近的模型合并研究基于任务向量完成各种任务算术操作和模型合并。对于任务
\(T_i\)
,任务向量
\(\tau_{i} \in \mathbb{R}^\textrm{d}\)
定义为通过从微调权重
\(\theta_\textrm{i}\)
中减去预训练权重
\(\theta_\textrm{pre}\)
所得到的向量,即
\(\tau_{i} = \theta_\textrm{i} - \theta_\textrm{pre}\)
,用于专注每个任务特定模型微调阶段发生的变化。基于任务向量的多任务模型合并方法可以表达为
\(\theta_m = \theta_\textrm{pre} + \lambda * \sum_{i=1}^{n}\tau_i\)
,其中系数
\(\lambda\)
表示合并任务向量
\(\tau_m\)
的重要性。这个概念简单而有效,显著优于简单的权重平均方案,即
\(\theta_m = (1/N)\sum_{i=1}^{n}\theta_i\)

平衡参数竞争

PCB-Merging
旨在调节每个任务和参数的缩放因子,实现任务内部和平衡任务之间的相互平衡。具体而言,使用参数竞争平衡(
PCB
)矩阵
\(\beta_i \in \mathbb{R}^{d}\)
来调整每个任务模型
\(\theta_i \in \mathbb{R}^{d}\)
中参数的规模,从而得到最终的融合模型,具体如下:

  • Intra-Balancing

首先,通过对任务向量的幅度应用非线性激活函数(即
softmax
)来实现
self-awareness
,强调重要参数,同时在一定程度上抑制冗余参数。

随着融合任务数量的增加,参数之间的竞争加剧。因此,使用任务数量
\(N\)
来调节冗余参数的抑制程度。

\[\begin{equation}
\beta_{intra, i} = \text{Softmax}(N*\text{Norm}({\tau}_i \odot{\tau}_i))
\end{equation}
\]

  • Inter-Balancing

接下来,使用
cross-awareness
来使一组任务内的参数能够与其他参数互动,从而解决任务之间潜在的冲突和复杂的相关性。

为了实现这一目标,计算不同任务向量中相同位置参数之间的相似度,使得每个参数能够基于来自其他任务的信息更新其分数。计算过程如下:

\[\begin{equation}
\beta_{inter, i} = \sum\nolimits_{j=1}^{n} \text{Softmax}(\text{Norm}({\tau}_i \odot{\tau}_j))
\end{equation}
\]

  • Drop and Rescale

得到
\(\beta_{i} = \beta_{intra, i} \odot \beta_{inter, i}\)
后,基于
\(\beta_i\)
构建一个掩码
\(m_i \in \mathbb{R}^{d}\)
以关注更重要的参数。具体而言,这个掩码
\(m_i\)
用于从
\(\beta_i\)

\(D\)
个元素中选择高分数元素。

定义掩码比例为
\(r\)
,其中
\(0 < r \leq 1\)
。掩码
\(m_i\)
可以通过以下公式推导得出:

\[\begin{equation}
m_{i, d} = \begin{cases}
1,& \text{if } \beta_{i, d} \geq\text{sorted}(\beta_i)[(1-r) \times D] \\
0,& \text{otherwise}
\end{cases}
\end{equation}
\]

重要性分数定义为
\(\hat{\beta} = m_i \odot \beta_i\)
,使用掩码平衡矩阵的分数来加权每个任务向量中每个参数的重要性,得到最终合并的任务向量
\(\tau_m\)

\[\begin{equation}
\tau_m = \sum\nolimits_{i=1}^{n}(\hat{\beta}_i \odot{\tau}_i) / \sum\nolimits_{i=1}^{n}\hat{\beta}_i
\end{equation}
\]

最终合并的任务向量
\(\tau_m\)
可以进一步按比例调整其幅度,并将其与初始参数值结合以生成融合后的模型参数
\(\theta_m\)
,表示为
\(\theta_m = \theta_\textrm{pre} + \lambda * \tau_m\)
,其中
\(\lambda\)
是一个缩放超参数。

系数搜索

先前的研究表明,基于任务向量的模型合并方法对合并系数
\(\lambda\)
非常敏感。即便选取了合适的统一
\(\lambda\)
,要进一步提高融合性能仍然需要对每个任务向量进行合并系数的网格搜索。这个过程复杂且繁琐,特别是在处理大量任务时。

论文采用智能优化算法来搜索混合系数,旨在比使用统一系数获得更大的改进。这一优化过程旨在寻找最佳的集合
\(\{\lambda_1, \cdots, \lambda_n\}\)
,以增强验证准确性,最终目标是最大化合并模型的验证准确性。

\[\begin{equation}
\theta_m = \theta_\textrm{pre} + \sum\nolimits_{i=1}^{n}(\hat{\beta}_i \odot \lambda_i {\tau}_i) / \sum\nolimits_{i=1}^{n}\hat{\beta}_i
\end{equation}
\]

在大多数实验设置中,主要使用协方差矩阵自适应进化策略(
CMA-ES
)。作为一种基于概率的种群优化算法,
CMA-ES
动态调整由协方差矩阵定义的搜索分布。它在每次迭代中系统地更新该分布的均值和协方差,以学习并利用搜索空间的潜在结构,从而提高优化效率。

主要结果




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

粘贴控件是一种特殊的系统安全控件,它允许应用在用户的授权下无提示地读取剪贴板数据。

在应用集成粘贴控件后,用户点击该控件,应用读取剪贴板数据时不会弹窗提示。可以用于任何应用需要读取剪贴板的场景,避免弹窗提示对用户造成干扰。

例如,用户在应用外(如短信)复制了验证码,要在应用内粘贴验证码。用户原来在进入应用后,还需要长按输入框、在弹出的选项中点击粘贴,才能完成输入。而使用粘贴控件,用户只需进入应用后直接点击粘贴按钮,即可一步到位。

粘贴控件效果如图所示。
img

约束与限制

  • 临时授权会持续到灭屏、应用切后台、应用退出情况发生。
  • 应用在授权期间没有调用次数限制。
  • 为了保障用户的隐私不被恶意应用获取,应用需确保安全控件是可见的且用户能够识别的。开发者需要合理的配置控件的尺寸、颜色等属性,避免视觉混淆的情况,如果发生因控件的样式不合法导致授权失败的情况,请检查设备错误日志。

开发步骤

以简化用户填写验证码为例,参考以下步骤,实现效果:点击控件获取临时授权,粘贴内容到文本框,效果图请见上文。

  1. 导入剪贴板依赖。
import { pasteboard } from '@kit.BasicServicesKit';
  1. 添加输入框和粘贴控件。

    粘贴控件是由图标、文本、背景组成的类似Button的按钮,其中图标、文本两者至少有其一,背景必选。图标和文本不支持自定义,仅支持在已有的选项中选择。

    应用申明安全控件的接口时,分为传参和不传参两种,不传参默认创建图标+文字+背景的按钮,传参根据传入的参数创建,不包含没有配置的元素。

    当前示例使用默认参数。具体请参见PasteButton控件。此外,所有安全控件都继承安全控件通用属性,可用于定制样式。

import { pasteboard, BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct Index {
  @State message: string = '';

  build() {
    Row() {
      Column({ space: 10 }) {
        TextInput({ placeholder: '请输入验证码', text: this.message })
        PasteButton()
          .padding({top: 12, bottom: 12, left: 24, right: 24})
          .onClick((event: ClickEvent, result: PasteButtonOnClickResult) => {
            if (PasteButtonOnClickResult.SUCCESS === result) {
              pasteboard.getSystemPasteboard().getData((err: BusinessError, pasteData: pasteboard.PasteData) => {
                if (err) {
                  console.error(`Failed to get paste data. Code is ${err.code}, message is ${err.message}`);
                  return;
                }
                // 剪贴板内容为 '123456'
                this.message = pasteData.getPrimaryText();
              });
            }
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台)

https://cli.vuejs.org/zh/

使用脚手架

  1. 安装脚手架

    npm install -g @vue/cli
    
  2. 使用脚手架创建一个项目

    vue create vue-demo
    
  3. 进入目录启动服务

    npm run serve
    

脚手架结构

├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件
├── vue.config.js:脚手架配置文件
// 默认的main.js
// 整个项目的入口文件

// 引入vue
import Vue from 'vue'
// 引入App组件,所有组件的父组件
import App from './App.vue'

// 关闭vue的生产提示
Vue.config.productionTip = false

// 创建vue实例对象 vm
new Vue({
  // 将App组件放入容器中
  render: h => h(App),
}).$mount('#app') // 挂载app容器

<!-- 默认index文件-->

<!DOCTYPE html>
<html lang="">
<head>
    <meta charset="utf-8">
    <!--      针对ie浏览器的配置,让ie浏览器以最高级别渲染页面-->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!--      开启移动端的理想视口-->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!--   配置页签图标   -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!--    配置网页标题-->
    <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- 浏览器不支持js 的时候, noscript中的元素会被渲染-->
<noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
</noscript>
<!-- 容器-->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

render函数

在main.js文件中,
import Vue from 'vue’
,此处导入的vue不是完整的,缺少模板解析器,所以不能使用template属性去设置组件标签

, vue代码完成之后是要打包的, 其中少不了vue的核心代码,当我们打包好之后, 我们其实是不需要再来解析模板的, 那么, vue的核心代码中的模板解析器根本用不着, 所以,vue为了使代码的体积减少, 就去掉了模板解析器,但是我们开发的时候, 又要使用, 所以就创建了一个 render 方法来解析模板,总之, 它的目的就是让打包后的代码体积尽量小,提升性能

关于不同版本的Vue:

  1. vue.js与vue.runtime.xxx.js的区别:
    (1).vue.js是完整版的Vue,包含:核心功能+模板解析器。
    (2).vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。

  2. 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项

    需要使用render函数接收到的createElement函数去指定具体内容

render
函数主要用于创建虚拟 DOM 节点(VNode)。它是一个 JavaScript 函数,接收一个名为
createElement
(通常缩写为
h
)的函数作为参数。这个
createElement
函数用于构建 VNode

    render(createElement){
        // 创建一个节点  <h1>hello world</h1> 并在页面渲染
        return createElement("h1","hello world") 

    }
// 将App组件传入,将APP中的template模版交给render渲染
// 简写的箭头函数
render: h => h(App),

脚手架默认配置

脚手架默认隐藏了webpack的相关配置,使用下面命令将相关配置输出为一份js文件供查看(仅查看)

vue inspect > output.js

默认情况下,public文件夹下的index、favicon ,src下的App.vue和main.js不允许修改,vue默认是从该路径找对应的文件

可修改的配置
,可以修改脚手架规则,进行个性化定制

ref属性

被用来给元素或子组件注册引用信息(id的替代者)

应用在html标签上获取的是真实DOM元素、组件标签上是组件实例对象(vc)

<template>
  <div id="app">

    <!--   设置标题DOM的ref是title-->
    <h1 ref="title">标题</h1>
    <button @click="showDOm">点击获取标题的DOM元素</button>
    <!--   设置组件ref是studen-->

    <StudentComp ref="student"></StudentComp>
  </div>
</template>

<script>
import StudentComp from "./components/StudentComp.vue";

export default {
  name: 'App',
  components: {
    StudentComp
  },
  data() {
    return {name: "app->vue", address: "Beijing"}
  },
  methods: {
    showDOm() {
      // 这里的this是vc

      //模版标签里ref="title", 在vc上面的结构是 在vc的$refs对象里面有title的key,value就是对应的DOM
      console.log(this.$refs.title)
      // 在组件标签上面使用ref,这里获取到的是对应组件的实例对象
      console.log(this.$refs.student)
    }
  }
}
</script>

props配置

可以让组件接收外部传过来的数据,动态传参

  1. 只接受参数

    <script>
     export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
       // 数组形式->使用props选项,配置city和age参数
      props:["city","age",]
    
    }
    
    
    </script>
    
    <template>
      <div class="student">
        <h1>名字:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        {{city}}
        <!-- 接受到age之后加1,age默认是字符串,所以不会正常计算,只会字符串拼接 --> 
        {{age + 1}} 
    
      </div>
    </template>
    

     <!-- 在使用组件标签的时候,传入对应的数据 -->
    <StudentComp city="beijing" age="18"></StudentComp>
    <!--  如果要传入的是个表达式 ,用数据绑定语法  :age="xxxxx"-->
    
  2. 接收参数-限制类型

    <script>
    export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
    // 对象形式
      props: {
        city: String, // 定义city参数,字符串类型
        age: Number // 定义age参数,Number类型
      }
    }
    
    
    </script>
    
    <template>
      <div class="student">
        <h1>名字:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        {{ city }}
        <!--     age限制是Number类型,可以正常计算-->
        {{ age + 1 }}
    
      </div>
    </template>
    
  3. 接收参数-类型限制-必填限制-默认值设置

    // 对象嵌套对象 
    props:{
        city:{
          type:String, // 字符串类型
          required:true // 必填项,如果不传递,默认是false
        },
        age:{
          type:Number,// number类型
          required: false,// 非必填
          default:18 // 默认值,如果不传参,走默认项
        }
      }
    
  4. props是只读的


    props是只读的,模版标签中传递什么就是什么,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据


    // props的渲染优先级比data高
    // 页面使用myCity进行渲染,myCity的初始值是props的city,从模版标签传入
    // 修改myCity即修改页面效果
    data: function () {
        return {name: "vue", address: "Beijing",myCity:this.city}
      }
    
  5. 通过props传递数据

<template>
  <div>

    <Student  :demo="demo"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  }
}
<script>
 export default {
   // eslint-disable-next-line vue/multi-word-component-names
   name: "Student",
   // 定义props属性
   props:["demo"],
  data: function () {
    return {name: "vue", address: "Beijing"}
  },
  methods: {
    sendStudentName(){
      this.demo(this.name)
    }
  }
}


</script>

mixin混入

可以把多个组件共用的配置提取成一个混入对象

  1. 第一步:定义混合

    // mixin.js文件 
    export const mixin = {
        // 回调
        methods:{
            showName(){
                alert(this.name)
            }
        }
    
    }
    
  2. 第二步:组件配置

    <script>
    
    // 导入mixin
    import {mixin} from "@/mixin";
    
    export default {
      name: "StudentComp",
      data: function () {
        return {name: "vue", address: "Beijing"}
      },
      // 配置混合(局部混入),导入的mixin
      // 如果有多个 ,数组可以配置多个
      mixins:[mixin]
    
    }
    
    
    </script>
    

    Vue.mixin(mixin) // 全局混入
    
  3. 第三部:正常使用

        <button @click="showName">按钮</button>
    
  4. 一个mixin对象可以报警data、methods、computed等各种组件选项

    理论上可以组件配置所有选项都可以使用,template和el可能会存在一定限制和合并问题

    export const mixin = {
        // 回调
        methods: {
            showName() {
                alert(this.name)
            }
        },
        mounted() {
            console.log("test")
        },
        data() {
            // 如果混合和组件内的data使用了同样的属性,以组件内部为主,如果不相同,两个会组合起来
            return {baseUrl: "xxxx"}
        },
        // 计算
        computed:{
            
        },
        
    }
    

插件

  1. 功能:用于增强Vue

  2. 本质:包含install方法的一个对象,install的第一个参数是Vue,第二个以后的参数是插件使用者传递的数据

  1. 定义插件

    export const TestPlugins  = {
      // 如果需要自定义参数,在Vue后传参即可
        install(Vue){
          // 可以自定义一些方法
            // 过滤器
            Vue.filter()
            // 混入
            Vue.mixin()
    
            // 在Vue原型上添加方法(vm和vc都可以用)
            Vue.property.say = ()=>{}
        }
    }
    
  2. 使用插件

    import {TestPlugins} from "@/plugins";
    
    Vue.use(TestPlugins)
    

scoped样式

多个不同的组件编写的样式,选择器最终是汇总在一起的,有可能出去类名、ID名字冲突,scoped可以让样式在局部生效,防止冲突,原理是在对应的元素上加上一个 data-v-xxxxx,唯一标识

<style scoped ></style>

组件化编码流程

  1. 实现静态组件:抽取组件,使用组件实现静态页面效果
    • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
  2. 展示动态数据:数据类型、名称、保存在哪个组件等等
    • 考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用
    • 一个组件在用:放在组件自身即可
    • 一些组件在用:放在他们共同的父组件上(状态提升)
  3. 交互:绑定事件监听等等
  4. props适用于:
    • 父组件 ==> 子组件 通信
    • 子组件 ==> 父组件 通信(要求父先给子一个函数)

浏览器本地存储

存储储内容大小一般支持5MB左右(不同浏览器可能还不一样)

  1. LocalStorage


    LocalStorage存储的内容,需要手动清除才会消失,关闭浏览器不会消失


        // 本地存储-> 存储到Local Storage
        // key和value都是字符串,若value不是字符串,会默认调toString方法
        // 如果key已经存在,更新数据
        localStorage.setItem("name","vue")
    
        // 读取数据,如果没有该key,读取的是null
        localStorage.getItem("name")
    
        // 删除数据
        localStorage.removeItem("name")
    
        // 清空数据-所有的数据
        localStorage.clear()
    
  2. sessionStorage


    SessionStorage存储的内容会随着浏览器窗口关闭而消失


        // 本地存储-> 存储到Session Storage
         sessionStorage.setItem("name","vue")
    
        // 读取数据,如果没有该key,读取的是null
        sessionStorage.getItem("name")
    
        // 删除数据
        sessionStorage.removeItem("name")
    
    
        // 清空数据-所有的数据
        sessionStorage.clear()
    

组件的自定义事件

在 Vue 中,组件自定义事件是一种用于组件间通信的重要机制。它允许子组件向父组件传递信息。当子组件发生某些操作或者状态变化时,可以通过触发自定义事件来通知父组件

例如,在一个包含表单组件(子组件)和显示数据组件(父组件)的应用中,当用户在表单组件中提交数据后,表单组件可以通过自定义事件将数据传递给父组件,以便父组件进行后续处理

通过自定义事件传递数据
  1. 定义自定义事件-第一种方式
<template>

  <div>
    <!--     给Student这个组件实例对象Vc绑定了一个事件-->
    <!--    给谁绑定了事件,就找谁触发事件-->
    <!--     v-on:自定义事件名="对应方法 "-->
    <!-- 若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法 -->    
    <!--  可以简写为@testDemo="demo" -->
    <Student v-on:testDemo="demo"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  }
}
</script>
  1. 定义自定义事件-第二种方式
<template>
  <div>
<!-- 给组件设置ref属性 -->
    <Student  ref="student"></Student>
  </div>
</template>

<script>


import Student from "@/components/Student.vue";

export default {
  name: 'App',
  components: {
    Student
  },
  methods: {
    demo(name) {
      console.log("子组件data name", name)
    }
  },
  // 当组件加载完毕之后
  mounted() {
    // student组件实例对象,绑定testDemo事件,触发Demo回调
    // 更加灵活,可以做一些前后置操作再绑定事件
    // 该方式回调要么配置在methods中,要么用箭头函数,写普通函数中的this是要绑定的组件实例对象
    this.$refs.student.$on("testDemo",this.demo)
  }
}
</script>
  1. 触发自定义事件
<script>
 export default {
   name: "Student",
  data: function () {
    return {name: "vue", address: "Beijing"}
  },
  methods: {
    sendStudentName(){
      //  通过$emit方法触发事件,传入自定义事件的名字
      //  触发testDemo事件
      this.$emit("testDemo",this.name)
    }
  }
}


</script>

<template>
   <div class="student">
    <h1>名字:{{ name }}</h1>
    <h1>地址:{{ address }}</h1>
    <button @click="sendStudentName">按钮</button>

  </div>


</template>
解绑自定义事件
// 在要解绑的组件上调用解绑指定组件   
this.$off("testDemo")
// 解绑多个指定事件
this.$off(["testDemo","xxxx"])
// 所有的自定义事件都解绑
this.$off()
native

如果在组件标签上绑定原生的事件,组件标签也会当成自定义事件去寻找自定义的方法

<!-- 使用native修饰符,告诉组件这个是原生事件 -->
<Student  ref="student" @click.native="show"></Student>

全局事件总线

一种组件间通信的方式,用于任意组件间通信,在应用程序中实现事件通信的机制,它允许不同的组件之间进行解耦的通信。可以把它想象成一个消息中心,各个组件或模块可以向这个中心发布(触发)事件,也可以从这个中心订阅(监听)事件。当一个事件被发布时,所有订阅了该事件的组件或模块都会收到通知并可以执行相应的操作

创建全局事件总线

VueComponent.prototype.
__proto__
=== Vue.prototype

$on、$off、$emit都是Vue原型上的方法,组件实例对象(vc)可以访问到Vue原型上的属性、方法

可以通过创建一个新的 Vue 实例来作为全局事件总线。通常在项目的入口文件中进行设置

// 入口文件
new Vue({
    render: h => h(App),
    // 生命周期函数:beforeCreate 组件实例刚被创建时调用
    beforeCreate() {

        // 在vue的原型上添加一个$bus属性,值是当前vue实例 

         Vue.prototype.$bus = this
    }
}).$mount('#app')
使用全局事件总线

接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身

// 子组件A
// 组件挂载后绑定事件
mounted() {
    // 在bus(vue)上绑定一个test事件
    this.$bus.$on("test",(data)=>{
      console.log("收到数据",data)
    })
  },
 // 组件销毁之前解绑
    beforeDestroy(){
      this.$bus.$off("test")
    }
    
触发全局事件总线
// 子组件B
sendData(){
      // 触发bus(vue)的test事件
      this.$bus.$emit("test","data数据")

    }

消息订阅与发布

PubSub 的概念
  • PubSub 是 Publish - Subscribe(发布 - 订阅)的缩写,它本质上是一种消息传递模式。在这种模式下,有发布者(Publishers)和订阅者(Subscribers)两类角色。发布者负责产生消息并将其发送到一个消息中心(也称为消息代理,Message Broker),订阅者则向消息中心表达自己对某些消息类型的兴趣,当消息中心收到发布者发送的匹配订阅者兴趣的消息时,就会将消息转发给订阅者

  • 一种组件间通信的方式,适用于任意组件间通信

  • 有一些专门的 PubSub 库可以帮助实现消息的发布和订阅。例如pubsu-js库

使用pubsub
  1. 安装pubsu-js库

    npm install -g pubsub-js       
    
  2. 引入pubsub

    import pubsub from 'pubsub-js'
    
  3. 订阅消息


    A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身


    // 组件挂载完之后订阅消息
    mounted() {
        // 参数:主题、回调函数(主题名、数据)
        // 订阅了testTopic主题的消息,如果有人给testTopic主题发消息,会接受到
        // pubsub.subscribe会返回一个订阅id
        this.pid = pubsub.subscribe("testTopic",function (msgName,data){
          // 此时this是undefined,因为使用的三方库
          // 可以使用箭头函数,this是当前vc 或者配置回调函数在这里调用
          console.log(data)
    
        })
      },
        // 组件销毁前
      beforeDestroy(){
        // 取消订阅(订阅id)
        pubsub.unsubscribe(this.pid)
      }
    
  4. 发布消息

        // 给订阅testTopic主题的发送消息,发送消息内容是helloworld
        pubsub.publish("testTopic","helloworld")
    

$nextTick

$nextTick
是 Vue.js 提供的一个实例方法,它的主要作用是在下次 DOM 更新循环结束之后执行延迟回调。在 Vue 中,数据的变化到 DOM 的更新是异步的,当数据发生改变时,Vue 会开启一个异步更新队列,将同一事件循环中的所有数据变化引起的 DOM 更新操作合并到一个更新任务中,等本轮事件循环结束后,再一次性执行 DOM 更新。
$nextTick
就是用于在这个 DOM 更新完成后执行一些操作

     edit(todo){
       if(todo.hasOwnProperty('isEdit')){
        todo.isEdit = true
      }else {
        this.$set(todo,'isEdit',true)
      }
       this.$refs.inputTitle.focus()
     }
// 问题示例:我们想修改一个状态,状态是true的时候输入框可以获取获取焦点
// 问题在于,todo.isEdit = true修改后,vue不是立马渲染的,而是等代码都执行完才渲染
// 相当于this.$refs.inputTitle.focus() 执行完 再去渲染,
// 执行的时候页面还没有渲染,是false,从而获取焦点失败
// 当页面渲染完后,再去获取焦点
this.$nextTick(function (){
        this.$refs.inputTitle.focus()
      })
  1. 作用:在下一次 DOM 更新结束后执行其指定的回调。

  2. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行

过渡与动画

在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名

image-20241121135250739

动画默认名称效果

vue会根据规则自动实现动画效果,适用一个动画

<template>
  <div>
    <button @click="isShow = !isShow"> 显示/隐藏</button>
    <!--   让谁有动画效果,就用transition 把谁包起来 -->
    <!--   appear属性用于控制元素在初始渲染时是否应用过渡效果 不加则初始渲染不应用-->
    <!-- 只适用于包裹单个元素 -->
    <transition appear>
      <h1 v-show="isShow">hello world</h1>

    </transition>

  </div>
</template>

<style scoped>
h1 {
  background-color: orange;
}

/* v-enter-active主要用于定义元素进入(插入)时过渡动画的行为 */
.v-enter-active {
  animation: test 1s;
}

/* v-leave-active用于定义元素离开(移除)时过渡动画的行为 */
.v-leave-active {
  animation: test 1s reverse;
}

/* 定义动画关键帧 */
@keyframes test {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0px);
  }

}
</style>
动画自定义名称

设置自定义名称,可以指定多个不同的效果

<!-- 指定name名称-->
<transition appear name="hello">
      <h1 v-show="isShow">hello world</h1>
 
    </transition>
/* 使用指定name */
.hello-enter-active {
  animation: test 1s;
}

/* 使用指定name */
.hello-leave-active {
  animation: test 1s reverse;
}
过渡效果
<template>
  <div>
    <button @click="isShow = !isShow"> 显示/隐藏</button>
    <!--   让谁有动画效果,就用transition 把谁包起来 -->
    <!--   appear属性用于控制元素在初始渲染时是否应用过渡效果 不加则初始渲染不应用-->
    <transition appear>
      <h1 v-show="isShow">hello world</h1>

    </transition>

  </div>
</template>

<style scoped>

h1{
  background-color: orange;
  transition: 0.5s linear;
}
/* 进入的起点 */
.hello-enter {
  transform: translateX(-100%);
}

/* 进入的终点 */
.hello-enter-to {
  transform: translateX(0);

}

/* 离开的起点 */
.hello-leave{
  transform: translateX(0%);

}

/* 离开的终点  */
.hello-leave-to{
  transform: translateX(-100%);

}
</style>

合并写法

h1{
  background-color: orange;
}

/* 进入的起点和离开的终点 */
.hello-enter,.hello-leave-to {
  transform: translateX(-100%);
}

/* 不去修改h1本身的样式,使用进入和离开的时候设置效果 */
.hello-enter-active,.hello-leave-active {
  transition: 0.5s linear;
}

/* 进入的终点和离开的启动 */
.hello-enter-to,.hello-leave {
  transform: translateX(0);

}
多个元素过渡
<!--  transition只能包裹一个元素   -->
<!--   包裹多个元素使用transition-group,且每个元素都有一个key值 -->
    <transition-group appear name="hello">
      <h1 v-show="isShow" key="1">hello world</h1>
      <h1 v-show="isShow" key="2">hello vue</h1>
      <h1 v-show="isShow" key="3">hello html</h1>

    </transition-group>
集成第三方动画

可以使用现成的三方动画库来实现效果

animate动画库文档(墙)

  1. 安装

    npm install animate.css --save
    
  2. 引入

    import "animate.css"
    
    
  3. 配置

        <!-- 配置name-->
        <!--   配置进入的效果  在该库文档上选用对应效果的名字-->
        <!--   配置离开的效果 在该库文档上选用对应效果的名字-->
    
        <transition-group
            appear
            name="animate__animated animate__bounce"
            enter-active-class="animate__bounceOutRight"
            leave-active-class="animate__fadeInDown"
        >
          <h1 v-show="isShow" key="1">hello world</h1>
          <h1 v-show="isShow" key="2">hello vue</h1>
          <h1 v-show="isShow" key="3">hello html</h1>
    
        </transition-group>