 响应信号变化,优雅处理副作用与资源清理)
Angular Signals 教程进阶用 effect() 响应信号变化优雅处理副作用与资源清理【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angularAngular 官方 signals 教程“Reacting to signal changes with effect”旨在教你掌握effect()这一 API当它读取的信号发生变化时相关函数会自动重新执行非常适合日志输出、localStorage持久化、DOM 操作与 API 调用等副作用场景。本指南基于 Angular 仓库中的 教程原文档 展开并结合 effect() 框架层实现源码 逐层剖析。学完本篇你将能判断“何时该用 effect、何时该改用computed()或linkedSignal()”并写出带自动清理、随组件销毁而释放资源的可靠副作用代码。本教程是 Angular signals 系列的第 10 步承接上一步的 通过 signal queries 查询子元素后者也明确预告了本步将学习“如何使用 effect 响应信号变化”。整个系列共 11 步位于仓库的 adev/src/content/tutorials/signals/steps 目录下。一、本活动要解决的实战场景主题管理器Theme Manager本活动内置了一个信号化改造完毕的“主题管理器”应用起点文件是 steps/10-reacting-to-signal-changes-with-effect/src/app/app.ts。组件内部已经声明了三个信号并在模板中通过按钮驱动状态变化theme signallight | dark(light); username signal(Guest); isLoggedIn signal(false); themeClass computed(() theme-${this.theme()});theme记录当前亮色/暗色主题按钮toggleTheme()负责切换isLoggedIn、username模拟用户登录状态与用户名login()/logout()负责更新themeClass由computed()从theme派生的样式类这一用法正是computed()的正确场景——它是纯派生值不需要副作用。这三个信号在组件里“静静地”变化着但当前没有任何代码会对它们的变化做出反应。本教程要做的就是为这些信号变化添加三个effect()让应用在信号更新时自动完成持久化、日志和定时任务。从模板信息看app.ts完成后的效果将包括把主题保存到localStorage记录用户登录/登出活动的变化每 5 秒执行一次定时器日志。二、理解 effect()信号世界通往命令式世界的桥梁2.1 什么是 effectEffects are functions that run automatically when their dependencies change, making them perfect for side effects like logging, DOM manipulation, or API calls.用 Angular 官方文档adev/src/content/guide/signals/effect.md中的定义来说effect 是一种操作当它读取的一个或多个信号值发生变化时会自动运行。效果函数运行的执行模型有两个关键特性至少运行一次effect 一旦创建会立即被调度执行一次异步执行effect 在变更检测change detection流程中异步运行而不是在信号被写入的瞬间同步执行动态依赖追踪与computed()相似effect 只在“最近一次执行”中读取过的信号上建立依赖之后即使函数体内新增了对某个信号的读取也仅从那时起生效。2.2 什么场景才适合用 effectAngular 给出的判断标准非常直白——“effect 应该是你最后才会想起的 API”Effects should be the last API you reach for。它的职责边界是把信号状态同步到命令式、非信号的 API 上。典型合法场景包括对应 effect.md 的 use cases记录信号值用于数据分析或调试日志与各类存储保持同步例如window.localStorage、sessionStorage、cookie 等实现模板语法无法表达的自定义 DOM 行为向canvas、图表库或其他第三方 UI 库执行自定义渲染。2.3 三条铁律何时不要用 effect原文档明确指出以下三种情况都不应该使用 effect而应换用其他 API从一个信号派生另一个值→ 使用computed()需要既能派生、又能手动设置的派生状态→ 使用linkedSignal()在信号之间复制数据→ 这说明状态设计出了问题应当把“单一事实来源source of truth”上移再用computed()或linkedSignal()派生而不是用 effect 做搬运工。官方指南 effect.md 还额外补充了一条重要警告不要用 effect 来做状态变化的传播否则可能触发ExpressionChangedAfterItHasBeenChecked错误、无限循环更新或不必要的变更检测周期。因此本活动的三个示例localStorage同步、日志、定时器都是刻意挑选的“恰到好处”的副作用场景。三、Step 1把 effect 引入组件第一个改动在 import 行把effect添加到既有导入中。// Add effect to existing imports import {Component, signal, computed, effect, ChangeDetectionStrategy} from angular/core;effect函数会创建一个响应式副作用当它读取的任何信号变化时副作用自动重新运行。3.1 为什么要在构造函数里调用 effect你可能已经注意到本教程所有effect()都写在constructor()中。这背后是框架层的硬性约束从 effect() 实现源码 可以看到if (ngDevMode !options?.injector) { assertInInjectionContext(effect); }即默认情况下effect()只能在注入上下文injection context中创建最省事的方式就是在组件、指令或服务的构造函数里调用。同时框架还会用assertNotInReactiveContext拒绝在某个响应式上下文如另一个computed或 effect 内部中同步创建 effect提示应将 effect 调度到组件构造函数这类位置。如果确实需要在构造函数之外调用例如放在某个方法中官方文档 effect.md 给出的解法是显式传入Injectoreffect( () { console.log(The count is: ${this.count()}); }, {injector: this.injector}, );四、Step 2用 effect 把主题自动持久化到 localStorage第二个改动在构造函数中添加一个“主题保存”副作用constructor() { // Save theme to localStorage whenever it changes effect(() { localStorage.setItem(theme, this.theme()); console.log(Theme saved to localStorage:, this.theme()); }); }这段代码的含义是effect 首次创建后立即执行一次——此时会把初始主题light写入localStorage执行过程中它“读取”了this.theme()因此 Angular 会自动追踪该信号为依赖之后每当用户点击按钮切换主题this.theme.set(...)被调用effect 都会自动重新运行把最新主题写入localStorage并打印日志。这正是“把 signal 状态同步到命令式、非 signal APIlocalStorage”的教科书式用法持久化属于典型副作用模板渲染和派生值都解决不了“写浏览器存储”这件事只有 effect 最合适。对应完整实现可参考 answer/src/app/app.ts。五、Step 3一个 effect 监听多个信号——用户活动日志第三个改动演示了 effect 的另一个特性可以同时读取多个信号任何一个发生变化都会触发重新运行。constructor() { // ... previous effect // Log user activity changes effect(() { const status this.isLoggedIn() ? logged in : logged out; const user this.username(); console.log(User ${user} is ${status}); }); }结合组件中的事件处理代码answer/src/app/app.ts来看点击login()会同时username.set(John Doe)与isLoggedIn.set(true)点击logout()会同时username.set(Guest)与isLoggedIn.set(false)。此时 effect 依赖了isLoggedIn()与username()两个信号。注意框架会合并同一次变更检测中的多次写入——当你点击登录时即使两个信号先后被修改日志 effect 也不会执行两遍而是只跑一次并输出最终的组合状态。这体现了 effect 的“异步、合并调度”特性是它与逐信号监听例如传统的subscribe的本质区别。六、Step 4带清理逻辑onCleanup的 effect——定时器第四个改动引入了 effect 的清理机制。effect()的回调函数会收到一个onCleanup注册函数用它登记“下次运行前 / effect 销毁时”要执行的清理逻辑constructor() { // ... previous effects // Timer effect with cleanup effect((onCleanup) { const interval setInterval(() { console.log(Timer tick - Current theme:, this.theme()); }, 5000); // Clean up the interval when the effect is destroyed onCleanup(() { clearInterval(interval); console.log(Timer cleaned up); }); }); }6.1 清理函数的执行时机这里需要准确理解onCleanup的两个触发时机effect 因依赖变化而重新运行之前如果 effect 内的this.theme()被多次读取导致 effect 重跑Angular 会先执行上一次注册的 cleanup本例中是clearInterval再执行新的 effect 体从而避免定时器重复堆积effect 自身被销毁时本组件创建的 effect 默认与组件生命周期绑定见下文 7.3 节组件销毁时 effect 被销毁cleanup 会被执行setInterval因此不会造成内存泄漏。从框架层看这一行为由 effect.ts 中的 EFFECT_NODE.cleanup 实现它以“后进先出”的方式弹出并执行所有已注册的 cleanup 函数无论执行成功与否都会清空列表为 effect 的下一次运行做准备。而 effect 的回调签名(onCleanup: EffectCleanupRegisterFn) void与EffectCleanupRegisterFn、EffectCleanupFn类型也都定义在 effect.ts 中是整个清理机制的公开 API。七、源码级解密Angular 的 effect() 内部是怎么运作的如果你想从“会用”进阶到“懂原理”packages/core/src/render3/reactivity/effect.ts 是最佳阅读对象。这个模块公开暴露了effect、EffectRef、CreateEffectOptions等 API并注释为publicApi 20.0。7.1 effect() 的完整签名与选项export function effect( effectFn: (onCleanup: EffectCleanupRegisterFn) void, options?: CreateEffectOptions, ): EffectRefeffect()返回一个EffectRef对象它只有一个方法destroy()用于手动关停 effectEffectRef 定义。而可选的配置对象CreateEffectOptions支持如下字段选项含义与默认行为injector指定在哪个Injector中创建 effect。不传时使用当前注入上下文通过inject(Injector)取得manualCleanup是否要求手动清理。默认false——effect 会自动把自己注册到当前DestroyRef随组件销毁而清理设为true且在注入上下文外使用时仍需提供injectordebugNameeffect 的调试名用于在 Angular DevTools 中识别该 effectallowSignalWrites已废弃。历史上用来允许在 effect 中写信号如今信号写入默认就允许该选项不再产生任何影响见 effect.ts 的告警逻辑7.2 两种 effectView Effect 与 Root Effect从 effect() 实现 可以看到Angular 会依据创建时的上下文把 effect 分为两类组件/视图效应View Effect在组件、指令或挂载于组件注入器的服务中创建。节点会关联到所在的LView被加入view[EFFECTS]集合并在变更检测的同步流程中作为组件生命周期事件执行——因此它可以安全地读取 input 信号或创建/销毁依赖组件状态的视图根效应Root Effect在组件树之外如根服务创建。从源码注释看它作为微任务独立调度与组件树及变更检测没有直接关联。判断逻辑是effect()内部尝试从注入器获取ViewContext拿得到就走createViewEffect否则走createRootEffect。教程中的示例都写在组件构造函数里因此全部属于 View Effect会随组件的变更检测节奏被调度执行。7.3 为什么不需要手动清理本组件的 effect一个值得注意的细节教程的示例代码并没有把每个effect()的返回值EffectRef存下来手动调用destroy()。原因在于框架的默认行为——当 effect 在组件上下文创建、且未设置manualCleanup: true时effect()会自动获取DestroyRef并注册销毁回调effect.tslet destroyRef options?.manualCleanup ! true ? injector.get(DestroyRef) : null; // ... if (destroyRef ! null) { node.onDestroyFns [destroyRef.onDestroy(() node.destroy())]; }也就是说组件销毁 ⇒ effect 自动销毁 ⇒ 执行其注册的 cleanup如clearInterval。这正是上一节定时器不会泄漏的底层保障。若你需要提前关停某个 effect例如“用户登出后停止埋点上报”则应保存返回的EffectRef并手动调用destroy()。八、Step 5在浏览器控制台验证三个 effect完成以上代码后在浏览器中打开应用并配合开发者工具控制台验证切换主题Toggle Theme点击主题切换按钮控制台应出现Theme saved to localStorage: dark或 light同时localStorage中的theme键被同步更新登录 / 登出Login/Logout点击登录应看到User John Doe is logged in登出则看到User Guest is logged out观察定时器Watch Timer保持页面打开每 5 秒控制台打印一次Timer tick - Current theme: ...。这一切都无需手动调用任何函数——effect 会随着被追踪信号的每一次变化自动重新运行。运行时行为校验中你可以顺带把登录/登出与主题切换组合操作观察日志 effect 与 localStorage effect 各自独立、按需触发的工作方式。最终的完整参考实现三个 effect 齐备、可直接对照检查位于 answer/src/app/app.ts本活动 config.json 中指定的待编辑文件为src/app/app.ts与src/app/app.css。九、关键概念回顾与使用纪律在本活动的结尾官方文档给出了四个必须牢记的核心要点Effect 是响应式的当它读取的任何信号变化时自动重新执行只做副作用适用于日志、DOM 操作、API 调用以及同步到命令式 API善用清理通过onCleanup回调释放定时器、订阅等资源自动追踪effect 自动记录它读取过的信号仅在这些信号变化时重跑动态依赖只追踪最近一次执行读取的信号。同时再次强调纪律effect 很强大但应该是你最后的选择——只有computed()纯派生值与linkedSignal()可派生也可手动设置的派生状态都无法满足时才轮到 effect 出场。需要在本教程之外的信号交互能力例如用信号管理异步数据、跨组件通信、model()双向绑定与 service/directive 中的信号用法可继续阅读 signals 教程系列的后续步骤见 steps 目录更系统的 effect 设计约束与执行细节可参阅 adev/src/content/guide/signals/effect.md。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考