2024年11月

一、引言

站长接触 AOT 已有 3 个月之久,此前在《
好消息:NET 9 X86 AOT的突破 - 支持老旧Win7与XP环境
》一文中就有所提及。在这段时间里,站长使用 Avalonia 开发的项目也成功完成了 AOT 发布测试。然而,这一过程并非一帆风顺。站长在项目功能完成大半部分才开始进行 AOT 测试,期间遭遇了不少问题,可谓是 “踩坑无数”。为了方便日后回顾,也为了给广大读者提供参考,在此将这段经历进行总结。

.NET AOT是将.NET代码提前编译为本机代码的技术。其优势众多,启动速度快,减少运行时资源占用,还提高安全性。AOT发布后无需再安装.NET运行时等依赖。.NET 8、9 AOT发布后,可在XP、Win7非SP1操作系统下运行。这使得应用部署更便捷,能适应更多老旧系统环境,为开发者拓展了应用场景,在性能提升的同时,也增加了系统兼容性,让.NET应用的开发和部署更具灵活性和广泛性,给用户带来更好的体验。

二、经验之谈

(一)测试策略的重要性

从项目创建伊始,就应养成良好的习惯,即只要添加了新功能或使用了较新的语法,就及时进行 AOT 发布测试。否则,问题积累到后期,解决起来会异常艰难,站长就因前期忽视了这一点,付出了惨痛的代价。无奈的解决方法是重新创建项目,然后逐个还原功能并进行 AOT 测试。经过了一周的加班AOT测试,每个 AOT 发布过程大致如下:

  1. 内网 AOT 发布一次需 2、3 分钟,这段时间只能看看需求文档、技术文章、需求文档、技术文章。。。
  2. 发布完成,运行无效果,体现在双击未出现界面,进程列表没有它,说明程序崩溃了,查看系统应用事件日志,日志中通常会包含异常警告信息。
  3. 依据日志信息检查代码,修改相关 API。
  4. 再次进行 AOT 发布,重复上述 1 - 3 步骤。

经过一周的努力,项目 AOT 后功能测试终于正常,至此收工。

(二)AOT 需要注意的点及解决方法

1. 添加rd.xml

在主工程创建一个XML文件,例如
Roots.xml
,内容大致如下:

<linker>
	<assembly fullname="CodeWF.Toolbox.Desktop" preserve="All" />
</linker>

需要支持AOT的工程,在该XML中添加一个
assembly
节点,
fullname
是程序集名称,
CodeWF.Toolbox.Desktop
是站长小工具的主工程名,
点击
查看源码。

在主工程添加
ItemGroup
节点关联该XML文件:

<ItemGroup>
    <TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>

2. Prism支持

站长使用了Prism框架及DryIOC容器,若要支持 AOT,需要添加以下 NuGet 包:

<PackageReference Include="Prism.Avalonia" Version="8.1.97.11073" />
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11073" />

rd.xml
需要添加

<assembly fullname="Prism" preserve="All" />
<assembly fullname="DryIoc" preserve="All" />
<assembly fullname="Prism.Avalonia" preserve="All" />
<assembly fullname="Prism.DryIoc.Avalonia" preserve="All" />

3. App.config读写

在.NET Core中使用
System.Configuration.ConfigurationManager
包操作App.config文件,
rd.xml
需添加如下内容:

<assembly fullname="System.Configuration.ConfigurationManager" preserve="All" />

使用
Assembly.GetEntryAssembly().location
失败,目前使用
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)
获取的应用程序程序配置,指定路径的方式后续再研究。

4. HttpClient使用

rd.xml
添加如下内容:

<assembly fullname="System.Net.Http" preserve="All" />

5. Dapper支持

Dapper的AOT支持需要安装
Dapper.AOT
包,
rd.xml
添加如下内容:

<assembly fullname="Dapper" preserve="All" />
<assembly fullname="Dapper.AOT" preserve="All" />

数据库操作的方法需要添加
DapperAOT
特性,举例如下:

[DapperAot]
public static bool EnsureTableIsCreated()
{
    try
    {
        using var connection = new SqliteConnection(DBConst.DBConnectionString);
        connection.Open();

        const string sql = $@"
            CREATE TABLE IF NOT EXISTS {nameof(JsonPrettifyEntity)}(
                {nameof(JsonPrettifyEntity.IsSortKey)} Bool,
                {nameof(JsonPrettifyEntity.IndentSize)} INTEGER
        )";

        using var command = new SqliteCommand(sql, connection);
        return command.ExecuteNonQuery() > 0;
    }
    catch (Exception ex)
    {
        return false;
    }
}

6. System.Text.Json

参考
JsonExtensions.cs

序列化

public static bool ToJson<T>(this T obj, out string? json, out string? errorMsg)
{
    if (obj == null)
    {
        json = default;
        errorMsg = "Please provide object";
        return false;
    }

    var options = new JsonSerializerOptions()
    {
        WriteIndented = true,
        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    };
    try
    {
        json = JsonSerializer.Serialize(obj, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        json = default;
        errorMsg = ex.Message;
        return false;
    }
}

反序列化

public static bool FromJson<T>(this string? json, out T? obj, out string? errorMsg)
{
    if (string.IsNullOrWhiteSpace(json))
    {
        obj = default;
        errorMsg = "Please provide json string";
        return false;
    }

    try
    {
        var options = new JsonSerializerOptions()
        {
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
            TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        };
        obj = JsonSerializer.Deserialize<T>(json!, options);
        errorMsg = default;
        return true;
    }
    catch (Exception ex)
    {
        obj = default;
        errorMsg = ex.Message;
        return false;
    }
}

7. 反射问题

参考项目
CodeWF.NetWeaver

  1. 创建指定类型的
    List<T>

    Dictionary<T>
    实例:
public static object CreateInstance(Type type)
{
    var itemTypes = type.GetGenericArguments();
    if (typeof(IList).IsAssignableFrom(type))
    {
        var lstType = typeof(List<>);
        var genericType = lstType.MakeGenericType(itemTypes.First());
        return Activator.CreateInstance(genericType)!;
    }
    else
    {
        var dictType = typeof(Dictionary<,>);
        var genericType = dictType.MakeGenericType(itemTypes.First(), itemTypes[1]);
        return Activator.CreateInstance(genericType)!;
    }
}
  1. 反射调用
    List<T>

    Dictionary<T>

    Add
    方法添加元素失败,下面是伪代码:
// List<T>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ child })
    
// Dictionary<Key, Value>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ key, value })

解决办法,转换为实现的接口调用:

// List<T>
(obj as IList).Add(child);

// Dictionary<Key, Value>
(obj as IDictionary)[key] = value;
  1. 获取数组、
    List<T>

    Dictionary<key, value>
    的元素个数

同上面Add方法反射获取Length或Count属性皆返回0,
value.Property("Length", 0)
,封装的Property非AOT运行正确:

public static T Property<T>(this object obj, string propertyName, T defaultValue = default)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(nameof(propertyName));

    var propertyInfo = obj.GetType().GetProperty(propertyName);
    if (propertyInfo == null)
    {
        return defaultValue;
    }

    var value = propertyInfo.GetValue(obj);

    try
    {
        return (T)Convert.ChangeType(value, typeof(T));
    }
    catch (InvalidCastException)
    {
        return defaultValue;
    }
}

AOT成功:直接通过转换为基类型或实现的接口调用属性即可:

// 数组
var length = ((Array)value).Length;

// List<T>
 if (value is IList list)
{
    var count = list.Count;
}

// Dictionary<key, value>
if (value is IDictionary dictionary)
{
    var count = dictionary.Count;
}

8. Windows 7支持

如遇AOT后无法在
Windows 7
运行,请添加
YY-Thunks
包:

<PackageReference Include="YY-Thunks" Version="1.1.4-Beta3" />

并指定目标框架为
net9.0-windows

9. Winform\兼容XP

如果第8条后还运行不了,请参考上一篇文章《
.NET 9 AOT的突破 - 支持老旧Win7与XP环境 - 码界工坊 (dotnet9.com)
》添加VC-LTL包,这里不赘述。

10. 其他

还有许多其他需要注意的地方,后续想起来逐渐完善本文。

三、总结

AOT 发布测试虽然过程中可能会遇到诸多问题,但通过及时的测试和正确的配置调整,最终能够实现项目的顺利发布。希望以上总结的经验能对大家在 AOT 使用过程中有所帮助,让大家在开发过程中少走弯路,提高项目的开发效率和质量。同时,也期待大家在实践中不断探索和总结,共同推动技术的进步和发展。

AOT可参考项目:

前言

Vue3.5响应式重构主要分为两部分:
双向链表

版本计数
。在上一篇文章中我们讲了
双向链表
,这篇文章我们接着来讲
版本计数

欧阳年底也要毕业了,加入欧阳的面试交流群(分享内推信息)、高质量vue源码交流群

版本计数

看这篇文章之前最好先看一下欧阳之前写的
双向链表
文章,不然有些部分可能看着比较迷茫。

在上篇
双向链表
文章中我们知道了新的响应式模型中主要分为三个部分:
Sub订阅者

Dep依赖

Link节点

  • Sub订阅者
    :主要有watchEffect、watch、render函数、computed等。

  • Dep依赖
    :主要有ref、reactive、computed等响应式变量。

  • Link节点
    :连接
    Sub订阅者

    Dep依赖
    之间的桥梁,
    Sub订阅者
    想访问
    Dep依赖
    只能通过
    Link节点
    ,同样
    Dep依赖
    想访问
    Sub订阅者
    也只能通过
    Link节点

细心的小伙伴可能发现了computed计算属性不仅是
Sub订阅者
还是
Dep依赖

原因是computed可以像
watchEffect
那样监听里面的响应式变量,当响应式变量改变后会触发computed的回调。

还可以将computed的返回值当做ref那样的普通响应式变量去使用,
所以我们才说computed不仅是Sub订阅者还是Dep依赖。

版本计数
中由4个version实现,分别是:全局变量
globalVersion

dep.version

link.version

computed.globalVersion

  • globalVersion
    是一个全局变量,初始值为
    0
    ,仅有响应式变量改变后才会触发
    globalVersion++

  • dep.version
    是在
    dep
    依赖上面的一个属性,初始值是0。当dep依赖是ref这种普通响应式变量,仅有响应式变量改变后才会触发
    dep.version++
    。当computed计算属性作为dep依赖时,只有等computed最终计算出来的值改变后才会触发
    dep.version++

  • link.version
    是Link节点上面的一个属性,初始值是0。每次响应式更新完了后都会保持和
    dep.version
    的值相同。在响应式更新前就是通过
    link.version

    dep.version
    的值是否相同判断是否需要更新。

  • computed.globalVersion
    :计算属性上面的版本,如果
    computed.globalVersion === globalVersion
    说明没有响应式变量改变,计算属性的回调就不需要重新执行。

而版本计数最大的受益者就是computed计算属性,这篇文章接下来我们将以computed举例讲解。

看个例子

我们来看个简单的demo,代码如下:

<template>
  <p>{{ doubleCount }}</p>
  <button @click="flag = !flag">切换flag</button>
  <button @click="count1++">count1++</button>
  <button @click="count2++">count2++</button>
</template>

<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);

const doubleCount = computed(() => {
  console.log("computed");
  if (flag.value) {
    return count1.value * 2;
  } else {
    return count2.value * 2;
  }
});
</script>

在computed中根据
flag.value
的值去决定到底返回
count1.value * 2
还是
count2.value * 2

那么问题来了,当
flag
的值为
true
时,点击
count2++
按钮,
console.log("computed")
会执行打印吗?也就是
doubleCount
的值会重新计算吗?

答案是:
不会
。虽然
count2
也是computed中使用到的响应式变量,但是他不参与返回值的计算,所以改变他不会导致computed重新计算。

有的同学想问为什么能够做到这么精细的控制呢?这就要归功于
版本计数
了,我们接下来会细讲。

依赖触发

还是前面那个demo,初始化时
flag
的值是true,所以在computed中会对
count1
变量进行读操作,然后触发get拦截。
count1
这个ref响应式变量就是由
RefImpl
类new出来的一个对象,代码如下:

class RefImpl {
  dep: Dep = new Dep();
  get value() {
    this.dep.track()
  }
  set value() {
    this.dep.trigger();
  }
}

在get拦截中会执行
this.dep.track()
,其中
dep
是由
Dep
类new出来的对象,代码如下

class Dep {
  version = 0;
  track() {
    let link = new Link(activeSub, this);
    // ...省略
  }
  trigger() {
    this.version++;
    globalVersion++;
    this.notify();
  }
}


track
方法中使用
Link
类new出来一个link对象,
Link
类代码如下:

class Link {
  version: number

  /**
   * Pointers for doubly-linked lists
   */
  nextDep?: Link
  prevDep?: Link
  nextSub?: Link
  prevSub?: Link
  prevActiveLink?: Link

  constructor(
    public sub: Subscriber,
    public dep: Dep,
  ) {
    this.version = dep.version
    this.nextDep =
      this.prevDep =
      this.nextSub =
      this.prevSub =
      this.prevActiveLink =
        undefined
  }
}

这里我们只关注Link中的
version
属性,其他的属性在上一篇双向链表文章中已经讲过了。


constructor
中使用
dep.version

link.version
赋值,保证
dep.version

link.version
的值是相等的,也就是等于0。因为
dep.version
的初始值是0,接着就会讲。

当我们点击
count1++
按钮时会让响应式变量
count1
的值自增。因为
count1
是一个ref响应式变量,所以会触发其set拦截。代码如下:

class RefImpl {
  dep: Dep = new Dep();
  get value() {
    this.dep.track()
  }
  set value() {
    this.dep.trigger();
  }
}

在set拦截中执行的是
this.dep.trigger()

trigger
函数代码如下:

class Dep {
  version = 0;
  track() {
    let link = new Link(activeSub, this);
    // ...省略
  }
  trigger() {
    this.version++;
    globalVersion++;
    this.notify();
  }
}

前面讲过了
globalVersion
是一个全局变量,初始值为0。

dep上面的
version
属性初始值也是0。


trigger
中分别执行了
this.version++

globalVersion++
,这里的this就是指向的dep。执行完后
dep.version

globalVersion
的值就是1了。而此时
link.version
的值依然还是0,这个时候
dep.version

link.version
的值就已经不相等了。

接着就是执行
notify
方法按照新的响应式模型进行通知订阅者进行更新,我们这个例子此时新的响应式模型如下图:
reactive

如果修改的响应式变量会触发多个订阅者,比如
count1
变量被多个
watchEffect
使用,修改
count1
变量的值就需要触发多个订阅者的更新。
notify
方法中正是将多个更新操作放到一个批次中处理,从而提高性能。由于篇幅有限我们就不去细讲
notify
方法的内容,你只需要知道执行
notify
方法就会触发订阅者的更新。

(这两段是
notify
方法内的逻辑)按照正常的逻辑如果
count1
变量的值改变,就可以通过
Link2
节点找到
Sub1
订阅者,然后执行订阅者的
notify
方法从而进行更新。

如果我们的
Sub1
订阅者是render函数,是这个正常的逻辑。但是此时我们的
Sub1
订阅者是计算属性
doubleCount
,这里会有一个优化,如果订阅者是一个计算属性,触发其更新时不会直接执行计算属性的回调函数,而是直接去通知计算属性的订阅者去更新,在更新前才会去执行计算属性的回调函数(这个接下来的文章会讲)。代码如下:

if (link.sub.notify()) {
  // if notify() returns `true`, this is a computed. Also call notify
  // on its dep - it's called here instead of inside computed's notify
  // in order to reduce call stack depth.
  link.sub.dep.notify()
}

link.sub.notify()
的执行结果是true就代表当前的订阅者是计算属性,然后就会触发计算属性“作为依赖”时对应的订阅者。我们这里的计算属性
doubleCount
是在template中使用,所以计算属性
doubleCount
的订阅者就是render函数。

所以这里就是调用
link.sub.notify()
不会触发计算属性
doubleCount
中的回调函数重新执行,而是去触发计算属性
doubleCount
的订阅者,也就是render函数。在执行render函数之前会再去通过
脏检查
(依靠版本计数实现)去判断是否需要重新执行计算属性的回调,如果需要执行计算属性的回调那么就去执行render函数重新渲染。

脏检查

所有的
Sub订阅者
内部都是基于
ReactiveEffect
类去实现的,调用订阅者的
notify
方法通知更新实际底层就是在调用
ReactiveEffect
类中的
runIfDirty
方法。代码如下:

class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions {
  /**
   * @internal
   */
  runIfDirty(): void {
    if (isDirty(this)) {
      this.run();
    }
  }
}


runIfDirty
方法中首先会调用
isDirty
方法判断当前是否需要更新,如果返回true,那么就执行
run
方法去执行Sub订阅者的回调函数进行更新。如果是
computed

watch

watchEffect
等订阅者调用run方法就会执行其回调函数,如果是render函数这种订阅者调用run方法就会再次执行render函数。

调用
isDirty
方法时传入的是this,值得注意的是this是指向
ReactiveEffect
实例。而
ReactiveEffect
又是继承自
Subscriber
订阅者,所以这里的this是指向的是订阅者。

前面我们讲过了,修改响应式变量
count1
的值时会通知
作为订阅者

doubleCount
计算属性。当通知
作为订阅者
的计算属性更新时不会去像watchEffect这样的订阅者一样去执行其回调,而是去通知计算属性
作为Dep依赖
时订阅他的订阅者进行更新。在这里计算属性
doubleCount
是在template中使用,所以他的订阅者是render函数。

所以修改count1变量执行runIfDirty时此时触发的订阅者是作为Sub订阅者的render函数,也就是说此时的this是render函数!!

我们来看看
isDirty
是如何进行脏检查,代码如下:

function isDirty(sub: Subscriber): boolean {
  for (let link = sub.deps; link; link = link.nextDep) {
    if (
      link.dep.version !== link.version ||
      (link.dep.computed &&
        (refreshComputed(link.dep.computed) ||
          link.dep.version !== link.version))
    ) {
      return true;
    }
  }
  return false;
}

这里就涉及到我们上一节讲过的双向链表了,回顾一下前面讲过的响应式模型图,如下图:
reactive
此时的sub订阅者是render函数,也就是图中的
Sub2

sub.deps
是指向指向
Sub2
订阅者X轴(横向)上面的Link节点组成的队列的头部,
link.nextDep
就是指向X轴上面下一个Link节点,通过Link节点就可以访问到对应的Dep依赖。

在这里render函数对应的订阅者
Sub2
在X轴上面只有一个节点
Link3

这里的for循环就是去便利Sub订阅者在X轴上面的所有Link节点,然后在for循环内部去通过Link节点访问到对应的Dep依赖去做版本计数的判断。

这里的for循环内部的if语句判断主要分为两部分:

 if (
  link.dep.version !== link.version ||
  (link.dep.computed &&
    (refreshComputed(link.dep.computed) ||
      link.dep.version !== link.version))
) {
  return true;
}

这两部分中只要有一个是true,那么就说明当前Sub订阅者需要更新,也就是执行其回调。

我们来看看第一个判断:

link.dep.version !== link.version

还记得我们前面讲过吗,初始化时会保持
dep.version

link.version
的值相同。每次响应式变量改变时走到set拦截中,在拦截中会去执行
dep.version++
,执行完了后此时
dep.version

link.version
的值就已经不相同了,在这里就能知道此时响应式变量改变过了,需要通知Sub订阅者更新执行其回调。

常规情况下Dep依赖是一个ref变量、Sub订阅者是wachEffect这种确实第一个判断就可以满足了。

但是我们这里的
link.dep
是计算属性
doubleCount
,计算属性是由
ComputedRefImpl
类new出来的对象,简化后代码如下:

class ComputedRefImpl<T = any> implements Subscriber {
  _value: any = undefined;
  readonly dep: Dep = new Dep(this);
  globalVersion: number = globalVersion - 1;
  get value(): T {
    // ...省略
  }
  set value(newValue) {
    // ...省略
  }
}

ComputedRefImpl
继承了
Subscriber
类,所以说他是一个订阅者。同时还有get和set拦截,以及初始化一个计算属性时也会去new一个对应的Dep依赖。

还有一点值得注意的是计算属性上面的
computed.globalVersion
属性初始值为
globalVersion - 1
,默认是不等于
globalVersion
的,这是为了第一次执行计算属性时能够去触发执行计算属性的回调,这个在后面的
refreshComputed
函数中会讲。

我们是直接修改的
count1
变量,在
count1
变量的set拦截中触发了
dep.version++
,但是并没有修改计算属性对应的
dep.version
。所以当计算属性作为依赖时单纯的使用
link.dep.version !== link.version
就不能满足需求了,需要使用到第二个判断:

(link.dep.computed &&
    (refreshComputed(link.dep.computed) ||
      link.dep.version !== link.version))

在第二个判断中首先判断当前当前的Dep依赖是不是计算属性,如果是就调用
refreshComputed
函数去执行计算属性的回调。然后判断计算属性的结果是否改变,如果改变了在
refreshComputed
函数中就会去执行
link.dep.version++
,所以执行完
refreshComputed
函数后
link.dep.version

link.version
的值就不相同了,表示计算属性的值更新了,当然就需要执行依赖计算属性的render函数啦。

refreshComputed函数

我们来看看
refreshComputed
函数的代码,简化后的代码如下:

function refreshComputed(computed: ComputedRefImpl): undefined {
  if (computed.globalVersion === globalVersion) {
    return;
  }
  computed.globalVersion = globalVersion;

  const dep = computed.dep;
  try {
    prepareDeps(computed);
    const value = computed.fn(computed._value);
    if (dep.version === 0 || hasChanged(value, computed._value)) {
      computed._value = value;
      dep.version++;
    }
  } catch (err) {
    dep.version++;
    throw err;
  } finally {
    cleanupDeps(computed);
  }
}

首先会去判断
computed.globalVersion === globalVersion
是否相等,如果相等就说明根本就没有响应式变量改变,那么当然就无需去重新执行计算属性回调。

还记得我们前面讲过每当响应式变量改变后触发set拦截是都会执行
globalVersion++
吗?所以这里就可以通过
computed.globalVersion === globalVersion
判断是否有响应式变量改变,如果没有说明计算属性的值肯定就没有改变。

接着就是执行
computed.globalVersion = globalVersion

computed.globalVersion
的值同步为
globalVersion
,为了下次判断是否需要重新执行计算属性做准备。

在try中会先去执行
prepareDeps
函数,这个先放放接下来讲,先来看看try中其他的代码。

首先调用
const value = computed.fn(computed._value)
去重新执行计算属性的回调函数拿到计算属性新的返回值
value

接着就是执行
if (dep.version === 0 || hasChanged(value, computed._value))

我们前面讲过了dep上面的version默认值为0,这里的
dep.version === 0
说明是第一次渲染计算属性。接着就是使用
hasChanged(value, computed._value)
判断计算属性新的值和旧的值相比较是否有修改。

上面这两个条件满足一个就执行if里面的内容,将新得到的计算属性的值更新上去,并且执行
dep.version++
。因为前面讲过了在外面会使用
link.dep.version !== link.version
判断dep的版本是否和link上面的版本是否相同,如果不相等就执行render函数。

这里由于计算属性的值确实改变了,所以会执行
dep.version++
,dep的版本和link上面的版本此时就不同了,所以就会被标记为dirty,从而执行render函数。

如果执行计算属性的回调函数出错了,同样也执行一次
dep.version++

最后就是剩余执行计算属性回调函数之前调用的
prepareDeps
和finally调用的
cleanupDeps
函数没讲了。

更新响应式模型

回顾一下demo的代码:

<template>
  <p>{{ doubleCount }}</p>
  <button @click="flag = !flag">切换flag</button>
  <button @click="count1++">count1++</button>
  <button @click="count2++">count2++</button>
</template>

<script setup>
import { computed, ref } from "vue";
const count1 = ref(1);
const count2 = ref(10);
const flag = ref(true);

const doubleCount = computed(() => {
  console.log("computed");
  if (flag.value) {
    return count1.value * 2;
  } else {
    return count2.value * 2;
  }
});
</script>


flag
的值为true时,对应的响应式模型前面我们已经讲过了,如下图:
reactive

如果我们将
flag
的值设置为false呢?此时的计算属性
doubleCount
就不再依赖于响应式变量
count1
,而是依赖于响应式变量
count2
。小伙伴们猜猜此时的响应式模型应该是什么样的呢?
reactive2

现在多了一个
count2
变量对应的
Link4
,原本
Link1

Link2
之间的连接也因为计算属性不再依赖于
count1
变量后,他们俩之间的连接也没有了,转而变成了
Link1

Link4
之间建立连接。

前面没有讲的
prepareDeps

cleanupDeps
函数就是去掉
Link1

Link2
之间的连接。

prepareDeps
函数代码如下:

function prepareDeps(sub: Subscriber) {
  // Prepare deps for tracking, starting from the head
  for (let link = sub.deps; link; link = link.nextDep) {
    // set all previous deps' (if any) version to -1 so that we can track
    // which ones are unused after the run
    link.version = -1
    // store previous active sub if link was being used in another context
    link.prevActiveLink = link.dep.activeLink
    link.dep.activeLink = link
  }
}

这里使用for循环遍历计算属性Sub1在X轴上面的Link节点,也就是Link1和Link2,并且将这些Link节点的
version
属性设置为-1。


flag
的值设置为false后,重新执行计算属性
doubleCount
中的回调函数时,就会对回调函数中的所有响应式变量进行读操作。从而再次触发响应式变量的get拦截,然后执行
track
方法进行依赖收集。注意此时新收集了一个响应式变量
count2
。收集完成后响应式模型图如下图:
reactive3

从上图中可以看到虽然计算属性虽然不再依赖
count1
变量,但是
count1
变量变量对应的
Link2
节点还在队列的连接上。

我们在
prepareDeps
方法中将计算属性依赖的所有Link节点的version属性都设置为-1,在
track
方法收集依赖时会执行这样一行代码,如下:

class Dep {
  track() {
    if (link === undefined || link.sub !== activeSub) {
      // ...省略
    } else if (link.version === -1) {
      link.version = this.version;
      // ...省略
    }
  }
}

如果
link.version === -1
,那么就将
link.version
的值同步为
dep.version
的值。

只有计算属性最新依赖的响应式变量才会触发
track
方法进行依赖收集,从而将对应的
link.version

-1
更新为
dep.version

而变量
count1
现在已经不会触发
track
方法了,所以变量
count1
对应的
link.version
的值还是
-1

最后就是执行
cleanupDeps
函数将
link.version
的值还是-1的响应式变量(也就是不再使用的
count1
变量)对应的Link节点,从双向链表中给干掉。代码如下:

function cleanupDeps(sub: Subscriber) {
  // Cleanup unsued deps
  let head;
  let tail = sub.depsTail;
  let link = tail;
  while (link) {
    const prev = link.prevDep;
    if (link.version === -1) {
      if (link === tail) tail = prev;
      // unused - remove it from the dep's subscribing effect list
      removeSub(link);
      // also remove it from this effect's dep list
      removeDep(link);
    } else {
      // The new head is the last node seen which wasn't removed
      // from the doubly-linked list
      head = link;
    }

    // restore previous active link if any
    link.dep.activeLink = link.prevActiveLink;
    link.prevActiveLink = undefined;
    link = prev;
  }
  // set the new head & tail
  sub.deps = head;
  sub.depsTail = tail;
}

遍历Sub1计算属性横向队列(X轴)上面的Link节点,当
link.version === -1
时,说明这个Link节点对应的Dep依赖已经不被计算属性所依赖了,所以执行
removeSub

removeDep
将其从双向链表中移除。

执行完
cleanupDeps
函数后此时的响应式模型就是我们前面所提到的样子,如下图:
reactive2

总结

版本计数主要有四个版本:全局变量
globalVersion

dep.version

link.version

computed.globalVersion

dep.version

link.version
如果不相等就说明当前响应式变量的值改变了,就需要让Sub订阅者进行更新。

如果是计算属性作为Dep依赖时就不能通过
dep.version

link.version
去判断了,而是执行
refreshComputed
函数进行判断。在
refreshComputed
函数中首先会判断
globalVersion

computed.globalVersion
是否相等,如果相等就说明并没有响应式变量更新。如果不相等那么就会执行计算属性的回调函数,拿到最新的值后去比较计算属性的值是否改变。并且还会执行
prepareDeps

cleanupDeps
函数将那些计算属性不再依赖的响应式变量对应的Link节点从双向链表中移除。

最后说一句,版本计数最大的赢家应该是computed计算属性,虽然引入版本计数后代码更难理解了。但是整体流程更加优雅,以及现在只需要通过判断几个version是否相等就能知道订阅者是否需要更新,性能当然也更好了。

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

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

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

论文: Agglomerative Token Clustering

创新点


  • 提出了层次
    token
    聚类(
    Agglomerative Token Clustering

    ATC
    ),这是一种新型的无参数层次合并的
    token
    减少方法。
  • 基于
    ATC
    ,在图像分类、图像合成,以及目标检测和分割任务上实现了最先进的性能,超越了所有其他
    token
    减少方法,包括基于合并的和基于修剪的
    token
    减少方法。
  • 在图像分类和目标检测与分割任务中,
    ATC
    可以在未经过任何微调的情况下(即开箱即用),达到与之前微调的最先进性能相当的效果。

内容概述


层次
token
聚类(
Agglomerative Token Clustering
,简称
ATC
)是一种新型的
token
合并方法,在图像分类、图像合成以及目标检测与分割任务中始终优于以往的
token
合并和修剪方法。
ATC
通过自下而上的层次聚类来合并簇,而无需引入额外的可学习参数。

在所有任务中,
ATC
都实现了最先进的性能。在不进行微调的情况下,甚至可以与之前的最先进技术相媲美。
ATC
在低保留率下尤其有效,此场景仅保留了少量的
token
,而保持任务性能尤其困难。

层次
token
聚类


与之前的
token
合并方法类似,
ATC
的目标是合并冗余
token
,同时保持或提升
ViT
模型的性能。在
ViT
块的自注意力和多层感知机(
MLP
)模块之间插入
token
合并操作,这与之前的基于合并的方法是一致的,比如
ToMe

层次聚类是一种经典的自下而上的层次聚类方法,其中每个元素最初都是其自身的聚类。通过根据某种连结函数和距离度量
\(D(\cdot)\)
迭代比较聚类,将两个最接近的聚类在每次迭代中合并。这一过程会持续进行,直到满足某个停止标准,例如所需聚类的数量(形成静态缩减方法),或者聚类之间的最小距离(形成动态缩减方法)。

论文考虑静态缩减场景,使用余弦距离作为距离度量
\(D(\cdot)\)
,并使用自注意力模块的键作为
token
特征。连结函数的选择对元素的聚类方式会有很大影响,主要有三种最常见的连结函数:单个,完整和平均。

\[\begin{equation}
D(I,J)^{\text{single}} = \min_{i\in I,\ j\in J} D(i,j)
\end{equation}
\]

\[\begin{equation}
D(I,J)^{\text{complete}} = \max_{i\in I,\ j\in J} D(i,j)
\end{equation}
\]

\[\begin{equation}
D(I,J)^{\text{average}} = \frac{1}{|I||J|}\sum_{i\in I}\sum_{j\in J}D(i,j)
\end{equation}
\]

其中
\(I\)

\(J\)
是包含元素
\(i \in I\)

\(j \in J\)
的聚类。

在达到停止标准之后,对每个聚类中的
token
进行平均,以获得更新的聚类表示。然而,随着
token
的合并,它们代表的不止一个输入图像块。为了更好地利用能够捕捉更大空间范围的
token
,使用加权平均作为聚类表示,并在自注意力模块中使用成比例的注意力。

主要实验




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

work-life balance.

想要导出这样的表格

数据准备格式

附上源码

1 usingNPOI.HSSF.UserModel;2 usingNPOI.SS.UserModel;3 usingNPOI.SS.Util;4 usingSystem.Data;5 usingSystem.Text.RegularExpressions;6 
7 namespaceTestConsoleApp8 {9     /// <summary>
10     ///导出Excel11     /// </summary>
12     public static classExportHelper13 {14         public static voidExport()15 {16             var dt =CreteTable();17             var titles = GetExcelTitles(dt.Columns, out intmaxTitleLevel);18 
19             HSSFWorkbook workbook = newHSSFWorkbook();20             ISheet sheet = workbook.CreateSheet("Sheet1");21 
22             var allRowCount = dt.Rows.Count +maxTitleLevel;23             //创建所有单元格
24             for (int i = 0; i < allRowCount; i++)25 {26                 var row =sheet.CreateRow(i);27                 for (int j = 0; j < dt.Columns.Count; j++)28 {29 row.CreateCell(j);30 }31 }32 
33             //合并创建表头
34             foreach (var tit intitles)35 {36 sheet.GetRow(tit.StartRow).GetCell(tit.StartColumn).SetCellValue(tit.Title);37                 if (tit.MergeColumnCount + tit.MergeRowCount > 0)38 {39                     sheet.AddMergedRegion(new CellRangeAddress(tit.StartRow, tit.StartRow + tit.MergeRowCount, tit.StartColumn, tit.StartColumn +tit.MergeColumnCount));40 }41 }42 
43             //生成数据行
44             for (int i = 0; i < dt.Rows.Count; i++)45 {46                 for (int j = 0; j < dt.Columns.Count; j++)47 {48                     string cellValue =dt.Rows[i][j].ToString();49                    sheet.GetRow(maxTitleLevel +i).Cells[j].SetCellValue(cellValue);50 }51 }52 
53             using FileStream stm = File.OpenWrite(@"D:\Drivers\Merge.xls");54 workbook.Write(stm);55 }56 
57         private staticDataTable CreteTable()58 {59             DataTable dt = newDataTable();60             dt.Columns.Add("编号");61             dt.Columns.Add("收入-线上采购-数量");62             dt.Columns.Add("收入-线上采购-金额");63 
64             dt.Columns.Add("收入-线下采购-数量");65             dt.Columns.Add("收入-线下采购-金额");66 
67             dt.Columns.Add("回收-数量");68             dt.Columns.Add("回收-金额");69 
70             dt.Columns.Add("支出-测试01-数量");71             dt.Columns.Add("支出-测试01-金额");72 
73             dt.Columns.Add("支出-测试02-数量");74             dt.Columns.Add("支出-测试02-金额");75 
76             dt.Columns.Add("其它-数量");77             dt.Columns.Add("其它-金额");78 
79             dt.Columns.Add("备注");80 
81             for (int i = 1; i <= 100; i++)82 {83                 var row =dt.NewRow();84 
85                 row["编号"] = "编号" +i;86                 row["收入-线上采购-数量"] =i;87                 row["收入-线上采购-金额"] =i;88                 row["收入-线下采购-数量"] =i;89                 row["收入-线下采购-金额"] =i;90                 row["回收-数量"] =i;91                 row["回收-金额"] =i;92                 row["支出-测试01-数量"] =i;93                 row["支出-测试01-金额"] =i;94                 row["支出-测试02-数量"] =i;95                 row["支出-测试02-金额"] =i;96                 row["其它-数量"] =i;97                 row["其它-金额"] =i;98                 row["备注"] =i;99 dt.Rows.Add(row);100 }101 
102             returndt;103 }104 
105 
106         private static List<ExcelTitle> GetExcelTitles(DataColumnCollection columns, out intmaxTitleLevel)107 {108             maxTitleLevel = 0;109             List<LevelExcelTitle> levelExcelTitles = new List<LevelExcelTitle>();110 
111             for (var index = 0; index < columns.Count; index++)112 {113                 var column =columns[index].ToString();114 
115                 var arr = column.Split("-");116 
117 
118                 if (maxTitleLevel <arr.Length)119 {120                     maxTitleLevel =arr.Length;121 }122 
123                 for (int i = 0; i < arr.Length; i++)124 {125                     levelExcelTitles.Add(newLevelExcelTitle()126 {127                         Title =arr[i],128                         LevelCode = string.Join("-", arr[..(i + 1)]),129                         RowIndex =i,130                         ColumnIndex =index,131                         TotalLevel =arr.Length132 });133 }134 }135 
136             var titleLevel =maxTitleLevel;137             var excelTitles =levelExcelTitles138                 .GroupBy(b => new
139 {140 b.LevelCode,141 b.Title142 })143                .Select(b => newExcelTitle()144 {145                    Title =b.Key.Title,146                    StartRow = b.Min(c =>c.RowIndex),147                    MergeRowCount = b.Min(c => c.RowIndex) + 1 == b.Max(c => c.TotalLevel) ? titleLevel - b.Max(c => c.TotalLevel) : 0,148 
149                    StartColumn = b.Min(c =>c.ColumnIndex),150                    MergeColumnCount = b.Count() - 1,//排除自身
151 }).ToList();152 
153             returnexcelTitles;154 }155 }156 
157     public classExcelTitle158 {159         /// <summary>
160         ///标题161         /// </summary>
162         public string Title { get; set; }163 
164         /// <summary>
165         ///开始行166         /// </summary>
167         public int StartRow { get; set; }168 
169         /// <summary>
170         ///合并行171         /// </summary>
172         public int MergeRowCount { get; set; }173 
174 
175         /// <summary>
176         ///开始列177         /// </summary>
178         public int StartColumn { get; set; }179 
180         /// <summary>
181         ///合并列182         /// </summary>
183         public int MergeColumnCount { get; set; }184 }185 
186     public classLevelExcelTitle187 {188         /// <summary>
189         ///标题190         /// </summary>
191         public string Title { get; set; }192 
193         public string LevelCode { get; set; }194 
195         /// <summary>
196         ///第几行197         /// </summary>
198         public int RowIndex { get; set; }199 
200         /// <summary>
201         ///第几列202         /// </summary>
203         public int ColumnIndex { get; set; }204 
205         /// <summary>
206         ///总层207         /// </summary>
208         public int TotalLevel { get; set; }209 }210 }

Manim
提供了一系列专为三维空间设计的对象,让创建三维数学动画变得更加轻松。

本篇开始介绍其中最简单的点和线相关对象,也就是
Dot3D
(三维的点),
Line3D
(三维的线)和
Arrow3D
(三维的箭头)。

  • Dot3D
    用于表示三维空间中的点,是构建其他复杂三维图形的基础,它适用于标记关键点、位置、向量起点等。
  • Line3D
    用于在三维空间中绘制线段,可以表示向量、路径、轨迹等。它适用于展示物体的运动轨迹、力的方向等。
  • Arrow3D

    Line3D
    的基础上添加了箭头,用于明确表示方向性的线段。它适用于表示矢量、力的方向、速度等。

1. 主要参数

Dot3D
的主要参数:

参数名称 类型 说明
point np.ndarray 点的位置,包含 x,y,z 3个值
radius float 点的半径
color Color 点的颜色
resolution tuple[int, int] 点的分辨率

Dot3D
本质上是个球形,所以有
radius
参数,控制点的大小;

resolution
参数设置的是点这个球形的分辨率,值越大,点看上去越圆滑,一般不需要设置这个参数,默认值就可以了。

Line3D
的主要参数:

参数名称 类型 说明
start np.ndarray 线段的起点坐标,包含 x,y,z 3个值
end np.ndarray 线段的终点坐标,包含 x,y,z 3个值
thickness float 线段的粗细
color Color 线段的颜色

Arrow3D
的主要参数:

参数名称 类型 说明
start np.ndarray 箭头的起点坐标,包含 x,y,z 3个值
end np.ndarray 箭头的终点坐标,包含 x,y,z 3个值
thickness float 箭头主体的粗细
height float 箭头尖部的高度
base_radius float 箭头尖部的底部半径
color Color 箭头的颜色

2. 主要方法

Dot3D

Arrow3D
没有什么特殊的方法,就是一些Manim对象常规的缩放,平移等方法。

但是,
Line3D
有几个自己特有的方法。

名称 说明
parallel_to 创建一条与给定线段平行的线段
perpendicular_to 创建一条与给定线段垂直的线段
pointify 获取表示对象中心的点

3. 使用示例

最后,还是通过示例来演示上面几个对象的重要参数和方法。

3.1. Dot3D的位置和颜色

在这个示例中,我们将创建个
Dot3D
对象。

第一个点具有默认的半径和颜色;而第二个点,我们将自定义其半径和颜色。

通过调整这些参数,我们可以清晰地看到两个点在三维空间中的位置和颜色差异。

# 使用默认参数
dot1 = Dot3D(point=axes.c2p(1, 2, 3))

# 自定义半径和颜色
dot2 = Dot3D(
    point=axes.c2p(2, 3, -1),
    radius=0.2,
    color=RED,
)

3.2. Line3D的长度和方向

此示例将展示如何使用Line3D对象在三维空间中绘制一条线段。

首先根据起点和终点设置一个线段,同时,调整线段的粗细和颜色来使其更加醒目。

然后,通过函数
parallel_to

perpendicular_to
分别绘制此线段的平行线(黄色)和垂线(红色)。

# 指定起点和终点 以及线段粗细
line = Line3D(
    start=LEFT + DOWN * 2 + IN,
    end=RIGHT + UP * 2 + OUT * 2,
    thickness=0.02,
    color=BLUE,
)

# 绘制line的平行线
Line3D.parallel_to(line, color=YELLOW)

# 绘制line的垂线
Line3D.perpendicular_to(line, color=RED)

3.3. Arrow3D的方向和样式

在这个示例中,我们使用
Arrow3D
对象来表示一个带箭头的线段,以强调方向性。

首先设置箭头的起点和终点,为了突出箭头,

再调整其粗细(
thickness
参数)、箭头尖部的高度(
height
参数)和底部半径(
base_radius
参数)。

# 指定起点和终点 以及箭头样式参数
arrow = Arrow3D(
    start=[-1, -1, -2],
    end=[1, 2, 2],
    thickness=0.05,
    height=0.2,
    base_radius=0.1,
    color=GREEN,
)

3.4. 综合使用Dot3D, Line3D和Arrow3D

在这个综合示例中,我们结合使用
Dot3D

Line3D

Arrow3D
来创建一个更复杂的三维场景。

首先,在场景中放置一个
Dot3D
对象作为起点。

然后,我们将使用
Line3D
对象从该点绘制一条线段到另一个位置,表示路径或轨迹。

最后,我们将在线段的终点处添加一个
Arrow3D
对象。

通过调整这些对象的参数(如位置、颜色、粗细等),可以创建一个既清晰又富有表现力的三维图形,用于演示、教学或科学研究等多种目的。

# 创建Dot3D对象作为起点
dot = Dot3D(point=[-1, -1, -1], color=BLUE)

# 创建Line3D对象连接起点和另一个点
line = Line3D(
    start=[-1, -1, -1],
    end=[2, 2, 2],
    thickness=0.05,
    color=YELLOW,
)

# 创建Arrow3D对象在终点处指示方向
arrow = Arrow3D(
    start=[2, 2, 2],
    end=[1, 2, -1],
    thickness=0.05,
    height=0.2,
    base_radius=0.1,
    color=RED,
)

4. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
dot_line.py
),

下载地址:
完整代码
(访问密码: 6872)