无需框架的响应式:原生 JS 今天能做什么
你想要响应式 UI 行为——状态变化自动更新 DOM——但你不想为一个简单的组件引入 40KB 的框架代码。好消息是:使用已在浏览器中稳定多年的 API,完全可以实现原生 JavaScript 响应式。
本文介绍了 2025 年末可用于构建响应式 UI 的原生工具:基于 Proxy 的响应式状态、用于发布/订阅的 EventTarget 和 CustomEvent,以及用于 DOM 感知响应的浏览器观察器。你将了解今天可用的功能、即将推出的特性,以及这些模式如何映射到框架内部。
核心要点
- Proxy 对象拦截属性变化,无需框架依赖即可实现自动 DOM 更新
- EventTarget 和 CustomEvent 为解耦的组件通信提供原生发布/订阅层
- 浏览器观察器(MutationObserver、IntersectionObserver、ResizeObserver)处理 DOM 和布局响应式
- TC39 Signals 提案可能会标准化响应式原语,但当前的 Proxy + EventTarget 模式已能实现类似效果
响应式的真正含义
响应式是一个简单的循环:状态变化触发 UI 更新。框架通过虚拟 DOM、编译器或细粒度依赖跟踪来自动化这一过程。但底层机制依赖的是你可以直接使用的 JavaScript 特性。
核心模式:
- 将状态存储在可跟踪的结构中
- 状态变化时通知订阅者
- 仅更新相关的 DOM
原生浏览器 API 无需外部依赖即可处理每个步骤。
基于 Proxy 的响应式状态
Proxy 对象拦截属性访问和赋值。结合 Reflect,它构成了基于代理的响应式状态的基础。
function createReactiveStore(initial, onChange) {
return new Proxy(initial, {
set(target, prop, value) {
const result = Reflect.set(target, prop, value)
onChange(prop, value)
return result
}
})
}
const state = createReactiveStore({ count: 0 }, (prop, value) => {
document.getElementById('count').textContent = value
})
state.count = 5 // DOM 自动更新
这种模式感觉像”信号”——你写入状态,副作用就会运行。Vue 3 的响应式系统正是出于这个原因在内部使用 Proxy。
局限性: Proxy 陷阱仅在对代理对象本身应用的变更时触发。如果嵌套对象或数组没有也被包装在各自的代理中,它们内部的变化(如 array.push())将不会被跟踪。许多开发者使用不可变更新(例如 state.items = [...state.items, newItem])来保证更新触发。
EventTarget 和 CustomEvent 作为发布/订阅层
对于解耦的组件通信,EventTarget 提供了原生的发布/订阅机制。任何对象都可以成为事件发射器。
const bus = new EventTarget()
// 订阅
bus.addEventListener('state-change', (e) => {
console.log('新值:', e.detail)
})
// 发布
bus.dispatchEvent(new CustomEvent('state-change', {
detail: { count: 10 }
}))
这种模式使用原生浏览器 API 驱动响应式 UI。组件订阅事件,响应变化,并保持解耦。与自定义发布/订阅实现不同,EventTarget 与浏览器 DevTools 集成,并遵循标准事件语义。
Discover how at OpenReplay.com.
用于 DOM 响应式的浏览器观察器
当你需要响应 DOM 或布局变化——而不仅仅是状态——时,浏览器观察器填补了这一空白。
MutationObserver 监视 DOM 修改:
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => console.log('DOM 已变化:', m))
})
observer.observe(document.body, { childList: true, subtree: true })
IntersectionObserver 跟踪元素可见性——对懒加载或分析很有用。
ResizeObserver 响应元素大小变化,无需轮询。
这些 API 长期以来一直很稳定,可以安全用于生产环境。它们通过处理外部因素修改 DOM 的情况来补充状态驱动的响应式。
TC39 Signals 提案:即将推出的功能
人们对标准化响应式原语越来越感兴趣。TC39 Signals 提案旨在定义框架可以共享的通用模型。
重要提示: 截至 2025 年,这仍然是一个提案——不是已发布的 JavaScript 特性。Solid、Angular 和 Preact 等框架已采用类似信号的模式,影响了提案的设计。但你今天还不能在浏览器中使用”原生信号”。
上述 Proxy + EventTarget 模式实现了类似的目标。如果信号标准化,迁移应该很简单,因为心智模型是一致的。
选择正确的模式
| 模式 | 最适合 | 权衡 |
|---|---|---|
| Proxy | 本地组件状态 | 除非嵌套值也被代理,否则仅跟踪代理对象上的变化 |
| EventTarget | 跨组件消息传递 | 手动连接 |
| MutationObserver | 响应外部 DOM 变化 | 性能开销 |
对于小型应用和组件,将基于 Proxy 的状态与 EventTarget 结合使用,无需框架开销即可满足大多数响应式 UI 需求。
结论
今天无需框架即可实现响应式。Proxy 处理状态跟踪,EventTarget 提供发布/订阅,浏览器观察器响应 DOM 变化。这些 API 稳定、文档完善,并可组合成轻量级响应式核心。
你不需要框架来获得细粒度响应式。你需要理解框架所基于的原语——现在你已经掌握了。
常见问题
Proxy 陷阱仅在直接对代理对象进行属性赋值时触发。对于嵌套对象,你需要递归地将每个嵌套对象包装在自己的 Proxy 中,或者在进行更改时替换整个嵌套结构。大多数开发者选择不可变更新模式,如展开运算符来创建新引用。
EventTarget 是原生浏览器 API,与 DevTools 集成并遵循标准调度语义。完整的冒泡和捕获仅在事件目标是 DOM 树的一部分时适用。自定义库可能提供额外功能,如通配符监听器或一次性订阅,但 EventTarget 不需要依赖项,并在所有现代浏览器中一致工作。
当你需要响应由外部代码、第三方脚本或浏览器扩展所做的 DOM 变化时,使用 MutationObserver。Proxy 跟踪你控制的 JavaScript 状态变化。MutationObserver 监视实际的 DOM 树,无论是什么导致了变化。它们服务于不同的目的,通常一起工作。
Signals 提案旨在标准化框架可以共享的响应式原语,而不是取代现有 API。Proxy 和 EventTarget 仍将是有效的方法。如果 Signals 发布,它们可能会通过为不同库之间的细粒度依赖跟踪提供标准接口来补充这些模式。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.