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

相关推荐
程序员黑豆19 分钟前
鸿蒙开发入门:以 Text 组件为例,掌握内置组件用法
前端·harmonyos
爱勇宝21 分钟前
AI不会淘汰所有人,但会淘汰这6种人
前端·后端·程序员
J船长41 分钟前
烂笔头扫盲:Embedding 与语义匹配:从概念到 pgvector 落地
前端
程序员黑豆1 小时前
鸿蒙应用开发之模拟器安装与使用教程
前端·harmonyos
王莎莎2 小时前
MCP 解决的是工具接入,科研 Agent 还缺的是科学证据接口标准化
前端·人工智能
zhanghaha13142 小时前
Python语言基础:4_数据类型转换
java·前端·python
kyriewen2 小时前
别再写useMemo了——2026年这5个React性能优化已经是反模式
前端·react.js·ai编程
OpenTiny社区2 小时前
TinyRobot v0.5.0 新版本强在哪?
前端·vue.js·github
奇牙coding2 小时前
gpt-5.6-sol 接入指南:reasoning_effort 参数配置、推理链验证与常见报错排查
前端·css·gpt·ai
sugar__salt3 小时前
Vue.js 前置知识:ES6+ 核心特性完全指南
前端·javascript·vue.js·vue·es6