Alien-Signals 响应式系统

基础使用

  • signal 一个包含 getter setter 的对象,在执行 getter setter 的时候会触发额外的逻辑
  • computed 和 vue 中的概念一样
  • effect 副作用函数,当 signal setter 执行后,effect 函数会自动再执行一次
  • effectScope 副作用函数作用域,可以使该作用域内的 effect 失效
  • Batch 批处理,对要执行的 effect 函数进行去重处理
scss 复制代码
import { signal, computed, effect } from 'alien-signals';

const count = signal(1);
const doubleCount = computed(() => count() * 2);

effect(() => {
  console.log(`Count is: ${count()}`);
}); // Console: Count is: 1

console.log(doubleCount()); // 2

count(2); // Console: Count is: 2

console.log(doubleCount()); // 4
scss 复制代码
import { signal, effect, effectScope } from 'alien-signals';

const count = signal(1);

const stopScope = effectScope(() => {
  effect(() => {
    console.log(`Count in scope: ${count()}`);
  }); // Console: Count in scope: 1
});

count(2); // Console: Count in scope: 2

stopScope();

count(3); // No console output

底层原理

推拉混合模型

  • Push Model - 数据变化时立即通知依赖项(高效但可能过度通知)
  • Pull Model - 需要时才计算最新值(精确但可能延迟通知)
  • Push-Pull Hybrid Model- 用 Push 方式通知可能变了,用 Pull 方式按需计算 "具体值"

Push 阶段的工作:

  1. 标记为脏:Signal将自己标记为 Dirty
  2. 传播通知:通过 propagate() 递归通知所有依赖的 Computed 和 Effect
  3. 标记为待处理:将依赖项标记为 Pending
  4. 加入队列:将 Effect 加入执行队列 queuedEffects

Pull 阶段的工作:

  1. 惰性检查:只有当 Computed 被读取时,才检查自己是否为脏
  2. 按需计算:只有确实为脏才执行计算函数
  3. 建立依赖:计算过程中读取其他 Signal,建立新的依赖关系
  4. 缓存结果:计算完成后缓存结果,清除脏标记

双向链表结构动态更新订阅依赖关系

  • 依赖链表: 从订阅者角度,指向它依赖的所有节点
  • 订阅链表:从被依赖者角度,指向所有订阅它的节点
perl 复制代码
// 定义响应式节点的接口
export interface ReactiveNode {
  deps?: Link;        // 该节点所依赖的其他节点的链表头(作为依赖者,指向其依赖项)
  depsTail?: Link;    // 依赖链表的尾部,用于高效追加
  subs?: Link;        // 订阅该节点的其他节点的链表头(作为被依赖者,指向其订阅者)
  subsTail?: Link;    // 订阅链表的尾部,用于高效追加
  flags: ReactiveFlags; // 节点的状态标志位
}

interface Link {
    version: number;
    dep: ReactiveNode;  // 被依赖的节点(如 Signal)
    sub: ReactiveNode;  // 订阅的节点(如 Effect/Computed)
    prevDep: Link | undefined; // 前一个同一 sub 的依赖
    nextDep: Link | undefined; // 后一个同一 sub 的依赖
    prevSub: Link | undefined; // 前一个同一 dep 的订阅
    nextSub: Link | undefined; // 后一个同一 dep 的订阅
}
  1. 通过版本号来优化连接管理

  • 自动清理过期依赖
  • 支持条件依赖
  • 内存安全
perl 复制代码
let currentVersion = 0;

function startTracking(sub: ReactiveNode): void {
    ++currentVersion; // 每次追踪开始时版本号递增
    sub.depsTail = undefined;
    // ...
}

function endTracking(sub: ReactiveNode): void {
    // 移除所有版本号不匹配的旧链接
    let toRemove = sub.depsTail !== undefined ? sub.depsTail.nextDep : sub.deps;
    while (toRemove !== undefined) {
        toRemove = unlink(toRemove, sub);
    }
}
  1. 依赖关系链表的动态更新过程

示例1:一个 effect 依赖了两个 signal

yaml 复制代码
const signalA = signal('A');
const signalB = signal('B');
effect(() => {
    console.log(signalA());
    console.log(signalB());
);

// 依赖关系
SignalA: { subs: Link1, subsTail: Link1 }
SignalB: { subs: Link2, subsTail: Link2 }
Effect1: { deps: Link1 → Link2, depsTail: Link2 }

Link1: { 
    dep: SignalA, 
    sub: Effect1,
    prevDep: undefined,
    nextDep: undefined,
    prevSub: undefined,
    nextSub: undefined
}
Link2: { 
    dep: SignalB, 
    sub: Effect1,
    prevDep: Link1,
    nextDep: undefined,
    prevSub: undefined,
    nextSub: undefined
}

示例2:两个 effect 依赖了同一个 signal

yaml 复制代码
const signalA = signal('A');
const signalB = signal('B');
effect(() => {
    console.log(signalA());
    console.log(signalB());
);
effect(() => {
    console.log(signalA());
})

// 依赖关系
SignalA: { subs: Link1 -> Link3, subsTail: Link3 }
SignalB: { subs: Link2, subsTail: Link2 }
Effect1: { deps: Link1 → Link2, depsTail: Link2 }
Effect2: { deps: Link3, depsTail: Link3 }

Link1: { 
    dep: SignalA, 
    sub: Effect1,
    prevDep: undefined,
    nextDep: undefined,
    prevSub: undefined,
    nextSub: Link3
}
Link2: { 
    dep: SignalB, 
    sub: Effect1,
    prevDep: Link1,
    nextDep: undefined,
    prevSub: undefined,
    nextSub: undefined
}
Link3: {
    dep: SignalA,
    sub: Effect2, 
    prevSub: Link1,
    nextSub: undefined
}

vue 响应式系统更新

Vue 基于 Proxy/Reflect 的响应式系统是纯粹的推送模型

javascript 复制代码
const reactive = (target) => {
    return new Proxy(target, {
        get(target, key, receiver) {
            track(target, key); // 收集当前正在运行的 effect
            return Reflect.get(target, key, receiver);
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);
            trigger(target, key); // 立即触发所有依赖的 effects
            return result;
        }
    });
};
  1. 过度通知问题
arduino 复制代码
const config = reactive({
    theme: 'dark',
    language: 'en',
    // ... 很多其他配置
});

effect(() => {
    // 只使用 theme
    applyTheme(config.theme);
});

// 修改 language 也会触发 effect!
config.language = 'zh'; // 不必要的重新执行
  1. 条件依赖的浪费
scss 复制代码
const state = reactive({
    showMessage: false,
    message: 'Hello'
});

effect(() => {
    if (state.showMessage) {
        // 只有当 showMessage 为 true 时才需要
        showPopup(state.message);
    }
});

// 即使 showMessage 为 false,修改 message 也会触发 effect
state.message = 'Hi'; // 浪费的计算
  1. 深层嵌套的性能问题
scss 复制代码
const largeData = reactive({
    items: Array(1000).fill().map((_, i) => ({ id: i, value: i }))
});

effect(() => {
    // 只使用第一个元素
    console.log(largeData.items[0].value);
});

// 修改最后一个元素也会触发整个 effect
largeData.items[999].value = 1000; // 过度触发

在 vue3.6 alpha 中响应式系统底层实现已经从原来的 proxy / reflect 迁移到 alien-signal,api 使用上和原来保持一致

Vue3.6 响应式系统: github.com/vuejs/core/...

Alien-Signal: github.com/stackblitz/...

相关推荐
DLF_ou3 小时前
vue3+openlayers项目初始化
vue.js
你单排吧3 小时前
Electron打包图标修改失败问题
前端
@AfeiyuO3 小时前
vue3 实现将页面生成 pdf 导出(html2Canvas + jspdf)
前端·pdf·vue
华仔啊3 小时前
Vue 的 DOM 更新竟然是异步的?90%的人没有搞懂 nextTick
前端·vue.js
Cyann3 小时前
Day1- React基础组件使用
前端·react.js
GISer_Jing3 小时前
Next系统学习(二)
前端·javascript·node.js
BillKu4 小时前
vue3 中 npm install mammoth 与 npm install --save mammoth 的主要区别说明
前端·npm·node.js
Ankle4 小时前
vue3 父子组件v-model传值方法总结
前端·vue.js
Liquidliang4 小时前
用Claude Code构建AI创意工作流:连接nano banana与veo3
前端·aigc