Skip to content

Signal 响应式原语

Signal 是一种轻量级的响应式原语,它代表了一个随时间变化的值,并且能够自动追踪依赖关系。Signal 是现代前端框架(如 Vue、Angular、Solid.js、Preact等)中实现响应式系统的核心概念。

Signal 的基本概念

Signal 本质上是一个包含值的容器,它能够:

  1. 存储一个值
  2. 追踪对该值的访问(读取)
  3. 在值发生变化时通知所有依赖项
javascript
// 基本使用示例
const count = signal(0);  // 创建一个初始值为 0 的 signal

// 读取值
console.log(count());  // 输出: 0

// 设置新值
count.set(1);
console.log(count());  // 输出: 1

// 更新值(基于当前值)
count.update(c => c + 1);
console.log(count());  // 输出: 2

Signal 与响应式系统

Signal 是构建响应式系统的基础,它通过以下机制实现响应式:

  1. 依赖追踪:当读取 signal 的值时,系统会记录当前正在执行的代码依赖于这个 signal,其实现基础是要求副作用函数要运行在指定的容器中,容器能够监控副作用函数对于其感兴趣的值获取操作;
  2. 变更通知:当 signal 的值发生变化时,系统会通知所有依赖这个 signal 的代码重新执行
  3. 批量更新:多个 signal 的变化会被批量处理,避免不必要的重复计算
javascript
// 依赖追踪示例
const firstName = signal('John');
const lastName = signal('Doe');

// 创建一个计算值
const fullName = computed(() => {
    // 当 firstName 或 lastName 变化时,这个函数会重新执行
    return `${firstName()} ${lastName()}`;
});

// 监听变化,将副作用函数放进容器中进行执行,这里的 effect 函数就是一个容器
effect(() => {
    console.log(`Full name changed to: ${fullName()}`);
});

// 修改值会触发重新计算
firstName.set('Jane');  // 输出: Full name changed to: Jane Doe

不同框架中的 Signal 实现

Solid.js

Solid.js 是最早采用 Signal 概念的框架之一:

javascript
import { createSignal, createEffect } from 'solid-js';

function Counter() {
    const [count, setCount] = createSignal(0);
    
    createEffect(() => {
        console.log('Count:', count());
    });
    
    return (
        <button onClick={() => setCount(c => c + 1)}>
            Count: {count()}
        </button>
    );
}

Preact Signals

Preact 的 Signal 实现:

javascript
import { signal, computed, effect } from '@preact/signals';

const count = signal(0);
const double = computed(() => count.value * 2);

effect(() => {
    console.log(`Count: ${count.value}, Double: ${double.value}`);
});

count.value = 1;  // 输出: Count: 1, Double: 2

Vue 3

Vue 3 的 ref 和 computed 本质上也是 Signal 的实现:

javascript
import { ref, computed, watchEffect } from 'vue';

const count = ref(0);
const double = computed(() => count.value * 2);

watchEffect(() => {
    console.log(`Count: ${count.value}, Double: ${double.value}`);
});

count.value = 1;  // 输出: Count: 1, Double: 2

Signal 的优势

  1. 细粒度更新:只更新真正发生变化的部分,而不是整个组件树
  2. 更少的抽象:相比 Observable 等概念,Signal 更简单直观
  3. 更好的性能:避免了虚拟 DOM 的 diff 过程,直接更新 DOM
  4. 更小的包体积:实现简单,代码量小

Signal 的最佳实践

  1. 最小化 Signal 数量:只对真正需要响应式的数据使用 Signal
  2. 合理使用 computed:对于派生数据,使用 computed 而不是普通 Signal
  3. 避免不必要的 effect:只在必要时使用 effect,避免副作用
  4. 批量更新:将多个更新操作放在一起,减少重渲染次数
javascript
// 不好的做法
const firstName = signal('John');
const lastName = signal('Doe');

// 每次更新都会触发重渲染
firstName.set('Jane');
lastName.set('Smith');

// 好的做法
batch(() => {
    firstName.set('Jane');
    lastName.set('Smith');
});

Signal 与状态管理的结合

Signal 可以很好地与状态管理库结合使用:

javascript
// 使用 Signal 实现简单的状态管理
function createStore() {
    const state = signal({ count: 0 });
    
    return {
        getState: () => state(),
        setState: (newState) => state.set(newState),
        subscribe: (callback) => {
            effect(() => {
                callback(state());
            });
        }
    };
}

const store = createStore();
store.subscribe(state => console.log('State changed:', state));
store.setState({ count: 1 });  // 输出: State changed: { count: 1 }

总结

Signal 是现代前端框架中实现响应式系统的重要概念,它通过简单直观的 API 提供了强大的响应式能力。Signal 的设计理念是"简单而强大",它既保持了使用上的简单性,又提供了足够的灵活性来处理复杂的响应式场景。随着前端框架的发展,Signal 可能会成为响应式编程的标准实现方式。