深入 Lyt.js 响应式系统:Proxy + Signal 双模式
作者:Lyt.js Team | 发布时间:2025 年 4 月
响应式系统是现代前端框架的灵魂。Vue 3 用 Proxy,Solid.js 用 Signal,Svelte 5 用 Runes ------ 每种方案都有其优势和取舍。Lyt.js 做出了一个大胆的决定:为什么不都支持呢?
响应式系统的两大流派
Proxy 流派:声明式响应式
以 Vue 3 为代表,通过 ES6 Proxy 拦截对象的读写操作,自动追踪依赖和触发更新。开发者只需要声明数据,框架自动处理响应式逻辑。
优势:心智负担低,代码直观,自动深层响应式
劣势:粒度较粗,整个组件重新渲染,需要虚拟 DOM diff 来精确更新
Signal 流派:细粒度响应式
以 Solid.js 为代表,通过 Signal(信号)建立精确的依赖关系。每个 Signal 独立追踪订阅者,更新时只通知相关的订阅者。
优势:极致性能,无虚拟 DOM 开销,精确更新
劣势:需要手动管理 Signal,心智负担较高
Lyt.js 的双模式设计
Lyt.js 同时实现了两套完整的响应式系统,并通过统一的组件接口让开发者自由选择:
javascript
import { defineComponent } from '@lytjs/core'
// Proxy 模式(默认)
const ProxyComponent = defineComponent({
reactivityMode: 'proxy',
state() {
return { count: 0, name: 'Lyt.js' }
},
template: `<div>{{ count }} - {{ name }}</div>`
})
// Signal 模式
const SignalComponent = defineComponent({
reactivityMode: 'signal',
state() {
return { count: 0, name: 'Lyt.js' }
},
template: `<div>{{ count }} - {{ name }}</div>`
})
两种模式使用完全相同的组件 API,模板语法也完全一致。区别只在于底层如何追踪和触发更新。
Proxy 模式深入解析
Lyt.js 的 Proxy 模式实现借鉴了 Vue 3 的设计,但做了多项优化:
三层缓存架构
使用三个 WeakMap 分别缓存普通代理、只读代理和浅层代理,确保同一个对象始终返回同一个代理实例:
javascript
const proxyMap = new WeakMap<object, any>() // 普通响应式
const readonlyMap = new WeakMap<object, any>() // 只读响应式
const shallowReactiveMap = new WeakMap<object, any>() // 浅层响应式
数组方法拦截
对数组的搜索方法和变异方法分别做了特殊处理:
- 搜索方法(includes/indexOf/lastIndexOf) :追踪每个元素的依赖,确保
arr.includes(item)能正确触发更新 - 变异方法(push/pop/shift/splice 等):暂停内部依赖收集,手动触发 length 更新,避免冗余通知
receiver 检查
Proxy 的 get 拦截器会检查 receiver,防止原型链上的重复触发:
javascript
get(target, key, receiver) {
// 如果 receiver 不是代理本身,说明是通过原型链访问的
if (target === toRaw(receiver)) {
track(target, key) // 收集依赖
}
const result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result) // 深层递归代理
}
return result
}
Signal 模式深入解析
Lyt.js 的 Signal 实现参考了 Solid.js 和 Angular Signals,但保持了零依赖的纯净实现。
核心 API
javascript
import { signal, computed, effect, batch, untrack } from '@lytjs/reactivity/signal'
// 创建可写信号
const count = signal(0)
// 创建计算信号(只读,惰性求值)
const double = computed(() => count() * 2)
// 创建副作用
const dispose = effect(() => {
console.log(`count = ${count()}, double = ${double()}`)
})
// 更新信号
count.set(1) // 输出: count = 1, double = 2
count.update(n => n + 1) // 输出: count = 2, double = 4
// 批量更新
batch(() => {
count.set(5)
count.set(10)
// effect 只会执行一次
})
// 清理
dispose()
自动依赖追踪
Signal 的依赖追踪通过全局变量 activeSubscriber 实现。当 effect 或 computed 执行时,会将自身设为活跃订阅者,此时读取任何 Signal 都会自动建立依赖关系:
javascript
let activeSubscriber: Subscriber | null = null
export function signal<T>(initialValue: T): WritableSignal<T> {
let value: T = initialValue
const subscribers = new Set<Subscriber>()
const sig = function SignalGetter(): T {
// 如果有活跃的订阅者,自动建立依赖
if (activeSubscriber && !isUntracked) {
subscribers.add(activeSubscriber)
}
return value
} as WritableSignal<T>
sig.set = function(newValue: T): void {
if (Object.is(value, newValue)) return // 值未变化则跳过
value = newValue
_notifySubscribers(subscribers)
}
return sig
}
循环依赖检测
Computed 内置了循环依赖检测机制,避免无限递归:
javascript
export function computed<T>(fn: () => T): ComputedSignal<T> {
let isComputing = false
// ...
const comp = function ComputedGetter(): T {
if (isDirty) {
if (isComputing) {
throw new Error('[lyt:signal] 检测到循环依赖')
}
isComputing = true
// 执行计算...
isComputing = false
}
return cachedValue
}
return comp
}
嵌套安全的批量更新
batch 支持嵌套调用,只有最外层 batch 完成时才统一执行更新:
javascript
let batchDepth = 0
const pendingNotifications: Set<Subscriber> = new Set()
export function batch(fn: () => void): void {
batchDepth++
try {
fn()
} finally {
batchDepth--
if (batchDepth === 0) {
_flushPending() // 只有最外层完成才执行
}
}
}
双模式如何统一?
关键在于 defineComponent 中的 reactivityMode 选项。当选择 Signal 模式时,Lyt.js 内部使用 SignalStateProxy 将 Signal 操作包装为类似 Proxy 的访问方式,使得 this.count++ 这样的代码在两种模式下都能正常工作:
javascript
// Signal 模式下,this.count 实际调用 signal()
// this.count++ 实际调用 signal.set(signal() + 1)
const state = createSignalStateProxy({
count: signal(0),
name: signal('Lyt.js')
})
state.count++ // 工作正常!
console.log(state.count) // 读取也工作正常!
性能对比
两种模式各有适用场景:
| 维度 | Proxy 模式 | Signal 模式 |
|---|---|---|
| 更新粒度 | 组件级(依赖 VDOM diff) | 节点级(精确更新) |
| 内存占用 | 较高(Proxy 对象 + VNode 树) | 较低(无 VNode) |
| 首次渲染 | 正常 | 正常 |
| 更新性能 | 中等 | 优秀 |
| 学习成本 | 低(自动响应式) | 低(API 统一) |
| 适用场景 | 大多数场景 | 性能敏感、大数据量 |
总结
Lyt.js 的双响应式系统不是简单的"两种方案拼凑",而是经过深思熟虑的架构设计。它让开发者在保持 Vue 风格开发体验的同时,能够在需要时切换到 Signal 级别的性能。这种灵活性是其他框架所不具备的。
无论你是 Vue 开发者想要更好的性能,还是 Solid.js 开发者想要更友好的 API,Lyt.js 都能给你一个满意的选择。