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

相关推荐
idcu2 小时前
Vapor Mode 揭秘:无虚拟 DOM 的极致性能
前端
idcu2 小时前
从 Vue 3 到 Lyt.js:无痛迁移指南
前端
尘世壹俗人2 小时前
如何检查服务器上消耗资源的程序是那个
服务器·前端·chrome
LIO2 小时前
Vue Router 进阶:深入用法与最佳实践
前端·vue-router
Hilaku2 小时前
做了 6 年前端,技术不差却拿不到 Offer?
前端·javascript·程序员
古茗前端团队2 小时前
钉钉小程序蓝牙打印探索与实践
前端·蓝牙
LIO2 小时前
一文看懂 Vue Router:精简、易懂、直接用
前端·vue-router
Highcharts.js2 小时前
技术组合分析:Highcharts 的数据集成能力解析
java·前端·金融·echarts·saas·bi·highcharts
在下有个宝贝2 小时前
GIS前端开发之路——Openlayers为地图添加自定义标注(四)
前端