ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

30 Seconds of Code:用原生 JavaScript 事件驱动实现 Signals 响应式系统

30 Seconds of Code:用原生 JavaScript 事件驱动实现 Signals 响应式系统 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载在 30-seconds-of-code 仓库的 JavaScript 片段集中event-driven-vanilla-js-signals.md 展示了如何不依赖任何框架仅用原生EventTarget、CustomEvent与类私有字段在不到 70 行代码内实现现代前端响应式编程中最核心的三个概念Signal信号、Effect副作用与ComputedValue派生值。读完本文你将掌握响应式库底层的工作原理并能直接把这些可运行代码用于自己的状态管理、UI 同步等场景。从一个熟悉的名字说起Signals 就是 Observable现代响应式库如 Solid、Angular Signals 等中反复出现的signals概念本质上就是经典Observer 模式观察者模式的另一种叫法一个可以被监听的值流。当信号的值发生变化时它会通知所有注册的监听者listeners后者再做出相应反应。仓库中另一篇姊妹文章 observable-proxy.md 曾用Proxy对象的set陷阱拦截属性赋值来实现 Observableclass Observable extends EventTarget { constructor() { super(); return new Proxy(this, { set: (target, property, value) { target[property] value; this.dispatchEvent(new CustomEvent(property, { detail: value })); return true; }, }); } }而本篇采用了一条更“直白”的路线去掉Proxy完全依赖事件机制并使用 signal、effect 这类更贴合响应式社区的命名。两者的共同基础都是EventTarget——它是浏览器与 Node.js 环境通用的标准接口在 Node.js 中仅使用EventEmitter时需要额外调整见 observable-proxy.md 的说明因此以下所有代码可以在两种运行时中直接运行。实现一个 Signal用类私有字段保存状态Signal类的实现非常小巧继承EventTarget用类私有属性#value语法见 MDN Private class properties保存当前值对外暴露get value/set value访问器读值get value直接返回私有字段写值set value支持两种赋值方式——直接给新值或传入一个接收当前值并返回新值的函数仿照 ReactsetState的函数式更新去重通知只有新值与旧值不相等时才真正更新并派发事件事件派发通过this.dispatchEvent(new CustomEvent(notify, { detail: nextValue }))广播变更CustomEvent.detail携带新值CustomEvent的用法可对照 trigger-event.md。class Signal extends EventTarget { #value; constructor(value) { super(); this.#value value; } get value() { return this.#value; } set value(newValue) { const nextValue typeof newValue function ? newValue(this.#value) : newValue; if (nextValue this.#value) return; this.#value nextValue; this.dispatchEvent(new CustomEvent(notify, { detail: nextValue })); } } const signal new Signal(42); signal.addEventListener(notify, event { console.log(Signal changed to ${event.detail}); }); signal.value 42; // No change, no event signal.value 43; // LOGS: Signal changed to 43 signal.value value value 1; // LOGS: Signal changed to 44注意示例中的行为细节赋相同值42不会触发任何事件赋43触发一次而传入函数value value 1时函数会以当前值43为入参计算出44并触发通知。这段代码同时体现了事件驱动响应式的一个关键取舍通知语义基于“值的变更”而非“赋值的次数”。实现一个 Effect自动订阅与手动清理只有“可监听”还不够响应式系统还需要“自动响应”。Effect就是 observers 的别称一个在它所依赖的任意 signal 变化时被调用的函数这让系统可以用声明式的方式对变化做出反应。Effect类不再继承EventTarget而是用私有字段#subscriptions new Set()收集反订阅函数unsubscribe。构造函数接收callback与dependenciessignal 数组并完成三步工作为每个依赖的 signal 注册notify监听器绑定callback同时向#subscriptions中存入一个“撤销函数”它调用removeEventListener移除对应监听立即执行一次callback()让 effect 与当前状态同步。清理时调用dispose()它会遍历并执行#subscriptions中的所有撤销函数。监听器的添加与移除机制与本仓库 add-remove-event-listener.md 中讲解的addEventListener/removeEventListener语义完全一致——这也是dispose能生效的前提。class Effect { #subscriptions new Set(); constructor(callback, dependencies []) { dependencies.forEach(dependency { dependency.addEventListener(notify, callback); this.#subscriptions.add(() { dependency.removeEventListener(notify, callback); }); }); callback(); } dispose() { this.#subscriptions.forEach(unsubscribe unsubscribe()); } } const signal new Signal(42); const effect new Effect(() { console.log(Effect triggered with value ${signal.value}); }, [signal]); // LOGS: Effect triggered with value 42 signal.value 43; // LOGS: Effect triggered with value 43 effect.dispose(); signal.value 44; // No effect运行结果验证了生命周期effect 创建时立即打印42signal 更新为43时 effect 自动触发dispose()之后即使 signal 再变为44也不再有任何输出。一个 Effect 可以同时依赖多个 signal——构造函数中的dependencies.forEach说明只需要在数组中列出多个依赖即可实现多源订阅。实现一个 ComputedValue信号与副作用的组合直接对 signal 计算值并不困难但真正的难点在于从一个信号派生新值再让 effect 对“派生值”的变化做出反应。派生值本质上是“依赖其他信号的信号”自身没有直接赋值入口而是基于依赖项实时计算。ComputedValue类同时继承了EventTarget并内部组合了一个Signal与一个Effect把前两个模式合二为一构造先用callback()的初始结果创建内部#signal再创建内部#effect其回调为this.#signal.value callback()——依赖变化时重新计算并写回内部信号取值get value委托给内部#signal事件委托重写addEventListener/removeEventListener把外部监听器全部转发给内部#signal从而复用Signal自带的事件派发逻辑清理dispose()委托给内部#effect解除对依赖信号的订阅。class ComputedValue extends EventTarget { #signal; #effect; constructor(callback, dependencies []) { super(); this.#signal new Signal(callback()); this.#effect new Effect(() { this.#signal.value callback(); }, dependencies); } get value() { return this.#signal.value; } addEventListener(type, listener) { this.#signal.addEventListener(type, listener); } removeEventListener(type, listener) { this.#signal.removeEventListener(type, listener); } dispose() { this.#effect.dispose(); } } const signal new Signal(42); const signalEffect new Effect(() { console.log(Signal changed to ${signal.value}); }, [signal]); // LOGS: Signal changed to 42 const computed new ComputedValue(() signal.value * 2, [signal]); const computedEffect new Effect(() { console.log(Computed value changed to ${computed.value}); }, [computed]); // LOGS: Computed value changed to 84 signal.value 43; // LOGS: Signal changed to 43 // LOGS: Computed value changed to 86观察输出顺序可以还原完整的数据流signal变为43→ 直接监听它的signalEffect打印43→ 内部 effect 感知依赖变化把callback()的结果43 * 2 86写入内部#signal→ 内部#signal派发notify事件 → 由于computedEffect通过ComputedValue委托的addEventListener监听了该事件打印86。这正是一条标准的单向数据流Signal → ComputedValue → Effect。把三者串起来一个可复用的迷你响应式系统将上面的三个类组合你得到的是一套可直接粘贴到任何浏览器或 Node.js 项目中的迷你响应式运行时。它演示了如何用事件驱动范式搭建从“可观察状态”到“自动副作用”再到“派生状态”的完整链路// 1. 状态Signal 保存原始值 const count new Signal(0); // 2. 派生ComputedValue 基于状态计算 const doubled new ComputedValue(() count.value * 2, [count]); // 3. 响应Effect 自动同步副作用如更新 DOM、发起请求 const render new Effect(() { document.title Count: ${count.value}, Doubled: ${doubled.value}; }, [count, doubled]); count.value 1; // 标题自动更新为 Count: 1, Doubled: 2这套模式的工程化价值在于状态的变更点被收敛到 signal 的set一处任何 UI 同步、日志、网络请求都通过声明式的 effect 表达避免了手动“找对象、加监听、再手动同步”的脆弱写法。同时这套实现也有明确的边界与改进空间从源码结构可以推断出以下可扩展方向事件类型单一化当前统一使用notify事件类型。若要区分不同来源的更新可参照 observable-proxy.md 的做法把属性名作为CustomEvent的type无派发控制Signal没有“暂停通知”或“批量更新”机制高频写入会产生大量事件在性能敏感场景需要引入类似微任务合并batching的优化对比替代方案若只在 Node.js 环境使用可用EventEmitter替换EventTarget见 observable-proxy.md 的注释若希望“赋值即响应”而无需显式set访问器则可选用Proxy路线。结语事件驱动是响应式的骨架短短70 行以内的代码我们完成了对 signals 与响应式模式的“祛魅”Signal用EventTargetCustomEvent提供可观察状态Effect用订阅集合实现自动响应与可清理的生命周期ComputedValue以“信号 副作用”的组合完成派生值的传播。正如原文结论所言这是一次极佳的训练让你真正理解响应式库在引擎盖下做了什么也让你体会到事件驱动编程的简洁力量——不需要任何框架浏览器与 Node.js 通用的标准 API 就足以撑起一套完整的响应式抽象。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐Civitai 事件引擎 Redis 缓存排查指南redis-inspect 技能全解析Civitai 事件引擎 Redis 缓存排查指南redis inspect 技能全解析 本文围绕 apps/event engine/.claude/ski教程文档30 seconds of code用 CSS Grid 实现响应式图片马赛克布局30 seconds of code用 CSS Grid 实现响应式图片马赛克布局 导读 图片马赛克image mosaic是一种常见于相册页、作品集与商教程文档30-seconds-of-code用 JavaScript 实现凯撒密码Caesar Cipher30 seconds of code用 JavaScript 实现凯撒密码Caesar Cipher 导读 凯撒密码Caesar cipher是最经典教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表