个人对于sign的理解

理解 Signal:下一代前端响应式核心机制

个人对于sign的理解

一、为什么会有 Signal?

在前端响应式编程的发展历史中,我们经历了多个阶段:

  • MVC 双向绑定(AngularJS)
  • 虚拟 DOM + Diff(React、Vue2)
  • Proxy 响应式(Vue3)
  • Hooks 状态管理(React 16+)
  • Vue3.6+ 中的 alien-signal

虽然这些方案都解决了"数据驱动视图"的核心问题,但它们普遍存在渲染粒度粗 的问题:

一个状态变化,可能会导致整个组件或子树重新渲染,即使只有一个小部分依赖它。

Signal 的出现,就是为了让状态更新的粒度更细、更直接------只更新依赖它的那部分视图


二、什么是 Signal?

一句话定义:

Signal 是一个可订阅的值容器,当值发生变化时,会通知所有依赖它的计算或副作用函数更新。

在现代框架中,Signal 通常分为三类:

  1. Writable Signal
    可写的信号,存储可变状态
  2. Computed Signal
    由其他信号派生的只读信号
  3. Effect
    副作用函数,当依赖的信号变化时执行

简单例子:

js 复制代码
import { signal, computed, effect } from 'some-signal-lib';

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

effect(() => {
  console.log("Count:", count(), "Double:", doubleCount());
});

count.set(1); // 自动触发 effect

三、Signal 与 Vue3 响应式的底层对比

特性 Vue3 reactive/ref Signal
实现方式 Proxy / getter getter + 订阅列表(Set)
依赖追踪方式 track / trigger(WeakMap) 显式注册到 subscribers
更新粒度 属性级别 值级别(更细)
调度机制 全局 effect 栈 + Map 存储 Signal 对象内部直接存储订阅者
应用场景 通用响应式系统 高性能组件更新 / 精细化状态管理

Vue3 的响应式是全局化的依赖管理,Signal 则更像是局部响应式容器,订阅和触发都局限在这个对象本身。

四、Signal 的实现原理

Signal 内部一般维护两个东西:

  • value:当前信号值
  • subscribers:依赖它的副作用函数集合(Set 去重)

4.1 依赖收集

当读取信号值时,如果存在当前活跃的副作用函数 ,就将它加入 subscribers

4.2 更新通知

当信号值变化时,循环执行 subscribers 中的副作用函数。

flowchart TD A[effect 执行] --> B[读取 signal 值] B --> C[注册 effect 到 subscribers] C --> D[更新 signal 值] D --> E[遍历 subscribers] E --> F[执行 effect]

五、极简版 Signal 实现

下面的代码实现了最基本的 signaleffect,可直接在浏览器运行:

js 复制代码
let activeEffect = null;

function signal(initial) {
  const subscribers = new Set();
  let value = initial;

  const read = () => {
    if (activeEffect) subscribers.add(activeEffect);
    return value;
  };

  const write = (newValue) => {
    value = newValue;
    subscribers.forEach(fn => fn());
  };

  return [read, write];
}

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

// 测试
const [count, setCount] = signal(0);

effect(() => {
  console.log("count changed:", count());
});

setCount(1);
setCount(2);

六、Computed Signal 的实现

在这个版本里,我们添加派生信号(computed):

js 复制代码
function computed(getter) {
  const [read, write] = signal();
  effect(() => write(getter()));
  return read;
}

// 测试
const [count, setCount] = signal(0);
const doubleCount = computed(() => count() * 2);

effect(() => {
  console.log("Double:", doubleCount());
});

setCount(3); // Double: 6

七、Signal 与 Vue3 响应式的对比

Vue3 的响应式和 Signal 的相似点:

  • 都有 副作用函数(effect)
  • 都有 依赖收集(track)触发更新(trigger)

但它们的依赖存储结构和触发路径完全不同。


7.1 数据结构差异

Vue3 响应式依赖存储
js 复制代码
WeakMap (targetMap)
  -> Map (depsMap)
      -> Set (dep)
          -> effect 函数
  • targetMap 记录不同的对象

  • depsMap 记录对象的不同属性

  • dep 存储依赖该属性的所有 effect

Signal 依赖存储
js 复制代码
Signal 对象
  -> subscribers (Set)
      -> effect 函数
  • 依赖直接存在 Signal 实例上

  • 没有全局 WeakMap,也不区分属性级别

7.2 执行路径对比

Vue3 track/trigger 流程
sequenceDiagram participant User as 用户访问/修改属性 participant Proxy as Proxy 拦截 participant Track as track() participant TargetMap as targetMap participant Trigger as trigger() participant Effect as effect() User->>Proxy: get/set 属性 Proxy->>Track: 调用 track/trigger Track->>TargetMap: 查找 target TargetMap->>TargetMap: 获取/创建 depsMap TargetMap->>TargetMap: 获取/创建 dep Track->>Effect: 添加 activeEffect Trigger->>TargetMap: 获取 depsMap TargetMap->>Trigger: 获取 dep Trigger->>Effect: 执行所有依赖
Signal 订阅/触发流程
sequenceDiagram participant User as 用户读取/设置值 participant Signal as Signal 对象 participant Subs as subscribers(Set) participant Effect as effect() User->>Signal: 调用 read() Signal->>Subs: 添加 activeEffect User->>Signal: 调用 write(newValue) Signal->>Subs: 遍历 subscribers Subs->>Effect: 执行所有依赖

7.3 精简源码对比

Vue3 核心
js 复制代码
function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, depsMap = new Map());
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, dep = new Set());
  dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  dep && dep.forEach(effectFn => effectFn());
}
Signal 核心
js 复制代码
function signal(initial) {
  const subscribers = new Set();
  let value = initial;

  const read = () => {
    if (activeEffect) subscribers.add(activeEffect);
    return value;
  };
  const write = (newValue) => {
    value = newValue;
    subscribers.forEach(fn => fn());
  };
  return [read, write];
}

7.4 核心差异总结

对比项 Vue3 响应式 Signal
存储结构 WeakMap → Map → Set Signal 对象 → Set
依赖收集范围 属性级别 值级别
全局性 依赖全局 targetMap 管理 每个信号独立管理
更新路径 多层查找 + 批量触发 直接触发订阅者
场景 全局状态、复杂组件树 精细化状态、局部性能优化

八、总结

  • Signal 是一种更细粒度的响应式机制
  • 相比 Vue3 的 Proxy 响应式,Signal 更新路径更短,订阅范围更小
  • 在性能敏感的场景(如大列表渲染、动画状态管理)中表现更佳
  • 越来越多的现代框架将 Signal 纳入核心
相关推荐
会是上一次1 分钟前
企业级WEB应用服务器TOMCAT
java·前端·tomcat
anyup3 分钟前
🚀 2025 最推荐的 uni-app 技术栈:unibest + uView Pro 高效开发全攻略
前端·vue.js·uni-app
小喷友3 分钟前
第 12 章:最佳实践与项目结构组织
前端·react.js·next.js
ze_juejin4 分钟前
Nuxt.js 混合渲染模式(部分静态化+部分动态渲染)
前端
用户52709648744907 分钟前
Vue3 + Element Plus 报错:Cannot read properties of null (reading 'ce')
前端
ze_juejin14 分钟前
Nuxt.js SSR (服务端渲染) 的底层原理
前端
阿邱吖19 分钟前
实习小记(类名添加问题)&&运算符返回结果
前端
aoi27 分钟前
Monaco json 代码块中插入不可编辑,整块删除,整块光标跳过变量块
前端
用户8338102512244 分钟前
我为什么做PmMock:让接口设计不再头疼
前端·后端
我是ed1 小时前
# vue3 实现web网页不同分辨率适配
前端