惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy & Cybersecurity Law Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
宝玉的分享
宝玉的分享
V
V2EX
爱范儿
爱范儿
Last Week in AI
Last Week in AI
美团技术团队
人人都是产品经理
人人都是产品经理
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
Security Latest
Security Latest
C
Cybersecurity and Infrastructure Security Agency CISA
Know Your Adversary
Know Your Adversary
I
Intezer
K
Kaspersky official blog
阮一峰的网络日志
阮一峰的网络日志
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
AWS News Blog
AWS News Blog
小众软件
小众软件
博客园 - 司徒正美
Cyberwarzone
Cyberwarzone
NISL@THU
NISL@THU
博客园 - 三生石上(FineUI控件)
C
CERT Recently Published Vulnerability Notes
博客园 - 聂微东
量子位
有赞技术团队
有赞技术团队
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
L
Lohrmann on Cybersecurity
P
Palo Alto Networks Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园 - Franky
雷峰网
雷峰网
月光博客
月光博客
Hacker News: Ask HN
Hacker News: Ask HN
Forbes - Security
Forbes - Security
博客园 - 【当耐特】
C
Cyber Attacks, Cyber Crime and Cyber Security

Angular

关于外部 js 修改 dx-select-box 的值的可行性 2025 年 Angular SSR 达到 Next.js 的水平了吗?记得两三年前, Angular 自己的官方文档 Google 都只索引个首页,现在好像正常收录了 - V2EX Angular 这么没落了吗 - V2EX ng-dnd 3.0 发布了!🎉 - V2EX Angular 页面跳转后返回前页状态(输入的数据)丢失,有没有什么简单的包引入后就能像普通页面一样跳转后保留输入数据? - V2EX 请教下 angular computed 相关的问题 - V2EX 开源复刻了一个 Angular Canvas 动态背景组件库,欢迎大家试用/PR [创意来自本站/t/1020300] - V2EX 有人成功部署过 Angular 17 SSR 到 AWS 么? vs code 的 angular 扩展如何在关闭 strictTemplates 的情况下强制启用模板属性跳转? 被 angular17 搞疯了 - V2EX angular 17 发布 - V2EX 怎么他喵的会有这种事 - V2EX 请问 ngFor 的数组更新后,如何在获知页面渲染已完成? - V2EX 大一留学生,想找一份 Angular 的远程实习补贴日常开销 - V2EX 新手请教 Angular 学习课程 [开工礼物] 🥳 Angular V16 发布啦 - V2EX Angular 写的网站最快实现 I18n 的方法是什么,只要能切换中英文就行,不需要支持其它语言 - V2EX 现在国外有哪一个大一点的网站是用 Angular 写前端的吗? - V2EX Material Motion 的最完整实现 - Material Studies 示例应用 Reply 的 Angular 实现现已公开 - V2EX Angular 15 发布 - V2EX 有 angular 大神吗, cdk-virtual-scroll-viewport 和 formArray 一起用滚动中间顺序乱了 - V2EX 问:来个精通 Angular 的大哥帮忙看一段代码 - V2EX 为什么 Angular 的一些组件仍在使用 Promise? - V2EX 有没有大佬写过 angular 动态表单 请教关于 SSR 的问题。SSR 有可能隐藏 AngularJS 入口文件吗? Angular 14 更新 - V2EX Angular SSR 多语言的最佳实践(SEO 效果好)是什么? - V2EX Angular 的 API 请求全放在一个服务里还是分开不同服务比较好?全放一起感觉文件太长了。另外服务和 component 放一起会不会看起来更整齐一些?官方好像不是这么做 - V2EX 小白请教关于 Angular 路由的动态注册方式 - V2EX 各位有写 angular 的吗?我想问一下关于 vscode 插件的问题 - V2EX 为什么现在国内用 Angular 的网站感觉非常少? - V2EX Angular 有没有类似 OneDrive 网页版文件列表的轮子? - V2EX Angular 开发小程序 - V2EX Angular 学起来真是太难了, 国内资料少, 用 G 搜呢,总感觉表达不准确,搜不到好的结果 - V2EX 服务端渲染(SSR)怎么区分 JS 代码块是在浏览器执行,还是在 Server 端执行? - V2EX [Angular] ngx-center 单运行时多项目共享依赖解决方案 - V2EX 最近 Angular 的占有率有所上升吗? - V2EX Angular12 正式版发布 - V2EX 借着 vue 和 react 的思路,跟着 b 站视频学了三天 angluar11 后,我来吐吐槽。。。 - V2EX ng 是不是没落了,国内书籍只有 7 版本 - V2EX 像富文本编辑器,组态设计器这些传统桌面应用要是放在 Web 端实现的话,选什么框架? - V2EX Angular 9+ 升级小记 —— 应付 MSAL-Angular 与 ngx-restangular 的兼容性问题 - V2EX angular 模板不够"typescript", 是我的姿势不对吗? - V2EX 这里好冷清,想问下有没有什么 angular 论坛比较火热的? - V2EX 为什么用 Angular 的比用 Vue/React 的少那么多? - V2EX AngularGo —— 一个开源 Angular SPA 模板 - V2EX 有没有好看的 Angular/material 主题? - V2EX
Angular 新纪元: Signals RFC
Char2s · 2023-04-28 · via Angular

Angular 每半年推出一个大版本,以保持框架的持续更新。然而,自从 2020 年初 Angular 9 推出重大更新并将默认编译和渲染引擎从 View Engine 切换到 Ivy 后,Angular 的迭代似乎一直处于相当消极的状态。更新内容主要集中在跟进 TypeScript 和 RxJS 版本、小特性和性能优化方面,而一些陈年问题仍未解决。

有人认为这是 Angular 稳定成熟的表现,也有人认为这是 Google 放弃 Angular 的前兆。在社区内充斥着各种消极情绪时,Angular 团队在一年时间内接连提出了 Standalone APIs RFC 和 Signals RFC 这两个提案,推出了全新的应用组织方式和与过去完全不同的基于 Signal 的响应性和数据流方案。Angular 似乎正在步入一个新的纪元。

Signals RFC 分为主 RFC 和四个子 RFC:

本文将概述主 RFC 和所有子 RFC 的内容,不包含笔者个人观点。需要注意的是,这些只是该 RFC 当前阶段( 2023/04/12 )的内容,RFC 可能随时更新,最终的实现可能有较大出入。

Signals RFC 是近年来 Angular 团队提出的规模最大,也最具颠覆性的提案,其目标是解决数个在社区中留存数年的问题,如基于 Zone.js 的变更检测的性能问题,基于 RxJS 的状态派生较为繁琐,无法便捷地对特定 Input 的变化作出响应等。

Angular 团队根据过去几年的社区反馈制定了以下目标:

  • Angular 应当具备一个清晰而统一的数据流模型
  • Angular 应内置对于声明式的状态派生的支持
  • 只有需要更新的 UI 需要同步,该细粒度应当在单个组件或者单个组件以下
  • 与 RxJS 等 reactive 库的流畅的互操作性
  • Angular 应提供更好的护栏,以避免开发者轻易陷入大幅影响性能的变更检测陷阱,或是触发 ExpressionChangedAfterItHasBeenChecked 错误
  • Angular 应提供一种可行的路径以开发完全不依赖 Zone.js 的应用
  • Angular 应简化许多框架特定的概念,如 View 查询、Content 查询和生命周期 Hooks

而 Signals RFC 则被视为是实现以上目标的基石。

Signal APIs

Signal 指的是具有显式变化语义的值。在 Angular 中,Signal 是由调用后立即返回当前值的零参数的 Getter 函数表示的:

interface Signal<T> {
  (): T;
  [SIGNAL]: unknown;
}

该 Getter 函数具有一个 [SIGNAL] 属性,框架可以用该属性辨识 signals 以用于内部优化。

Signal 本质上应当是只读的,只用于获取当前的值以及对值的变更做出响应。

Getter 函数用于访问当前值,同时将本次 Signal 的读取操作记录在一个 Reactive Context 中,用于构建响应式依赖关系图。

在 Reactive Context 外的 Signal 读取也是被允许的。这意味者非响应式的代码,如第三方库的代码,也可以读取 Signal 的值,而无需注意其响应式的本质。

Writable Signals

Angular 的 Signal 库提供一个默认的 Writable Signal 的实现,用于通过其内置的方法变更 Signal 的值:

interface WritableSignal<T> extends Signal<T> {
  /**
   * 将 Signal 直接设置到一个新的值。
   */
  set(value: T): void;
  /**
   * 基于 Signal 的当前值以对值进行更新。
   */
  update(updateFn: (value: T) => T): void;
  /**
   * 通过对 Signal 的当前值进行直接的变化来更新 Signal 的当前值(不改变对象)。
   */
  mutate(mutatorFn: (value: T) => void): void;
  /**
   * 返回一个非 Writable 的 Signal ,该 Signal 访问当前 WritableSignal 但不允许对值进行变更。
   */
  asReadonly(): Signal<T>;
}

通过 signal 函数可以创建 WritableSignal 的实例:

function signal<T>(
  initialValue: T,
  options?: {equal?: (a: T, b: T) => boolean}
): WritableSignal<T>;

用法示例:

const counter = signal(0);
counter.set(5);
counter.update(currentValue => currentValue + 1);

Equality

可以选择性的为 WritableSignal 指定一个 Equality 比较函数,如果该 Equality 函数确定两个值是相等的,那么:

  • Signal 的值将不会被更新
  • 该变更将不会被传播

默认的 Equality 函数使用 === 来比较基本类型值,但比较对象和数组将会永远返回不等。这允许 Signal 持有非基本类型的值的同时,仍然可以传播其值的变更,例如:

const todos = signal<Todo[]>([{todo: 'Open RFC', done: true}]);
// 不使用不可变数据时,我们仍然可以更新该列表并触发值的变更。
todos.mutate(todosList => {
    todosList.push({todo: 'Respond to RFC comments', done: false});
});

Signal 概念的其他实现是可能的:只要实现相应的接口,Angular 或任何第三方库都可以创建自定义的 Signal 版本。

Getter 函数

在 Angular 选择的这一实现中,Signal 是由一个 Getter 函数表示的。使用这一 API 的优势有:

  • 这是一个 JavaScript 内置的结构,使得 Signal 的读取在 TypeScript 代码和模板表达式中保持一致
  • 其非常明确地表明其主要操作是“读取”
  • 其非常明确的表明正在发生的操作并非普通的属性读取
  • 其在语法上非常轻量(由于 Signal 读取是非常常见的操作,这是相当重要的一点)

然而,Getter 函数也有诸多弊病:

在模板中的函数调用

多年来,Angular 开发者已经学会了对在模板中调用函数保持警惕。其出现的原因是组件的变更检测运行频繁,而函数则很有可能隐藏具有高计算成本的逻辑。

这些担忧并不适用于 Signal 的 Getter 函数。Getter 函数是相当高效的访问器,计算成本极低,因此反复频繁地调用 Signal 的 Getter 函数并不是一个问题。

然而,在 Signal 读取中使用函数调用仍然可能在会在刚开始时让那些习惯于在模板中避免函数调用的开发者感到困惑。

与类型缩窄的交互

TypeScript 可以在条件语句中缩窄一个表达式的类型。即使 user.name 可以被赋值为 null,以下代码依然可以通过类型检测,这正是因为 TypeScript 知道在 if 的主体中, user.name 不可能是 null

if (user.name) {
  console.log(user.name.first);
}

然而,TypeScript 并不会缩窄函数返回值的类型,因为其不能保证每个函数都像 Signal 函数这样每次都会返回相同的值。因此,上面的示例并不适用于 Signal:

if (user.name()) {
  console.log(user.name().first); // 类型错误
}

对于这个简单的示例,我们可以很简单地将 user.name() 提取为一个在 if 语句外的常量:

const name = user.name();
if (name) {
  console.log(name.first);
}

然而在模板中,由于无法声明中间变量,这种方式将不在奏效。目前只能依赖一些 Workaround 来解决该问题。

替代语法

Angular 团队考虑了多种 Getter 函数的替代语法,包括 Vue 的 .value 语法,React 的 [value, setValue] 语法,Vue 的 Proxy 响应性,和 Marko/Svelte 的编译时响应性。详见 RFC 原文

Computed Signals

Computed Signal 是从一个或多个依赖值派生出的新的值,会在其依赖的值变更时更新。Computed Signal 也可以依赖其他 Computed Signals 。

示例:

const counter = signal(0);
const isEven = computed(() => counter() % 2 === 0);
const color = computed(() => isEven() ? 'red' : 'blue');

computed 函数的签名是:

function computed<T>(
  computation: () => T,
  options?: {equal?: (a: T, b: T) => boolean}
): Signal<T>;

computed 函数的参数中,名为 compuatation 的函数不应该有任何副作用 —— 对任何 Signal 的变更都应当被避免。Angular 的 Signal 库将会检测在 computation 函数中对其他 Signal 的变更并抛出异常。

与 Writable Signal 类似,Computed Signal 同样可以可选地指定一个 Equality 函数,用于在两个值被确定为相等时阻止更深层的依赖链的重新计算。

示例(默认的 Equality 函数):

const counter = signal(0);

const isEven = computed(() => counter() % 2 === 0);

const color = computed(() => isEven() ? 'red' : 'blue');

// 当将 counter 设置到一个不同的奇数值时:
// - isEven 将必定重新计算(其依赖的值变更了)
// - color 无需重新计算( isEven 的值没有变化)
counter.set(2);

Angular 选择的用于实现 Computed Signal 的算法对计算的时机和正确性具有强有力的保证:

  • 懒计算: 在 Computed Signal 的值被读取之前,其 computation 函数不会被调用
  • 自动销毁: 一旦 Computed Signal 的引用超出作用域,其就自动符合 GC 的条件。Angular 的 Signal 库不会暴露任何显式的清理操作。
  • 防抖计算: 对于依赖项的值的变更,Angular 会保证 computation 函数被调用的次数是最少的。computation 函数永远不会为过时的值或是中间值执行,并且对知名的 "Diamond Dependency Problem" 免疫。防抖计算无需任何显式的“事务”或“批量”操作。

Computed Signal 中的条件分支

Computed Signal 会跟踪在 computation 函数中读取了哪些 Signal ,以便知道何时需要重新计算。这个依赖集是动态的,并且会随着每次计算自我调整。因此,在该条件计算中:

const greeting = computed(() => showName() ? `Hello, ${name()}!` : 'Hello!');

如果 showName 的值改变了, greeting 总是会重新计算,但如果 showName 的值为 false,那么 name 就不会是 greeting 的依赖,其值的变化也就不会导致 greeting 重新计算。

Effect

Effect 是具有副作用的操作。每当其中读取的任何一个 Signal 的值更改时,Effect 会自动安排重新运行。

声明 Effect 的基本 API 签名如下:

function effect(
  effectFn: (onCleanup: (fn: () => void) => void) => void,
  options?: CreateEffectOptions
): EffectRef;

用法示例:

const firstName = signal('John');
const lastName  = signal('Doe');

// 这个 Effect 会向控制台输出 firstName 和 lastName ,并且每当他们的值变更时都会重新向控制台输出。 
effect(() => console.log(firstName(), lastName()));

Effect 具有多种用途,包括:

  • 在多个独立的模型之间同步数据
  • 触发网络请求
  • 执行渲染

Effect 函数可以选择性地注册一个清理函数。清理函数会在下一次运行改 Effect 前被执行,可以用于“取消”上一次运行 Effect 时遗留的工作。例如:

effect((onCleanup) => {
    const countValue = this.count();

    let secsFromChange = 0;
    const id = setInterval(() => {
      console.log(
        `${countValue} 在 ${++secsFromChange} 秒内没有变化`
      );
    }, 1000);

    onCleanup(() => {
      console.log('清理并重新安排 Effect');
      clearInterval(id);
    });
});

Effect 的运行时机

在 Angular Signal 库中,Effect 总会在更改 Signal 的操作完成后运行。

鉴于 Effect 用途的多样性,可能存在各种不同的运行时机。这就是实际的 Effect 运行时机不能被保证,并且 Angular 可能会选择不同策略的原因。应用程序开发人员不应依赖于任何观察到的执行时间。唯一可以保证的是:

  • Effect 会运行至少一次
  • Effect 会在其依赖的值变更后的某一时间点运行
  • Effect 运行的次数会被尽可能减小:如果一个 Effect 依赖多个 Signal ,并且其中的几个 Signal 的值同时改变了,Effect 只会被安排运行一次

终止 Effect

每当一个 Effect 的依赖项的值变更,该 Effect 都会被安排运行。从这个角度上看,Effect 似乎是“永生”的,其永远准备着对 Reactive Graph 中发生的变化做出响应。这样的无限寿命显然不是我们想要的,因为 Effect 应当在应用结束或是其他 Lifespan Scope 结束时被终止。

Angular Effect 的默认寿命是与框架中的 DestroyRef 相关联的。也就是说,Effect 会尝试注入当前的 DestroyRef 实例,然后在其中注册其终止函数。

对于需要更详细的寿命控制的情景,可以在创建 Effect 时启用 manualCleanup 选项:

effect(() => {...}, {manualCleanup?: boolean});

当该选项启用时,即使创建该 Effect 的组件或是指令已被销毁,该 Effect 也不会被终止。

可以使用创建 Effect 时返回的 EffectRef 实例手动终止该 Effect:

const effectRef = effect(() => {...});
effectRef.destroy();

在 Effect 中更改 Signal 的值

Angular 团队普遍认为在 Effect 中更改 Signal 的值可能会导致预料外的表现(如无限循环)和难以跟踪的数据流。因此,任何尝试从 Effect 中更改 Signal 的值的操作都会向控制台输出一个错误并被阻止。

可以通过在创建 Effect 时启用 allowSignalWrites 选项来覆写该默认行为,如:

const counter = signal(0);
const isBig = signal(false);

effect(() => {
    if (counter() > 5) {
        isBig.set(true);
    } else {
        isBig.set(false);
    }      
}, {allowSignalWrites: true});

注意,一般来说使用 Computed Signal 是一种更声明式,更直观,且更可预测的同步数据的方式:

const counter = signal(0);
const isBig = computed(() => counter() > 5);

基于 Signal 的组件

剩下一大半因长度限制搬不过来,见知乎原文: https://www.zhihu.com/question/595093745/answer/2981152934