Angular Signals?依托答辩

2023-03-25 669 次浏览 前端

就在上个月(2023 年 2 月),Angular 终于也坐不住了,正式加入了 Signals 大军。

RFC: Angular Reactivity with Signals 上,Angular 为我们带来了 Angular Signals,意在提供一种细粒度响应式能力,以尝试摆脱传统的基于 zone.js 的变更检测机制。这或许是迫于竞争压力,又或是跟随业界「趋势」。

本文将介绍 Angular Signals 并给出一些个人理解和批评。

浅尝

直接上菜,示例摘自 Twitter @sarah_edo:

// 嗯,也可以在类外面用,虽然意义不明
const myValue = signal(10000);
effect(() => {
  console.log('MyValue changed', myValue());
});

@Component({
  selector: 'my-app',
  standalone: true,
  template: `
    <div>Count: {{ count() }}</div>
    <div>Double: {{ double() }}</div>

    <button (click)="inc()">Increase</button>
    <button (click)="reset()">Reset</button>

    <br>
    <!-- <test-arrays /> -->
    <!-- <test-objects /> -->

  `,
  imports: [TestArraysCmp, TestObjectsCmp],
})
export class App {
  count = signal(0);

  double = computed(() => this.count() * 2);

  countType = computed(() => (this.count() % 2 === 0 ? 'even' : 'odd'));

  constructor() {
    effect(() => {
      console.log('Count changed', this.count());
      console.log(this.count(), 'is', this.countType());
    });
  }

  inc() {
    this.count.update((c) => c + 1);
  }

  reset() {
    this.count.set(0);
  }
}

接触过 Solid.js 的朋友们估计会很熟悉,这味太冲了。

简单解释一下,调用 signal(default) 后获得一个包装函数,其是一个 getter,同时提供 set(设置值)、update(基于之前的值更新)和 mutate(突变对象内层属性)等 setter 方法。当 getter 被调用时,Angular Core 会构建依赖图追踪依赖,在 setter 调用时触发副作用。

此外,Angular Signals 还提供 computed,用于派生计算属性,以及 effect 声明副作用。其用法和 Solid.js、Vue Composition API 类似。

## 品鉴

Angular 选择 Signals 这个陈年烂货确实引发了一些争议。当时看到 RFC 的时候我的心情是「难以置信」「惊恐」,堪称魔幻。Signals 这种 primitive 在语法上天然更适合那些 Hooks 向、函数式的框架。当然不是说 Angular 就不能用,而是这就像你用勺子吃面条,多少有点别扭。

Signals 本质上是一种 value 的容器或者包装,只暴露 getter 和 setter 访问器,在调用对应访问器的时候注册依赖追踪或触发订阅。其性质上和 Vue Refs 类似,都是一种基于运行时的响应式实现。

但为什么说更适合类 Hooks 的框架呢?除了语法层面上的相似,最主要的原因是 Signals 可以对闭包内定义的原始数据类型变量追加响应性(虽然这是在增加心智负担且损害 DX 的前提下做到的)。这个道理很简单:

// 无法观察
let count = 1;
count++;

// 通过包装追加响应性
const count = ref(1);
count.value++;
// or
const [count, setCount] = createSignal(1);
setCount((prev) => prev + 1);

但 Angular... 容我吐槽一下:我记得你应该是 OOP 吧,你需要面向闭包吗?他喵的来凑什么热闹?简直是做了违背祖宗的决定,改的亲妈都不认了。

如果只是为了引入响应性的话,做法远不止上 Signals 这一种,无论是运行时还是编译时都可以做。有没有更契合 Angular 的方式呢?早就有前人的经验摆在面前,而且是 DX 无损的。比如 Vue:

export default {
    data() {
    return {
      count: 1,
    };
  },
  methods: {
    change() {
      // 没错,
      // 我不需要加 ()
      // 也不需要加 .value
      // 因为 `count` 已经是对象的属性,可以被观察
      this.count++;
    },
  },
}

OOP 风格的响应式是相对成熟的,改写成 Class 形式也是一样。那么对于 RFC 提到的其他特性,不违背祖宗的话,到底能不能实现?

能,太能了,心智负担更低,而且更 Angular:

@Component()
export default class AppComponent {
  // 或者命名为 count$,呃,一些陈旧的习俗
  @Observable count = 1;
  
  @Computed get double() {
    return this.count * 2;
  }
  
  @Computed get countType() {
    return this.count % 2 === 0 ? 'even' : 'odd';
  }
  
  // 通过 watch 来注册副作用
  // 抱歉,我接受不了 effect() 那种隐式自动推导依赖项的形式
  // 虽然那种也能实现
  // (但深入思考一下,effect 真的有必要存在吗?🤔)
  @Watch(['count'])
  someCountEffect() {
    console.log('Count changed', this.count);
  }
  
  inc() {
    this.count++;
  }
}

// PS: 其实这些是从 Vue 类组件和 MobX 上抄来魔改的

这只是我拍脑袋得到的一种设计,远没有 Signals 那么激进,仅以此抛砖引玉。除此之外,社区也还有很多尝试,如基于 Rx 的响应式方案等,或许更 Angular?

但 Angular Signals?恕我直言,依托答辩。

结语

对于 Angular 在探索和改进上的努力,尤其是将响应性能力提上日程,还是要给予肯定的。但其交出的答卷却让人大跌眼镜,在明显有更贴合自身设计的方式和社区尝试面前,Angular 团队还是选择了更「潮流」,但未必更契合的方案。谁知道是不是间接受到知名 Signals 传教士 Hevery 的影响呢 [doge]?(典:useSignal 是 Web 框架的未来

我看了不少讨论,并没获得有说服力的理由,或许 Angular 团队有基于他们场景和限制的一些考量。但这个能力,更多的感觉还是「跟风」和「将就」。从设计角度上说,我不看好这个特性,也不会去使用它。而且,新特性将导致社区的进一步分化,诸如 zone or signal, rx or signal 等等,更多的选择也意味着更大的心智负担和项目维护压力。

Good luck, Angular.


好多年没有进行技术写作了,最近几年,前端领域虽然有惊喜,但让人无语的趋势和风气也越来越多。打算开始随便写一点对娱乐圈的无逻辑吐槽,不求净化环境,但希望能引起思考和讨论,也算是我这个系列的初衷吧。

最后也抛出几个和本文相关的「暴论」,不打算解释,留给各位读者思考,说不定,有启发呢?

  1. zone.js 这套东西 ,方向其实没错
  2. Signals 绝不是前端的未来

扩展阅读


bLue 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
本文地址:https://dreamer.blue/blog/post/2023/03/25/angular-signals-holy-shit.dream

还不快抢沙发

添加新评论