What’s Signal?
先看一下官方定义:
1 | A signal is a wrapper around a value that notifies interested consumers when that value changes. Signals can contain any value, from primitives to complex data structures. |
翻译一下:Signal是一个包装器,它包装了一个值,并在该值发生变化时通知感兴趣的消费者。Signal可以包含任何值,从基本类型到复杂的数据结构。据说signal借鉴了solid.js
的思想(看这里),但是我没有考证过。
Signal是Angular中重要的新特性,从xxx版开始引入,至xxx版本稳定,Signal的出现,彻底更改了Angular的变更检测机制,原本基于Zone.js
的变更检测机制被Signal
+ OnPush
取代,Signal的出现,使得Angular的性能得到了极大的提升。
今天,我们就来揭开Signal的神秘面纱。
Why Signal?
首先,我们来探讨一下Angular为什么要引入Signal?
在Signal之前,Angular是基于Zone.js做变更检测的,不可否认Zone.js是一个非常强大的库,但是它也有一些缺点,比如:
- 性能问题:Zone.js的性能并不是很好,特别是在大型项目中,Zone.js的性能问题会暴露的更加明显。除非你使用了
OnPush
策略,否则Zone.js会在每次异步操作后都会触发变更检测,这样会导致性能问题。 - 由于Zone.js要monkey patch所有的异步操作(在Angular app启动时),所以Angular项目在启动的时候会有一些性能损失。
- 有些异步操作无法monkey patch,比如
async/await
,Angular的解决办法是将其降级到Promise
。如今几乎所有浏览器都支持async/await
,这种做法显然不太合理。
基于以上原因,Angular Team很早就考虑移除Zone.js,但是Zone.js的变更检测机制是Angular的核心,移除Zone.js,意味着Angular无法自动进行变更检测,也就是说变更检测的触发机制由Angular通过Zone.js自动检测,变成了需要用户手动触发,而Signal就是为此而服务的。
通过Signal定义的数据,当它变化时,Angular会自动进行变更检测,这样就不需要Zone.js了,也就解决了上面提到的问题。
signal 语法
Signal语法非常简单,下面来看看如何定义和使用signal。
定义signal
1 | const count = signal(0); |
上面是一个简单的signal定义,它的初始值是0。如果要定义复杂的值,可以使用泛型:
1 | const user = signal<User>({name: 'zdd', age: 18}); // generic type |
读取signal的值
1 | // Signals are getter functions - calling them reads their value. |
设置signal的值
1 | count.set(1); |
更新signal的值(根据前一个值)
1 | // Signals can also be updated by passing a function that receives the current value and returns the new value. |
signal的分类
Signal有两种类型:Signal
和Compute Signal
。Signal
是指普通的signal,它是可读写的;Compute Signal
是指由其他signal计算得到的signal,它是只读的。
上面的count就是普通的Signal,可读写,下面我们看一个compute signal的例子。下面代码中,doubleCount是一个compute signal,它的值是由count的值乘以2得到的。
1 | const count = signal(0); |
Signal | Compute Signal | |
---|---|---|
读写性 | 可读写 | 只读 |
一个例子
我们以一个todo list为例,来看看Signal的使用。首先我们来定义List组件:
1 | // list-item.model.ts |
在上面的代码中,我们使用readonly listItems = signal<ListItem[]>([...]);
定义了一个signal, 它的初始值是一个包含两个ListItem的数组。我们还定义了一个addItem
方法,用来添加一个新的ListItem,以及一个removeItem
方法,用来删除一个ListItem。
signal与普通的js变量不同,它的读取和写入需要特殊的语法。
- 读取signal的值:
this.listItems()
- 注意不是this.listItems
,后面要加括号,有点像函数调用。 - 写入signal的值:
this.listItems.set(newValue)
1 | <!--list.component.html--> |
然后我们来定义ListItem组件:
1 | // list-item.model.ts |
Compute Signal
Compute Signal
是指依赖于其他Signal计算得到的Signal,它是只读的,当依赖的signal值变化时,compute signal的值也会相应变化。下面是一个例子:
1 | const count: WritableSignal<number> = signal(0); |
Computed signal
有两个重要特性:
- lazy load - 直到你第一次读取它的值时,它才会计算。
- memoization - 第一次读取后缓存值,当依赖的signal值没有变化时,它不会重新计算,而是直接读取缓存的值。当依赖的signal值变化时,重新计算并缓存。
比较函数
signal是如何比较新旧值的呢?signal使用Object.is
来比较新旧值,如果新旧值相等,那么signal不会触发变更检测。对于js对象来说,Object.is
相当于引用(地址)比较,也就是说只有两个对象引用相等时,Object.is
才会返回true。
1 | const a = [1, 2, 3]; |
创建signal时,你可以指定一个比较函数来改变默认的比较方式,比如使用lodash
的isEqual
函数来进行深比较,这样当两个对象的值相等时,signal也会触发变更检测。
1 | import _ from 'lodash'; |