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

发布时间:2026/10/4 5:36:18

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),仅供参考
延伸阅读

更多相关文章

2026/10/4 5:36:18

理清字节序

大小端字节序说到字节序,它描述的是:一个多字节数值,在内存里存放时,高位字节和低位字节的排列顺序。其有大小端之分。大端:低内存地址放高位字节,高内存地址放低位字节。小端:低内存地址放低位…

2026/10/4 5:31:17

PyInstaller打包Scrapy报OSError解决方案

1. 这不是PyInstaller的锅,是Scrapy和Python运行时机制在“打架”你打包Scrapy项目时突然弹出OSError: could not get source code,第一反应可能是“PyInstaller又抽风了”,但真相往往更微妙——这根本不是打包工具的问题,而是Scr…

2026/10/4 5:31:17

QGC视频流二次开发实战:从GStreamer管线到黑屏排查

做 QGC 二次开发,十个需求里至少一半要碰视频流。不是要接机载摄像头,就是要在地面站上显示自定义图传画面,要么就是嫌默认的 UDP 拉流不够稳想换 RTSP。视频流这个模块,恰恰是 QGC 源码里最绕的一块:一头连着飞控端的…

2026/10/4 6:16:19

K210人脸检测与识别实战:KPU底层调优与边缘部署全链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 6:16:19

从0到1搭建AI内容生产流水线:架构设计与工程实践

1. 先搞清楚“AI内容生产流水线”到底在说什么“从0到1搭建AI内容生产流水线”这个说法,最近一年在技术社区里出现的频率越来越高。很多人第一次听到会以为就是“用AI写文章”,但真正动手做过的人都知道,这两件事之间的差距,大概相…

2026/10/4 6:16:19

建筑学AI开题报告写作指南:从场地分析到技术路线一次讲清

建筑学专业的开题报告比一般专业更"重":既要文字论述,又要设计前期分析图,设计类课题还要附场地调研。很多同学图做得漂亮,文字部分却逻辑混乱,开题答辩被评委连环追问"你的研究问题到底是什么"&a…

2026/10/4 6:16:19

基于Python+爬虫的旅游景点数据分析与可视化平台设计与实现

选题背景与意义 随着互联网技术的迅猛发展和智能设备的普及,旅游行业正经历着深刻的数字化转型。越来越多的游客在出行前依赖网络平台获取景点信息、评价反馈及行程规划建议,这使得在线旅游数据呈现出爆炸式增长。与此同时,各大旅游网站如携程…

2026/10/4 6:16:19

试了五六款论文工具后,我为什么长期留在汇写?

我属于那种"工具控",写毕业论文这大半年,前后试过的论文辅助网站不下五六款。有的主打AI写作,有的主打降重,有的主打查重,还有的就是一个空壳壳,点进去全是诱导充值。踩过的坑多了,我…

2026/10/4 6:11:19

公交数据中心云平台建设:从方案书到落地实践

简介:这份《公交数据中心云平台建设方案书》是面向智慧城市与公交企业信息化规划的技术文档,适合交通管理部门、公交集团信息化人员、云计算/大数据项目架构师参考,用于解决公交运营数据采集、处理、分析及智能决策等建设问题。全包共1个doc文…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑