2024年9月

在大型项目中,由于各种组件的复杂性和互连性,管理依赖项可能变得具有挑战性。如果没有适当的工具或文档,可能很难浏览项目并对依赖项做出假设。以下是在大型项目中难以导航项目依赖项的几个原因:

  • 复杂性
    :大型项目通常由许多模块组成。了解这些依赖项如何相互交互可能会让人不知所措,尤其是当存在多层依赖项时。
  • 依赖关系链
    :依赖关系可以形成长链,其中一个模块依赖于另一个模块,而另一个模块又依赖于另一个模块,依此类推。跟踪这些链并了解更改的影响可能具有挑战性,因为一个模块中的修改可能会对其他模块产生级联影响。
  • 缺少文档
    :在某些情况下,项目可能缺乏全面的文档来清楚地概述依赖关系及其关系。如果没有适当的文档,开发人员可能需要花费额外的时间来调查和逆向工程项目结构,以了解依赖关系。

为了应对这些挑战,您可以使用 Dependify 工具:
https://github.com/NikiforovAll/dependify
,该工具提供 .NET 应用程序中依赖项的可视化表示。此工具允许您浏览依赖关系图,查看组件之间的关系,并识别项目中的潜在问题或瓶颈。

Dependify 可以帮助开发者管理和可视化项目依赖关系。Dependify 有多个功能和应用场景:

  1. CLI 支持
    :Dependify 可以直接从命令行界面(CLI)使用,支持 plain、mermaidjs 和 JSON 格式,也可以在浏览器中使用。

  2. Aspire 支持
    :Dependify 提供了 Aspire 支持,包括 Aspire Hosting 和 Ollama Aspire 组件,后者可以在本地运行 phi3:mini 模型并集成到 Dependify 中。

  3. NuGet 包
    :Dependify 作为一个 NuGet 包发布,版本为 1.0.0-beta3,可以在 Visual Studio 的 Package Manager Console 中使用 Install-Package 命令安装。

  4. Browserify 插件
    :Dependify 允许在构建步骤中使用 Browserify 的所有功能,同时仍然使用当前的方法消费打包文件。

  5. 项目依赖探索
    :Dependify 可以探索 .NET 项目中的依赖关系,支持显示指定路径中的项目或解决方案的依赖关系,输出格式可以是 tui 或 mermaid 格式。

  6. 依赖注入库
    :Dependify 是一个库,允许开发者通过添加属性到类或工厂方法来注册服务到 Microsoft 的依赖注入系统。

  7. 任务依赖管理
    :Dependify 提供了一种直观和简单的方式来映射任务依赖关系,可视化进度,并与团队共享。它还集成了由 XcelerateAI 驱动的生成式 AI,可以实时预测项目的下一个最佳行动。


综上所述,Dependify 是一个多功能的工具,适用于不同的开发场景,从项目依赖管理到任务进度可视化,再到依赖注入的自动化,都能提供支持,更详细的信息可以参看作者写的两篇博客介绍文章:

Java系列

Java核心知识体系1:泛型机制详解
Java核心知识体系2:注解机制详解
Java核心知识体系3:异常机制详解
Java核心知识体系4:AOP原理和切面应用
Java核心知识体系5:反射机制详解
Java核心知识体系6:集合框架详解
Java核心知识体系7:线程不安全分析
Java核心知识体系8:Java如何保证线程安全性

1 先导

image
Java线程基础主要包含如下知识点,相信我们再面试的过程中,经常会遇到类似的提问。

  1. 线程有哪几种状态? 线程之间如何转变?
  2. 线程有哪几种实现方式? 各优缺点?
  3. 线程的基本操作(线程管理机制)有哪些?
  4. 线程如何中断?
  5. 线程有几种互斥同步方式? 如何选择?
  6. 线程之间的协作方式(通信和协调)?

下面我们 一 一 解读。

2 线程的状态和流转

image

2.1 新建(New)

如上图,创建完线程,但尚未启动。

2.2 可运行(Runnable)

如上图,处于可运行阶段,正在运行,或者正在等待 CPU 时间片。包含了
Running

Ready
两种线程状态。

2.3 阻塞(Blocking)

如上图,正被Lock住,等待获取一个排它锁,如果其他的线程释放了锁,该状态就会结束。

2.4 无限期等待(Waiting)

如上图,处在无限期等待阶段,等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
主要有两种方式进行释放:

  • 调用方的线程执行完成
  • 使用 Object.notify() / Object.notifyAll()进行显性唤醒

2.5 限期等待(Timed Waiting)

如上图,因为有时间控制,所以无需等待其它线程显式地唤醒,一定时间之后,系统会自动唤醒。
所以他有三种方式进行释放:
主要有两种方式进行释放:

  • 调用方的线程执行完成
  • 使用 Object.notify() / Object.notifyAll()进行显性唤醒
  • 时间到结束
    • Thread.sleep()
    • Object.wait() 方法,带Timeout参数
    • Thread.join() 方法,带Timeout参数

2.6 死亡(Terminated)

  • 线程结束任务之后结束
  • 产生了异常并结束

3 线程实现方式

在Java中,线程的实现方式主要有两种:继承
Thread
类和实现
Runnable
接口。此外,Java 5开始,引入了
java.util.concurrent
包,提供了更多的并发工具,如
Callable
接口与
Future
接口,它们主要用于任务执行。

3.1 继承Thread类

通过继承
Thread
类来创建线程是最基本的方式。你需要创建一个扩展自
Thread
类的子类,并重写其
run()
方法。然后,可以创建该子类的实例来创建新的线程。

class MyThread extends Thread {
    public void run() {
        System.out.println("线程运行中");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // 调用start()方法来启动线程
    }
}

3.2 实现Runnable接口

另一种方式是让你的类实现
Runnable
接口,并实现
run()
方法。然后,你可以创建
Thread
类的实例,将实现了
Runnable
接口的类的实例作为构造参数传递给它。

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("线程运行中");
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); // 调用start()方法来启动线程
    }
}

3.3 使用Callable和Future

虽然
Callable

Future
不是直接用于创建线程的,但它们提供了一种更灵活的方式来处理线程执行的结果。
Callable
类似于
Runnable
,但它可以返回一个结果,并且可以抛出异常。
Future
用于获取
Callable
执行的结果。

import java.util.concurrent.*;

class MyCallable implements Callable<String> {
    public String call() throws Exception {
        return "任务完成";
    }
}

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new MyCallable());
        System.out.println(future.get()); // 阻塞等待获取结果
        executor.shutdown();
    }
}

3.4 优缺点解读

  • 继承Thread类
    :简单直观,但Java不支持多重继承,如果类已经继承了其他类,则不能再用这种方式。另外继承整个 Thread 类开销过大,太重了。
  • 实现Runnable接口
    :更加灵活,推荐的方式。
  • Callable和Future
    :提供了更为强大的功能,例如返回执行结果和抛出异常,但通常用于与
    ExecutorService
    等高级并发工具一起使用。

4 线程管理机制

Java 中的线程管理机制非常强大,涵盖了从简单的线程创建到复杂的线程池管理等多个方面。

4.1 Executor 框架

Executor
框架是 Java 并发包(
java.util.concurrent
)中的一个关键组件,它提供了一种更高级别的抽象来管理线程池。通过使用
Executor
,你可以更容易地控制线程的创建、执行、调度、生命周期等。它主要有三种类型:

  1. CachedThreadPool: 一个任务创建一个线程
  2. FixedThreadPool: 所有任务只能使用固定大小的线程
  3. SingleThreadExecutor: 单个线程,相当于大小为 1 的 FixedThreadPool。
  • 优点
    :提高程序性能和响应速度,通过复用线程来减少线程创建和销毁的开销,简化并发编程。
  • 使用示例

    ExecutorService executor = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
        Runnable worker = new WorkerThread("" + i);
        executor.execute(worker);
    }
    executor.shutdown();
    

4.2 守护线程(Daemon Threads)

守护线程是一种特殊的线程,它主要用于程序中“后台”任务的支持。守护线程与普通线程的区别在于,当程序中所有非守护线程结束时,JVM 会自动退出,即使还有守护线程在运行。守护线程常用于垃圾回收、JVM 内部的监控等任务。
设置守护线程
:通过调用线程对象的
setDaemon(true)
方法,在启动线程之前将其设置为守护线程。

 Thread thread = new Thread(new MyRunnable());
 thread.setDaemon(true);

4.3 sleep() 方法

sleep()
方法是
Thread
类的一个静态方法,用于让当前正在执行的线程暂停执行指定的时间(毫秒),以毫秒为单位。在指定的时间过去后,线程将回到可运行状态,等待CPU的调度。

  • 用途
    :常用于线程间的简单同步。
  • 注意

    sleep()
    方法不会释放锁(如果当前线程持有锁的话)。
  • 示例
 try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

4.4 yield() 方法

yield()
方法也是
Thread
类的一个静态方法,它告诉调度器当前线程愿意放弃当前处理器的使用,但这并不意味着线程会立即停止执行或进入等待/阻塞状态。
调度器可以忽略这个提示,继续让当前线程运行。

  • 用途
    :提示调度器让出CPU时间,但具体是否让出取决于调度器的实现。
  • 注意

    yield()
    方法不会使线程进入阻塞状态,也不会释放锁(如果持有的话),类似仅建议。
  • 示例
Thread.yield();

5 线程中断方式

在Java中,线程中断是一种重要的线程间通信机制,用于通知线程应该停止当前正在执行的任务。线程中断的方式主要有以下几种:

5.1 使用
interrupt()
方法

interrupt()
方法是Java推荐的线程中断方式。它并不会直接停止线程,而是设置线程的中断状态为true。线程需要定期检查这个中断状态(通过
isInterrupted()
方法),并根据需要自行决定如何响应中断请求,比如退出循环、释放资源等。

  • 优点
    :安全、灵活,符合Java的并发编程理念。
  • 示例

    Thread thread = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            // 执行任务
        }
        // 线程中断后的清理工作
    });
    thread.start();
    // 稍后中断线程
    thread.interrupt();
    

5.2 使用
Executor
的中断操作

  1. 调用 Executor 的 shutdown() 方法,会等待线程都执行完毕之后再关闭
  2. 调用 Executor 的 shutdownNow() 方法,则相当于直接调用具体线程的 interrupt() 方法

6 线程互斥同步方式

Java中的线程互斥同步是并发编程中的一个重要概念,用于保证多个线程在访问共享资源时的互斥性,即同一时间只有一个线程能够访问某个资源。Java提供了多种机制来实现线程的互斥同步,主要包括以下几种方式:

6.1 synchronized关键字

1. 基本概念
:synchronized是Java中最基本的同步机制,它可以用来修饰方法或代码块。当一个线程访问一个被synchronized修饰的方法或代码块时,其他试图访问该方法或代码块的线程将被阻塞,直到当前线程执行完毕释放锁。
2. 使用方法

  • 修饰方法:直接在方法声明上加上synchronized关键字,例如
    public synchronized void method() {...}
  • 修饰代码块:将需要同步的代码放在synchronized(对象) {...}中,这里的对象就是锁对象,例如
    synchronized(this) {...}

    synchronized(某个对象) {...}

3. 特性

  • 可见性:synchronized不仅保证了互斥性,还保证了变量的可见性。当一个线程释放锁时,会将锁变量的值刷新到主存储器中,从而使其他线程可以看到最新的变量值。
  • 可重入性:synchronized支持可重入性,即同一个线程可以多次获取同一个锁,而不会导致死锁。

4. 示例

public class Counter {  
    private int count = 0;  
  
    // synchronized修饰方法  
    public synchronized void increment() {  
        count++;  
    }  
  
    public synchronized int getCount() {  
        return count;  
    }  
}  
  
public class TestSynchronized {  
    public static void main(String[] args) throws InterruptedException {  
        Counter counter = new Counter();  
  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 10; i++) {  
                counter.increment();  
            }  
        });  
  
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 10; i++) {  
                counter.increment();  
            }  
        });  
  
        t1.start();  
        t2.start();  
  
        t1.join();  
        t2.join();  
  
        System.out.println("Final count: " + counter.getCount());  
    }  
}

6.2 ReentrantLock类

  • 基本概念
    :ReentrantLock是java.util.concurrent.locks包中的一个可重入锁,它提供了比synchronized更灵活的锁定机制。
  • 使用方法

    • 创建锁对象:
      ReentrantLock lock = new ReentrantLock();
    • 加锁:
      lock.lock();
    • 释放锁:通常将释放锁的代码放在finally块中,以确保锁一定会被释放,例如
      try {...} finally { lock.unlock(); }
  • 特性

    • 支持公平锁和非公平锁:通过构造器参数可以指定使用哪种锁,默认是非公平锁。
    • 支持尝试获取锁:提供了
      tryLock()
      等方法,尝试获取锁,如果获取不到则不会阻塞线程。
    • 支持中断锁定的线程:与synchronized不同,ReentrantLock的锁可以被中断。
import java.util.concurrent.locks.ReentrantLock;  
  
public class CounterWithLock {  
    private int count = 0;  
    private final ReentrantLock lock = new ReentrantLock(); // 创建ReentrantLock对象  
  
    public void increment() {  
        lock.lock(); // 加锁  
        try {  
            count++;  
        } finally {  
            lock.unlock(); // 释放锁,放在finally块中确保一定会被释放  
        }  
    }  
  
    public int getCount() {  
        lock.lock(); // 加锁  
        try {  
            return count;  
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}  
  
public class TestReentrantLock {  
    public static void main(String[] args) throws InterruptedException {  
        CounterWithLock counter = new CounterWithLock();  
  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 10000; i++) {  
                counter.increment();  
            }  
        });  
  
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 10000; i++) {  
                counter.increment();  
            }  
        });  
  
        t1.start();  
        t2.start();  
  
        t1.join();  
        t2.join();  
  
        System.out.println("Final count: " + counter.getCount());  
    }  
}

6.3 对比

对于大多数简单场景,synchronized关键字是最直接、最简单的选择;而对于需要更灵活控制锁的场景,则可以考虑使用ReentrantLock等高级同步机制。

7 线程协作(通信)方案

Java中线程之间的协作主要可以通过多种机制实现,其中等待/通知机制(
wait/notify/notifyAll
)和
join
方法是两种常用的方式。下面我将分别给出这两种方式的简单代码示例。

7.1 等待/通知机制(wait/notify/notifyAll)

等待/通知机制依赖于Java中的
Object
类,因为
wait()
,
notify()
, 和
notifyAll()
方法都定义在
Object
类中。这些方法必须在同步块或同步方法中被调用,因为它们是用来控制对某个对象的访问的。

示例代码

public class WaitNotifyExample {
    private final Object lock = new Object();
    private boolean ready = false;

    public void doWait() {
        synchronized (lock) {
            while (!ready) {
                try {
                    lock.wait(); // 当前线程等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 保持中断状态
                }
            }
            // 当ready为true时,继续执行
        }
    }

    public void doNotify() {
        synchronized (lock) {
            ready = true;
            lock.notify(); // 唤醒在此对象监视器上等待的单个线程
            // 或者使用 lock.notifyAll(); 唤醒所有等待的线程
        }
    }

    public static void main(String[] args) {
        WaitNotifyExample example = new WaitNotifyExample();

        Thread t1 = new Thread(() -> {
            System.out.println("Thread 1 is waiting");
            example.doWait();
            System.out.println("Thread 1 is proceeding");
        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000); // 假设t2需要一些时间来完成准备工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Thread 2 is notifying");
            example.doNotify();
        });

        t1.start();
        t2.start();
    }
}

在这个例子中,
t1
线程在
doWait()
方法中等待,直到
t2
线程调用
doNotify()
方法并设置
ready

true

t2
线程模拟了一些准备工作,并在之后唤醒
t1

7.2 Join 方法

join
方法是
Thread
类的一个方法,用于让当前线程等待另一个线程完成其执行。

示例代码

public class JoinExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000); // 假设t1执行需要一些时间
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Thread 1 completed");
        });

        t1.start();

        try {
            t1.join(); // 当前线程(main线程)等待t1完成
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Thread 1 has joined, continuing main thread");
    }
}

在这个例子中,
main
线程启动了一个新线程
t1
,并通过调用
t1.join()
等待
t1
完成。
t1
线程在完成后会打印一条消息,而
main
线程会在
t1
完成后继续执行并打印另一条消息。

8 总结

总结一下,我们讲了让如下内容

  1. 线程流转状态
  2. 线程实现方式
  3. 线程基本操作
  4. 线程中断方案
  5. 线程互斥同步方法
  6. 线程协作(通信)方案

1、引言

在大数据和云计算快速发展的今天,Redis作为一款高性能的内存键值存储系统,在数据缓存、实时计算、消息队列等领域发挥着重要作用。然而,随着Redis集群规模的扩大和复杂度的增加,如何高效地管理和运维Redis数据库成为了许多开发者和运维人员面临的挑战。Tiny RDM(Tiny Redis Desktop Manager)作为一款轻量级、跨平台的Redis桌面管理工具,以其高效、灵活和易用的特点,为Redis的管理和运维提供了全新的解决方案。

2、Tiny RDM介绍

Tiny RDM
是一款由Tiny Craft团队开发的开源Redis桌面管理工具,它支持macOS、Windows和Linux操作系统,安装包大小仅为10M左右,实现了极致的轻量化和跨平台特性。Tiny RDM不仅提供了丰富的Redis数据操作功能,还具备现代化的界面设计和良好的用户体验,使得Redis的管理和运维变得更加简单高效。

3、核心功能与技术特点

1、极致轻量与跨平台
Tiny RDM的安装包大小仅为10M左右,无论在哪个操作系统上都能快速安装和运行。它支持macOS、Windows和Linux三大主流操作系统,确保了广泛的兼容性。这种极致轻量和跨平台的特性,使得Tiny RDM成为了一款非常便携的Redis管理工具,用户可以随时随地使用它进行Redis数据库的管理和运维。

下载地址:
https://github.com/tiny-craft/tiny-rdm/releases

2、现代化界面与主题切换
Tiny RDM的界面设计简洁现代,符合现代审美趋势。它提供了浅色和深色两种主题切换,以满足不同用户的视觉需求。同时,Tiny RDM还支持多国语言,确保全球开发者都能无障碍地使用它。

3、丰富的登录方式与个性化连接设定
Tiny RDM支持SSH/SSL/哨兵/集群等多种登录方式,确保了与Redis服务器的安全稳定连接。同时,它还提供了丰富的个性化连接配置选项,如端口号、密码、数据库索引等,用户可以根据自己的需求进行灵活配置。

4、支持多种数据结构与操作
Tiny RDM全面支持Redis的各种数据结构操作,包括字符串(Strings)、列表(Lists)、哈希(Hashes)、集合(Sets)、排序集(Sorted Sets)以及流(Streams)等。用户可以通过可视化界面轻松地进行数据的增删改查操作,大大提高了工作效率。

5、高效的数据加载与查询
针对大规模Redis实例和海量数据的管理挑战,Tiny RDM采用了SCAN命令进行分段加载机制,确保了即使处理数百万计的键也能轻松应对。同时,它还支持对List、Hash、Set和Sorted Set等复杂数据类型的分段加载和查询,大大提升了数据读取与操作的效率。

6、强大的调试与分析功能
Tiny RDM内置了命令行模式,满足习惯于命令行操作的用户需求。同时,它还提供了慢日志查询、服务器命令实时监控、发布/订阅等功能,帮助用户深入优化Redis的性能和稳定性。此外,Tiny RDM还保存了命令操作历史记录,便于用户回溯和重复执行命令。

7、自定义解码器与编码器
除了内置常用的解码方式(如Base64、GZip等)外,Tiny RDM还支持用户自定义解码器和编码器。这使得用户可以根据实际需求,对数据库中的原始数据进行灵活解析和转换,从而满足更复杂的数据处理需求。

4、应用场景

Tiny RDM适用于各种Redis数据库的管理和运维场景,包括但不限于:

  • 个人开发者和小型团队在开发过程中的Redis数据库管理。
  • 企业级Redis集群的运维和监控。
  • Redis性能测试和压力测试中的数据模拟。
  • Redis数据备份和迁移。

5、小结

Tiny RDM
作为一款高效、灵活且易用的Redis桌面管理工具,以其极致轻量、跨平台、现代化界面和丰富的功能特性,为Redis的管理和运维提供了全新的解决方案。无论是个人开发者还是企业技术团队,都可以通过Tiny RDM实现对Redis数据库的集中化、可视化的管理和操作,从而有效提升工作效率和降低运维成本。如果你正在寻找一款优秀的Redis管理工具,那么Tiny RDM绝对值得一试。

项目地址

https://github.com/tiny-craft/tiny-rdm

论文提出了第一个端到端的半监督伪装目标检测模型
CamoTeacher
。为了解决半监督伪装目标检测中伪标签中存在的大量噪声问题,包括局部噪声和全局噪声,引入了一种名为双旋转一致性学习(
DRCL
)的新方法,包括像素级一致性学习(
PCL
)和实例级一致性学习(
ICL
)。
DRCL
帮助模型缓解噪音问题,有效利用伪标签信息,使模型在避免确认偏差的同时可以获得充分监督。广泛的实验验证了
CamoTeacher
的优越性能,同时显著降低了标注要求。

来源:晓飞的算法工程笔记 公众号

论文: CamoTeacher: Dual-Rotation Consistency Learning for Semi-Supervised Camouflaged Object Detection

Introduction


伪装物体检测(
COD
)旨在识别在其环境中完全融入的物体,包括动物或具有保护色彩并具有融入周围环境能力的人造实体,这一任务由于低对比度、相似纹理和模糊边界而变得复杂。与一般物体检测不同,
COD
受到这些因素的挑战,使得检测变得格外困难。现有的
COD
方法严重依赖于大规模的像素级注释数据集,其创建需要大量的人力和成本,从而限制了
COD
的进展。

为了缓解这一问题,半监督学习作为一种有希望的方法出现,利用标记和未标记数据。然而,由于复杂的背景和微妙的物体边界,其在
COD
中的应用并不直接。半监督学习在
COD
中的有效性受到伪标签中存在的大量噪声的严重影响,伪标签的噪声有两种主要类型:像素级噪声,表明在单个伪标签内的变化,以及实例级噪声,显示不同伪标签之间的变化。这种区分是至关重要的,因为它指导了如何改进伪标签质量以提高模型训练的方法。(
1
)像素级噪声的特点是在伪标签的各个部分内部的标注不一致。如图
1a
中所示,在第一行中,壁虎的尾部在视觉上比头部更难以识别。由
SINet
生成的伪标签在其尾部区域中的准确性较低(由红色框标出)。这一观察结果强调了对伪标签内的所有部分统一处理的不当性。(
2
)实例级噪声指的是不同伪标签之间噪声水平的变化。如图
1a
所示,第三行的伪标签与第二行相比不太准确,因为第三行中的伪装对象更难以检测。这些差异表明每个伪标签对模型训练的贡献是不同的,强调了需要对整合伪标签信息采取细致差异的方法。

为了解决在没有未标记
GT
的数据的情况下评估伪标签噪声的挑战,论文提出了基于两个旋转视图的像素级不一致性和实例级一致性的两种新策略。具体来说,对于像素级噪声,论文观察到通过比较两个旋转视图的伪标签计算出的像素级不一致性,可以反映相对于
GT
的实际误差,如图
2a
所示。这种关系显示了不同部分之间平均像素级不一致性与平均绝对误差(
MAE
)之间的正相关性,如图
2b
的折线所示。因此,具有较高像素级不一致性的区域更容易出现不准确性,表明在训练过程中需要减弱这些区域的重要性。

对于实例级噪声,跨旋转视图具有更大相似性的伪标签展示了更低的噪声水平,如图
3a
所示。伪标签和
GT
计算的
SSIM
之间的实例级一致性与正相关性进一步支持了这一观察结果,如图
3b
所示。因此,表现出更高实例级一致性的伪标签可能具有更高质量,并应在学习过程中优先考虑。

通过这些观察结果,论文提出了一种名为
CamoTeacher
的半监督伪装物体检测框架,该框架结合了一种名为
Dual-Rotation Consistency Learning

DRCL
)的新方法。具体而言,
DRCL
通过两个核心组件来实现其策略:像素级一致性学习(
PCL
)和实例级一致性学习(
ICL
)。
PCL
通过考虑不同旋转视图之间的像素级不一致性,创新地为伪标签中的不同部分分配可变权重。同时,
ICL
根据实例级一致性调整各个伪标签的重要性,实现细致、噪声感知的训练过程。

论文采用
SINet
作为基础模型来实现
CamoTeacher
,并将其应用于更经典的伪装物体检测(
COD
)模型,即基于
CNN

SINet-v2

SegMaR
,以及基于
Transforme

DTINet

FSPNet
。在四个
COD
基准数据集(即
CAMO

CHAMELEON

COD10K

NC4K
)上进行了大量实验,结果显示
CamoTeacher
不仅在与半监督学习方法相比方面达到了最先进的水平,而且与已建立的全监督学习方法相媲美。具体来说,如图
1b
所示,仅使用了
20%
的标记数据,它几乎达到了在
COD10K
上全监督模型的性能水平。

论文的贡献可以总结如下:

  1. 引入了第一个端到端的半监督伪装物体检测框架
    CamoTeacher
    ,为未来半监督伪装物体检测的研究提供了一个简单而有效的基准。

  2. 为解决半监督伪装物体检测中伪标签中大量噪声的问题,提出了
    Dual-Rotation Consistency Learning

    DRCL
    ),其中包括
    Pixel-wise Consistency Learning

    PCL
    )和
    Instance-wise Consistency Learning

    ICL
    ),允许自适应调整不同质量伪标签的贡献,从而有效利用伪标签信息。


  3. COD
    基准数据集上进行了大量实验,相较于完全监督设置,取得了显著的改进。

Methodology


Task Formulation

半监督伪装物体检测旨在利用有限的标记数据训练一个能够识别与周围环境无缝融合的物体的检测器。由于物体与背景之间的对比度较低,这个任务本身具有挑战性。给定一个用于训练的伪装物体检测数据集
\(D\)
,含
\(M\)
个标记样本的标记子集表示为
\(D_L=\{x_i^{l}, y_i\}_{i=1}^{M}\)
,含
\(N\)
个未标记样本的未标记子集表示为
\(D_U=\{x_i^{u}\}_{i=1}^{N}\)
,其中
\(x_i^{l}\)

\(x_i^{u}\)
表示输入图像,
\(y_i\)
表示标记数据的相应注释掩码。通常,
\(D_L\)
只占整个数据集
\(D\)
的很小一部分,这突出了
\(M \ll N\)
的半监督学习场景。对于
\(M \ll N\)
的强调,强调了半监督学习中的挑战和机遇:通过利用未标记数据
\(D_U\)
尚未发掘的潜力来提升检测能力,而这远远超过了标记子集
\(D_L\)

Overall Framework

如图
4
所示,采用
Mean Teacher
作为初步方案,以实现端到端的半监督伪装物体检测框架。该框架包含两个具有相同结构的
COD
模型,即教师模型和学生模型,分别由参数
\(\Theta_t\)

\(\Theta_s\)
参数化。教师模型生成伪标签,然后用于优化学生模型。整体损失函数
\(L\)
可以定义为:

\[\begin{equation}
L = L_s + \lambda_u L_u ,
\end{equation}
\]

其中,
\(L_s\)

\(L_u\)
分别表示有监督损失和无监督损失,
\(\lambda_u\)
是平衡损失项的无监督损失权重。按照经典的
COD
方法,使用二元交叉熵损失
\(L_{bce}\)
用于训练。

在训练过程中,采用弱数据增强
\(\mathcal{A}^w(\cdot)\)
和强数据增强
\(\mathcal{A}^s(\cdot)\)
策略的组合。弱数据增强应用于有标记数据以减轻过拟合,而无标记数据在强数据增强下经历各种数据扰动,以创造同一图像的不同视角。有监督损失
\(L_s\)
的定义如下:

\[\begin{equation}
L_s = \frac{1}{M} \sum\limits^{M}_{i=1} L_{bce}(F(\mathcal{A}^w(x_i^l);\Theta_s), y_i) ,
\end{equation}
\]

其中,
\(F(\mathcal{A}(x_i);\Theta)\)
表示模型
\(\Theta\)
对第
\(i\)
张图像在增强
\(\mathcal{A}(\cdot)\)
下的检测结果。对于无标记的图像,首先应用弱数据增强
\(\mathcal{A}^w(\cdot)\)
,然后将其传递给教师模型。这一初始步骤对于在不显著改变图像核心特征的变化下生成可靠的伪标签
\(\widehat{y_i}\)
至关重要。这些伪标签作为学生模型的一种软监督形式。接下来,相同的图像经过强数据增强
\(\mathcal{A}^s(\cdot)\)
后传递给学生模型。这个过程引入了更高层次的变异性和复杂性,模拟更具挑战性的条件,以适应学生模型。学生模型基于这些经过强增强的图像生成预测
\(p_i\)
,利用伪标签
\(\widehat{y_i}\)
作为无标记数据学习的指导。可以将其形式化为:

\[\begin{equation}
\widehat{y_i} = F(\mathcal{A}^w(x_i^u);\Theta_t), \ p_i = F(\mathcal{A}^s (\mathcal{A}^w(x_i^u));\Theta_s) .
\end{equation}
\]

因此,无监督损失
\(L_u\)
可以表示为:

\[\begin{equation}
L_u = \frac{1}{N} \sum\limits^{N}_{i=1} L_{bce}(p_i, \widehat{y_i}).
\end{equation}
\]

最后,学生模型通过总损失
\(L\)
进行密集训练,该损失包含了半监督框架中有监督和无监督学习的两个方面。这种方法确保学生模型从有标记和伪标记数据中受益,提高其检测能力。同时,教师模型通过指数移动平均(
EMA
)机制进行系统更新,有效地提取学生知识并防止噪音干扰,具体表述为:

\[\begin{equation}
\Theta_t \leftarrow \eta \Theta_t + (1 - \eta)\Theta_s ,
\end{equation}
\]

其中,
\(\eta\)
是一个超参数,表示保留的比例。

Dual-Rotation Consistency Learning

由于物体的伪装性质,伪标签中包含大量噪音,直接使用它们来优化学生模型可能会损害模型的性能。为解决这个问题,最直观的一个可能方法是设置一个固定的高阈值来过滤高质量的伪标签,但这会导致召回率较低,并使得难以充分利用伪标签的监督信息。为此,论文提出了双旋转一致性学习(
DRCL
),以动态调整伪标签的权重,减少噪音的影响。

对图像
\(x_i\)
进行两个独立的随机旋转,其中
\(x_i\)
在之前已进行了翻转和随机调整大小,得到两个不同的旋转视图
\(x_i^{r_1}\)

\(x_i^{r_2}\)

\[\begin{equation}
x_i^{r_1} = R(\mathcal{A}^w(x_i), \theta_1), \ x_i^{r_2} = R(\mathcal{A}^w(x_i), \theta_2),
\end{equation}
\]

其中,
\(x_i^{r} = R(x_i, \theta)\)
表示将输入图像
\(x_i\)
旋转
\(\theta\)
度。将获得的旋转视图输入到教师模型中,得到相应的预测值,即
\(\widehat y_i^{r} = F(x_i^{r}; \Theta_t)\)
。随后,对预测值进行
\(-\theta\)
的相反旋转,使其返回到原始的水平方向,得到
\(\widehat y_i^{h_1}\)

\(\widehat y_i^{h_2}\)
,以便在不同的旋转视图下计算预测不一致性。

\[\begin{equation}
\widehat y_i^{h_1} = R(\widehat y_i^{r_1}, -\theta_1), \ \widehat y_i^{h_2} = R(\widehat y_i^{r_2}, -\theta_2).
\end{equation}
\]

请注意,旋转会引入黑色的边界区域,这些区域不参与
DRCL
的计算过程。

由于伪标签的不同区域和不同伪标签之间的噪声水平不同,引入
PCL

ICL
动态调整不同像素在伪标签内部和各个伪标签之间的贡献。

  • Pixel-wise Consistency Learning

在像素级别上对水平预测
\(\widehat y_i^{h_1}\)

\(\widehat y_i^{h_2}\)
进行减法运算,得到像素级别的不一致性
\(\Delta_i\)

\[\begin{equation}
\Delta_i = | \widehat y_i^{h_1} - \widehat y_i^{h_2} |.
\end{equation}
\]

不同视图之间的像素级不一致性
\(\Delta_i\)
反映了伪标签的可靠性。然而,在两个旋转视图的预测值都接近
0.5
的情况下,
\(\Delta_i\)
无法有效区分它们。这些预测表现出高度的不确定性,意味着不能明确将它们分类为前景或背景,并且很可能代表嘈杂的标签。因此,有必要通过降低它们的权重来减弱它们的影响。因此,计算水平预测值
\(\widehat y_i^{h}\)
的平均值,

\[\begin{equation}
\widehat y_i^{h} = avg ( \widehat y_i^{h_1} , \widehat y_i^{h_2} ),
\end{equation}
\]

其中,
\(avg(\cdot, \cdot)\)
表示计算两个像素级别输入的平均值,并使用其与
0.5

L2
距离作为调整权重的一个组成部分。

因此,根据不同旋转视图之间的像素级别不一致性,推导出像素级别一致性权重
\(\omega_i^{pc}\)
,如下所示:

\[\begin{equation}
\omega_i^{pc} = (1 - \Delta_i^{\alpha})||\widehat y_i^{h} - \mu||_2^2 ,\label{wlc}
\end{equation}
\]

其中,
\(\alpha\)
是一个超参数,
\(\mu=0.5\)
。这个动态的像素级一致性权重
\(\omega_i^{pc}\)
会给与不同旋转视图间预测一致的区域分配更高的权重,而对于预测不一致的区域则分配较小的权重。

总而言之,将
PCL
损失函数
\(L_u^{PC}\)
表述为:

\[\begin{equation}
\label{unsup_loss}
\begin{split}
L_u^{PC} &= \frac{1}{N} \sum\limits^{N}_{i=1} \omega_{i}^{pc} L_{bce}(p_{i}, \widehat {y}_{i}^{r_1}) \\
&= - \frac{1}{NHW} \sum\limits^{N}_{i=1} \sum\limits^{H \times W}_{j=1} \omega_{i, j}^{pc} [\widehat {y}_{i, j}^{r_1}\log p_{i, j} \\
& \quad \quad \quad \quad \quad \quad + (1 - \widehat {y}_{i, j}^{r_1})\log (1-p_{i, j})] ,
\end{split}
\end{equation}
\]

自适应地调整每个像素的权重,以确保对学生模型进行全面监督,同时避免带来偏见。

  • Instance-wise Consistency Learning

不同图像之间的伪装程度会有所不同,导致伪标签质量在图像之间存在显著变化。平等地对待所有伪标签是不合理的。不幸的是,对于未标记的图像,评估伪标签质量是具有挑战性的,因为没有可用的
GT
标签。论文呢观察到两个旋转视图的实例一致性和伪标签质量之间存在正相关,由
SSIM
量化。基于此,引入
ICL
来调整具有不同质量的伪标签的贡献。将实例级一致性权重
\(\omega_i^{ic}\)
表示如下:

\[\begin{equation}
\omega_i^{ic} = (SSIM( \widehat y_i^{h_1} , \widehat y_i^{h_2} ))^{\beta},
\end{equation}
\]

其中,
\(\beta\)
是一个超参数,用于调整实例级一致性和伪标签质量之间的分布关系。

使用交并比(
IoU
)损失作为实例级限制,因此,
ICL
损失可以表示为:

\[\begin{equation}
\begin{split}
L_{u}^{IC} &= \frac{1}{N} \sum\limits^{N}_{i=1} \omega_i^{ic} L_{iou}( p_i , \widehat y_i^{r_1} ) \\
&= \frac{1}{NHW} \sum\limits^{N}_{i=1} \sum\limits^{H \times W}_{j=1} \omega_i^{ic} \Bigg ( 1 - \frac{ p_{i, j} \widehat {y}_{i,j}^{r_1} }{ p_{i,j} + \widehat {y}_{i, j}^{r_1} - p_{i,j} \widehat y_{i, j}^{r_1} } \Bigg ).
\end{split}
\end{equation}
\]

因此,最终的总损失
\(L\)
由三个部分组成:有监督损失
\(L_s\)

PCL
损失
\(L_u^{LC}\)

ICL
损失
\(L_u^{GC}\)
,可以表示为:

\[\begin{equation}
L = L_s + \lambda_u^{pc} L_u^{PC} + \lambda_{u}^{ic} L_u^{IC},
\end{equation}
\]

其中,
\(\lambda_u^{pc}\)

\(\lambda_{u}^{ic}\)
是超参数。

Experiment


Experiment Settings

  • Dataset

在四个基准数据集
CAMO

CHAMELEON

COD10K

NC4K
上评估了
CamoTeacher
模型。在
CAMO
数据集中,共有
2500
张图像,包括
1250
张伪装图像和
1250
张非伪装图像。
CHAMELEON
数据集包含
76
张手动注释图像。
COD10K
数据集由
5066
张伪装图像、
3000
张背景图像和
1934
张非伪装图像组成。
NC4K
是另一个包含
4121
张图像的大规模
COD
测试数据集。根据先前的工作中的数据划分,使用
COD10K

3040
张图像和
CAMO

1000
张图像作为实验的训练集。剩余的图像来自这两个数据集,被用作测试集。在训练过程中,采用了半监督分割的数据划分方法。我们从训练集中随机采样了
1%

5%

10%

20%

30%
的图像作为有标签的数据,剩余的部分作为无标签的数据。

  • Evaluation Metrics

参考先前的工作,在
COD
中使用了
6
个常见的评估指标来评估我们的
CamoTeacher
模型,包括
S-measure
(
\(S_{\alpha}\)
)、加权
F-measure
(
\(F_{\beta}^w\)
)、平均
E-measure
(
\(E_{\phi}^m\)
)、最大
E-measure
(
\(E_{\phi}^x\)
)、平均
F-measure
(
\(F_{\beta}^m\)
)和平均绝对误差(
\(M\)
)。

  • Implementation Details

提出的
CamoTeacher
模型使用
PyTorch
进行实现。采用
SINet
作为
COD
模型的基线。使用带有动量
0.9

SGD
优化器和多项式学习率衰减,初始学习率为
0.01
,来训练学生模型。训练周期设置为
40
个周期,其中前
10
个周期为
burn-in
阶段。批量大小为
20
,有标签数据和无标签数据的比例为
1
:
1
,即每个批次包含
10
个有标签和
10
个无标签的图像。在训练和推断过程中,每个图像被调整为
\(352 \times 352\)
的大小。通过
EMA
方法更新教师模型,动量
\(\eta\)

0.996
。弱数据增强包括随机翻转和随机缩放,而强数据增强涉及颜色空间转换,包括
Identity

Autocontrast

Equalize

Gaussian blur

Contrast

Sharpness

Color

Brightness

Hue

Posterize

Solarize
,从这个列表中随机选择最多
3
个。

Results



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

work-life balance.

前言

Vue3.5正式版
在这两天发布了,网上已经有了不少关于Vue3.5版本的解读文章。但是欧阳发现这些文章对3.5中新增的功能介绍都
不是很全
,所以导致不少同学有个
错觉
,觉得Vue3.5版本不过如此,选择跳过这个版本等下个大版本再去更新。所以欧阳写了这篇
超级详细
的Vue3.5版本解读文章,小伙伴们可以看看在3.5版本中有没有增加一些你期待的功能。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

版本号

这次的版本号是
天元突破红莲螺岩
,这是07年出的一个二次元动漫,欧阳是没看过的。在此之前我一直以为这次的版本号会叫
黑神话:悟空
,可能悟空不够二次元吧。

响应式

响应式相关的内容主要分为:重构响应式、响应式props支持解构、新增
onEffectCleanup
函数、新增
base watch
函数、新增
onWatcherCleanup
函数、新增
pause

resume
方法。

重构响应式

这次响应式的重构是属于Vue内部优化,对于普通开发者来说是无感的。重构后内存占用减少了56%,优化手段主要是通过
版本计数

双向链表数据结构
,灵感来源于
Preact signals
。后续欧阳会出一系列关于响应式相关的源码文章,大家可以关注一波欧阳。

响应式props支持解构

在3.5中响应式props支持解构终于正式稳定了,在没有这个功能之前我们想要在js中访问prop必须要这样写:
props.name
,否则
name
将会丢失响应式。

有了响应式props解构后,在js中我们就可以直接解构出
name
来使用,比如下面这样的代码:

<script setup lang="ts">
const { name } = defineProps({
  name: String,
});

console.log(name);
</script>


defineProps
搭配解构一起使用后,在编译时就可以将
name
处理成
props.name
。编译后简化的代码如下:

setup(__props) {
  console.log(__props.name);
  const __returned__ = {};
  return __returned__;
}

从上面的代码可以看到
console.log(name)
经过编译后变成了
console.log(__props.name)
,这样处理后
name
当然就不会丢失响应式了。

新增onEffectCleanup函数

在组件卸载之前或者下一次
watchEffect
回调执行之前会自动调用
onEffectCleanup
函数,有了这个函数后你就不需要在组件的
beforeUnmount
钩子函数去统一清理一些timer了。比如下面这个场景:

import { watchEffect, ref } from "vue";
import { onEffectCleanup } from "@vue/reactivity";

const flag = ref(true);
watchEffect(() => {
  if (flag.value) {
    const timer = setInterval(() => {
      // 做一些事情
      console.log("do something");
    }, 200);
    onEffectCleanup(() => {
      clearInterval(timer);
    });
  }
});

上面这个例子在
watchEffect
中会去注册一个循环调用的定时器,如果不使用
onEffectCleanup
,那么我们就需要在
beforeUnmount
钩子函数中去清理定时器。

但是有了
onEffectCleanup
后,将
clearInterval
放在他的回调中就可以了。当组件卸载时会自动执行
onEffectCleanup
传入的回调函数,也就是会执行
clearInterval
清除定时器。

还有一点值得注意的是
onEffectCleanup
函数目前没有在
vue
包中暴露出来,如果你想使用可以像我这样从
@vue/reactivity
包中导入
onEffectCleanup
函数。

新增base watch函数

我们之前使用的
watch
函数是和Vue组件以及生命周期一起实现的,他们是深度绑定的,所以
watch
函数代码的位置在vue源码中的
runtime-core
模块中。

但是有的场景中我们只想使用vue的响应式功能,也就是vue源码中的
reactivity
模块,比如小程序
vuemini
。为此我们不得不将
runtime-core
模块也导入到项目中,或者像
vuemini
一样去手写一个watch函数。

在3.5版本中重构了一个
base watch
函数,这个函数的实现和vue组件没有一毛钱关系,所以他是在
reactivity
模块中。详情可以查看我之前的文章:
Vue3.5新增的baseWatch让watch函数和Vue组件彻底分手

还有一点就是这个
base watch
函数对于普通开发者来说没有什么影响,但是对于一些下游项目,比如
vuemini
来说是和受益的。

新增onWatcherCleanup函数

和前面的
onEffectCleanup
函数类似,在组件卸载之前或者下一次
watch
回调执行之前会自动调用
onWatcherCleanup
函数,同样有了这个函数后你就不需要在组件的
beforeUnmount
钩子函数去统一清理一些timer了。比如下面这个场景:

import { watch, ref, onWatcherCleanup } from "vue";

watch(flag, () => {
  const timer = setInterval(() => {
    // 做一些事情
    console.log("do something");
  }, 200);
  onWatcherCleanup(() => {
    console.log("清理定时器");
    clearInterval(timer);
  });
});


onEffectCleanup
函数不同的是我们可以从vue中import导入
onWatcherCleanup
函数。

新增pause和resume方法

有的场景中我们可能想在“一段时间中暂停一下”,不去执行
watch
或者
watchEffect
中的回调。等业务条件满足后再去恢复执行
watch
或者
watchEffect
中的回调。在这种场景中
pause

resume
方法就能派上用场啦。

下面这个是
watchEffect
的例子,代码如下:

<template>
  <button @click="count++">count++</button>
  <button @click="runner2.pause()">暂停</button>
  <button @click="runner2.resume()">恢复</button>
</template>

<script setup lang="ts">
import { watchEffect } from "vue";

const count = ref(0);
const runner = watchEffect(() => {
  if (count.value > 0) {
    console.log(count.value);
  }
});
</script>

在上面的demo中,点击
count++
按钮后理论上每次都会执行一次
watchEffect
的回调。

但是当我们点击了暂停按钮后就会执行
pause
方法进行暂停,在暂停期间
watchEffect
的回调就不会执行了。

当我们再次点击了恢复按钮后就会执行
resume
方法进行恢复,此时
watchEffect
的回调就会重新执行。

console.log
的结果如下图:
console

从上图中可以看到
count
打印到4后就没接着打印了,因为我们执行了
pause
方法暂停了。当重新执行了
resume
方法恢复后可以看到
count
又重新开始打印了,此时从8开始打印了。

不光
watchEffect
可以执行
pause

resume
方法,
watch
一样也可以执行
pause

resume
方法。代码如下:

const runner = watch(count, () => {
  if (count.value > 0) {
    console.log(count.value);
  }
});

runner.pause()	// 暂停方法
runner.resume()	// 恢复方法

watch的deep选项支持传入数字

在以前
deep
选项的值要么是
false
,要么是
true
,表明是否深度监听一个对象。在3.5中
deep
选项支持传入数字了,表明监控对象的深度。

比如下面的这个demo:

const obj1 = ref({
  a: {
    b: 1,
    c: {
      d: 2,
      e: {
        f: 3,
      },
    },
  },
});

watch(
  obj1,
  () => {
    console.log("监听到obj1变化");
  },
  {
    deep: 3,
  }
);

function changeDeep3Obj() {
  obj1.value.a.c.d = 20;
}

function changeDeep4Obj() {
  obj1.value.a.c.e.f = 30;
}

在上面的例子
watch

deep
选项值是3,表明监听到对象的第3层。

changeDeep3Obj
函数中就是修改对象的第3层的
d
属性,所以能够触发
watch
的回调。


changeDeep4Obj
函数是修改对象的第4层的
f
属性,所以不能触发
watch
的回调。

SSR服务端渲染

服务端渲染SSR主要有这几个部分:新增
useId
函数、Lazy Hydration  懒加载水合、
data-allow-mismatch

新增
useId
函数

有时我们需要生成一个随机数塞到DOM元素上,比如下面这个场景:

<template>
  <label :htmlFor="id">Do you like Vue3.5?</label>
  <input type="checkbox" name="vue3.5" :id="id" />
</template>

<script setup lang="ts">
const id = Math.random();
</script>

在这个场景中我们需要生成一个随机数
id
,在普通的客户端渲染中这个代码是没问题的。

但是如果这个代码是在SSR服务端渲染中那么就会报警告了,如下图:
useId

上面报错的意思是服务端和客户端生成的
id
不一样,因为服务端和客户端都执行了一次
Math.random()
生成
id
。由于
Math.random()
每次执行的结果都不同,自然服务端和客户端生成的
id
也不同。

useId
函数的作用就是为了解决这个问题。

当然
useId
也可以用于客户端渲染的一些场景,比如在列表中我们需要一个唯一键,但是服务端又没有给我们,这时我们就可以使用
useId
给列表中的每一项生成一个唯一键。

Lazy Hydration  懒加载水合

异步组件现在可以通过 defineAsyncComponent() API 的 hydrate 选项来控制何时进行水合。(欧阳觉得这个普通开发者用不上,所以就不细讲了)

data-allow-mismatch

SSR中有的时候确实在服务端和客户端生成的html不一致,比如在DOM上面渲染当前时间,代码如下:

<template>
  <div>当前时间是:{{ new Date() }}</div>
</template>

这种情况是避免不了会出现前面
useId
例子中的那种警告,此时我们可以使用
data-allow-mismatch
属性来干掉警告,代码如下:

<template>
  <div data-allow-mismatch>当前时间是:{{ new Date() }}</div>
</template>

Custom Element 自定义元素改进

这个欧阳也觉得平时大家都用不上,所以就不细讲了。

Teleport组件新增defer延迟属性

Teleport
组件的作用是将children中的内容传送到指定的位置去,比如下面的代码:

<div id="target"></div>
<Teleport to="#target">被传送的内容</Teleport>

文案
被传送的内容
最终会渲染在
id="target"
的div元素中。

在之前有个限制,就是不能将
<div id="target">
放在
Teleport
组件的后面。

这个也很容易理解DOM是从上向下开始渲染的,如果先渲染到
Teleport
组件。然后就会去找id的值为
target
的元素,如果找不到当然就不能成功的将
Teleport
组件的子节点传送到
target
的位置。

在3.5中为了解决这个问题,在
Teleport
组件上新增了一个
defer
延迟属性。

加了
defer
延迟属性后就能将
target
写在
Teleport
组件的后面,代码如下:

<Teleport defer to="#target">被传送的内容</Teleport>
<div id="target"></div>

defer
延迟属性的实现也很简单,就是等这一轮渲染周期结束后再去渲染
Teleport
组件。所以就算是
target
写在
Teleport
组件的后面,等到渲染
Teleport
组件的时候
target
也已经渲染到页面上了。

useTemplateRef
函数

vue3中想要访问DOM和子组件可以使用ref进行模版引用,但是这个ref有一些让人迷惑的地方。

比如定义的ref变量到底是一个响应式数据还是DOM元素?

还有template中ref属性的值明明是一个字符串,比如
ref="inputEl"
,怎么就和script中同名的
inputEl
变量绑到一块了呢?

3.5中的
useTemplateRef
函数就可以完美的解决了这些问题。

这是3.5之前使用ref访问input输入框的例子:

<input type="text" ref="inputEl" />

const inputEl = ref<HTMLInputElement>();

这个写法很不符合编程直觉,不知道有多少同学和欧阳一样最开始用vue3时会给
ref
属性绑定一个响应式变量。比如这样:
:ref="inputEl"

更加要命的是这样写还不会报错,就是
inputEl
中的值一直是
undefined

最后一番排查后才发现
ref
属性应该是绑定的变量名称:
ref="inputEl"

使用
useTemplateRef
函数后就好多了,代码如下:

<input type="text" ref="inputRef" />

const inputEl = useTemplateRef<HTMLInputElement>("inputRef");

使用
useTemplateRef
函数后会返回一个ref变量,
useTemplateRef
函数传的参数是字符串
"inputRef"

在template中
ref
属性的值也是字符串
"inputRef"
,所以
useTemplateRef
函数的返回值就指向了DOM元素input输入框。这个比3.5之前的体验要好很多了,详情可以查看我之前的文章:
牛逼!Vue3.5的useTemplateRef让ref操作DOM更加丝滑

总结

对于开发者来说Vue3.5版本中还是新增了许多有趣的功能的,比如:
onEffectCleanup
函数、
onWatcherCleanup
函数、
pause

resume
方法、
watch

deep
选项支持传入数字、
useId
函数、
Teleport
组件新增
defer
延迟属性、
useTemplateRef
函数。

这些功能在一些特殊场景中还是很有用的,欧阳的个人看法还是得将Vue升到3.5。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书
vue3编译原理揭秘
,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。