Lit 组件接入 Preact Signals 状态管理:@lit-labs/preact-signals 完整指南
Lit 组件接入 Preact Signals 状态管理lit-labs/preact-signals 完整指南【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit本文围绕 Lit 官方实验包lit-labs/preact-signals源码位于 packages/labs/preact-signals展开讲解如何将 Preact Signals 的细粒度响应式状态引入 Lit Web Components通过SignalWatchermixin 让元素在更新生命周期内自动追踪 signal 访问并触发重渲染通过watch()指令实现精准的定向 DOM 更新并通过内置的html/svg模板标签实现 signal 的零样板自动包装。读完本文你将掌握三种接入方式的适用场景、底层原理与取舍并能在自己的 Lit 项目中落地共享状态方案。为什么要使用 SignalsSignals 是一种创建共享可观察状态的简便方式——即许多元素都可以读取、并且在状态变化时自动更新的状态。它非常适合像游戏状态这类需要被多个组件同时读取的场景。这类需求同样可以由 Redux、MobX 之类的状态管理方案、RxJS 之类的可观察对象或者EventTarget来覆盖。而 Signals 的优势在于一种很好的开发者体验DX平衡粒度细、可组合并且 API 相当简单。需要特别澄清的一点是与许多框架不同Signals 并不会给大多数 Lit 组件带来显著的性能提升。这是因为 Lit 的更新本身已经很快——Lit 更新是批量化的并且每次渲染并不做 VDOM diff它只检查哪些绑定值发生了变化然后只更新这些绑定的 DOM。从本质上看Lit 元素的模板已经有点像 signal 产生的值它基于输入响应式属性的变化计算得出。与 Lit 模板的差别在于数据流的方向Lit 是 push推送式变化被推入元素时元素才做出反应Signals 是 pull拉取式signal 会自动订阅它所访问的其他 signal。这两种方式非常兼容——通过一个像本包这样的集成库我们可以让元素订阅它们访问过的 signal并在 signal 变化时触发更新。为什么选择 Preact Signals目前市面上已经有很多 signal 库但它们之间并不能无缝兼容我们无法跨库地泛化监听所有 signal 访问并运行 effect所以需要逐个库单独提供支持。Preact Signals 是一个很好的起点原因在于它已经具备与其他库的集成以标准 JS 模块的形式分发体积小、速度快、质量高。这一点在本包的依赖配置中可以直接得到印证package.json 中核心依赖只有两个preact/signals-core^1.3.0和lit^3.1.2——preact/signals-core提供了 signal 的原生能力而 src/index.ts 通过export * from preact/signals-core直接将其全部导出因此你从本包中就能同时拿到signal()、computed()、effect()等核心 API。⚠️Lit Labs 实验状态说明本包属于 Lit Labs 计划的一部分发布目的是收集设计反馈后续可能发生破坏性变更或停止维护。在生产环境使用前请先阅读官方 Lit Labs 文档。安装与快速开始本包以 ESM 模块发布并提供了development导出条件用于开发模式下的调试构建。安装方式与其他 npm 包一致且signal、computed、watch、SignalWatcher、html、svg等所有 API 均从单一入口导入npm install lit-labs/preact-signalsimport {LitElement, html} from lit; import {customElement} from lit; import {SignalWatcher, signal} from lit-labs/preact-signals;接下来依次介绍三种接入方式SignalWatchermixin、watch()指令以及自动包装的html标签。方式一SignalWatcher mixin——让整个更新生命周期感知 signalSignalWatcher是一个 mixin它使元素在响应式更新生命周期内观察所有 signal 访问并在这些 signal 变化时触发一次元素更新。被观察的生命周期方法包括shouldUpdate()willUpdate()update()render()updated()firstUpdated()响应式控制器的hostUpdate()与hostUpdated()这实际上相当于把render()的返回值变成了一个计算后的 signalcomputed signal——只要渲染过程中读到的任何 signal 发生变化元素就会自动重新渲染。import {LitElement, html, css} from lit; import {customElement} from lit; import {SignalWatcher, signal} from lit-labs/preact-signals; const count signal(0); customElement(signal-example) export class SignalExample extends SignalWatcher(LitElement) { static styles css :host { display: block; } ; render() { return html pThe count is ${count.value}/p button click${this._onClick}Increment/button ; } private _onClick() { count.value count.value 1; } }重要约束元素不应在这些生命周期方法中写入signal否则可能造成无限循环——因为写入 signal 会触发 effect 回调进而再次调度更新形成写→更新→再写的死循环。底层实现performUpdate 中的 effect 包装从源码 src/lib/signal-watcher.ts 可以看到其核心机制mixin 重写了performUpdate()在每次更新前先释放上一次创建的 effect然后通过effect()包裹super.performUpdate()调用this.__dispose effect(() { if (updateFromLit) { updateFromLit false; super.performUpdate(); } else { // This branch is an effect run from Preact signals. // This will cause another call into performUpdate, which will // then create a new effect watching that update pass. this.requestUpdate(); } });关键点有两个每次performUpdate()都新建一个 effect用于捕获该次更新阶段update、render、updated等内对 signal 的全部访问区分触发来源如果 effect 回调由 Lit 的更新流程直接调用则执行真正的super.performUpdate()如果由 signal 变化触发则调用this.requestUpdate()调度一次新的元素更新从而形成signal 变化 → 元素更新 → 重新捕获依赖的闭环。同时mixin 还接管了连接生命周期connectedCallback()中在super.connectedCallback()之后主动requestUpdate()以便重新连接后重新渲染、重新捕获当前的 signal 访问依赖disconnectedCallback()中调用this.__dispose?.()释放 effect断开对 signal 的订阅。测试验证订阅、断开与重连测试文件 src/test/signal-watcher_test.ts 对上述行为做了完整验证watches a signal创建SignalWatcher(LitElement)元素渲染count.value修改count.value 1后 DOM 文本自动更新为count: 1unsubscribes to a signal on element disconnect元素remove()断开后修改 signal 不再触发读取readCount保持 1重新append回容器后isUpdatePending变为truemixin 触发了更新signal 再次可驱动更新读到新值count: 3类可直接实例化SignalWatcher(TestEl)对非抽象类直接实例化并正常渲染、响应更新对抽象类则保持抽象类型不被破坏。方式二watch() 指令——精准的定向 DOM 更新watch()是一个指令它接受单个 Signal渲染其当前值并订阅其更新——当 signal 变化时只更新 DOM 中对应的一部分而不会触发整个元素的重新渲染。import {LitElement, html, css} from lit; import {customElement} from lit; import {watch, signal} from lit-labs/preact-signals; const count signal(0); customElement(signal-example) export class SignalExample extends LitElement { static styles css :host { display: block; } ; render() { return html pThe count is ${watch(count)}/p button click${this._onClick}Increment/button ; } private _onClick() { count.value count.value 1; } }watch()允许非常精准的 DOM 更新这对性能可能是有好处的但老规矩一切以实测为准measure!。其代价是生命周期回调不再自动被观察signal 访问因此由 signal 派生出来的值必须用computed()包装成计算 signal 后再交给watch()。底层实现AsyncDirective subscribe peek源码 src/lib/watch.ts 展示了它的实现细节其中两个设计非常精妙peek()读取render()中通过signal.peek()返回值——peek()会读取 signal 的当前值但不建立依赖追踪。这样即使元素同时使用了SignalWatcher仅传给watch()的 signal 也不会被 mixin 的 effect 追踪从而避免 signal 更新触发整元素更新这一点在下面混用一节会展开。subscribe()订阅首次渲染时对 signal 执行signal.subscribe(callback)通过updateFromLit标志跳过 subscribe 时同步回调的首次调用此时返回值已在render()中给出后续 signal 变化时回调中调用this.setValue(value)定向更新 DOM 部分。指令还实现了disconnected()与reconnected()生命周期钩子断开时释放订阅重连时重新订阅。对于宿主元素断开与指令本次渲染未被使用两种断开原因重连后的同步回调都能保证取到最新值多余的一次setValue()会被 lit-html 的脏值检查自然过滤。测试验证不触发元素重渲染src/test/watch_test.ts 中最能说明问题的是does not trigger an element update用例元素同时使用SignalWatcher与watch(count)第一次渲染后renderCount 1count.value 1后 DOM 文本更新为count: 1但// The updated DOM is not because of an element render assert.equal(renderCount, 1, A); // The signal update does not trigger a render assert.equal(el.isUpdatePending, false);renderCount依然是 1、isUpdatePending为false——证明 signal 的更新完全由watch()指令消化元素自身完全没有参与重渲染。这正是定向更新与整元素更新两种模式的本质差异。混用 SignalWatcher 与 watch() 指令两种方式可以自由混用。当你把一个 signal 直接传给watch()时它并不在SignalWatcher所观察的回调中被访问因为内部用了peek()读取所以该 signal 的更新只会触发一次定向的 DOM 更新而不会触发整个元素的更新。反过来render()中那些通过signal.value直接读取的 signal则仍由SignalWatcher的 effect 追踪会在变化时触发整元素更新。这是一个非常实用的分工模式频繁变化、且只影响局部 DOM 的状态 → 用watch()做定向更新影响模板整体结构、或需要参与shouldUpdate()/willUpdate()等逻辑的状态 → 直接读取 signal交给SignalWatcher处理。方式三内置 html 标签与 withWatch()——自动包装本包还导出了一个专用的html模板标签可以替代 Lit 默认的html标签使用。它会自动把模板表达式中的任何 signal 包装进watch()无需手动编写import {LitElement, html, css} from lit; import {customElement} from lit; import {html, signal} from lit-labs/preact-signals; const count signal(0); customElement(signal-example) export class SignalExample extends LitElement { static styles css :host { display: block; } ; render() { return html pThe count is ${count}/p button click${this._onClick}Increment/button ; } private _onClick() { count.value count.value 1; } }注意此时模板中直接写${count}signal 实例本身而不是${count.value}——html标签会自动识别并包装。withWatch()可组合的标签包装器withWatch()是一个高阶函数它包装任意html标签函数为其附加自动包装 signal 的能力。这意味着你可以把它与其他 html 标签包装器组合使用例如 Lit 的withStatic()静态模板包装器import {withStatic} from lit/static-html.js; import {withWatch} from lit-labs/preact-signals; const myHtml withWatch(withStatic(html));在源码 src/lib/html-tag.ts 中可以看到实现withWatch(coreTag)返回一个新标签函数将模板值数组中每个instanceof Signal的值替换为watch(v)其余值原样透传。基于此包导出了两个现成的标签htmlwithWatch(coreHtml)即带 signal 自动包装能力的 HTML 模板标签svgwithWatch(coreSvg)SVG 模板同样支持。对应的测试 src/test/html-tag_test.ts 验证了模板中直接嵌入${count}修改 signal 后 DOM 自动更新。从实现看自动包装依赖instanceof Signal判断源码中留有 TODO 注释期望未来有替代方案因此混入其他库 signal 时不会被自动包装仍需手动使用watch()。三种方式的选型建议与注意事项方式触发粒度适用场景注意点SignalWatchermixin整元素更新render()视作 computed signal状态影响模板整体、需要参与更新生命周期逻辑生命周期方法内禁止写 signal防止死循环watch()指令定向更新单处 DOM高频变化的局部状态、追求最小化 DOM 操作派生值需用computed()包装与SignalWatcher混用时不会引发整元素更新内置html/svg标签与watch()相同想少写样板、模板直接嵌入 signal依赖instanceof Signal自动识别实践要点总结不要盲目追求 signalLit 的批量更新机制已经足够快性能提升需以实际测量为准不要在生命周期回调里写 signalSignalWatcher场景否则可能无限循环派生状态用computed()包装在watch()场景下生命周期回调不会被自动观察多级依赖请显式声明断开即释放两种机制mixins 的 effect 与指令的 subscribe都会在元素/指令断开时自动取消订阅避免内存泄漏API 全部来自单一入口signal、computed、effect等由preact/signals-core重导出无需额外安装。深入阅读本包官方说明packages/labs/preact-signals/README.md公共导出入口packages/labs/preact-signals/src/index.tsSignalWatchermixin 实现packages/labs/preact-signals/src/lib/signal-watcher.tswatch()指令实现packages/labs/preact-signals/src/lib/watch.tshtml/svg标签与withWatch()实现packages/labs/preact-signals/src/lib/html-tag.ts测试用例signal-watcher_test.ts、watch_test.ts、html-tag_test.ts包配置与依赖声明packages/labs/preact-signals/package.json【免费下载链接】litLit is a simple library for building fast, lightweight web components.项目地址: https://gitcode.com/GitHub_Trending/li/lit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →