深入 Lyt.js 响应式系统:Proxy + Signal 双模式

深入 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 都能给你一个满意的选择。

相关推荐
KaMeidebaby7 小时前
卡梅德生物技术快报|PD1 单克隆抗体定制配套 N 糖全谱质控开发
前端·人工智能·算法·数据挖掘·数据分析
nuIl8 小时前
实现一个 Coding Agent(3):工具调用
前端·agent·cursor
nuIl8 小时前
实现一个 Coding Agent(4):ReAct 循环
前端·agent·cursor
nuIl8 小时前
实现一个 Coding Agent(1):一次 LLM 调用
前端·agent·cursor
nuIl8 小时前
实现一个 Coding Agent(2):让 LLM 流式响应
前端·agent·cursor
copyer_xyf8 小时前
Python 异常处理
前端·后端·python
sugar__salt8 小时前
从栈队列数据结构到JS原型面向对象全解
前端·javascript·数据结构
独特的螺狮粉8 小时前
篮球集训班器具管理系统 - 鸿蒙PC Electron框架完整技术实现指南
前端·javascript·华为·electron·前端框架·开源·鸿蒙
pusheng20258 小时前
IFSJ全英文专访:中国创新力量重塑先进气体感知技术,赋能全球关键基础设施安全
前端·网络·人工智能·物联网·安全
AI_零食9 小时前
番茄钟鸿蒙PC Electron框架完成:状态机、定时器管理与专注力工具设计
前端·javascript·华为·electron·开源·鸿蒙·鸿蒙系统