信号和可观察到的甜点ð
#javascript #网络开发人员 #angular #signals

Chau Tran合作。

迁移波向信号是真实的,状态管理库已开始增加支持以支持可观察到和信号。

这是我与Chau一起拍摄的,将可观察物和信号组合成一个。

小历史

Angular自开始以来就具有可观察到的东西,并且开发人员已使用并在其应用程序的几乎每个部分中都使用了它。

Angular的反应性 rxjs (除Zone.js自动化更改检测)!

然后,信号ð来了!他们改变了一切!一般而言,我们看到模板的方式,改变角度的检测和反应性!

谁会想到角度建议在模板中调用功能?!

@Component({
    template: `<div>Count: {{ count() }}</div>`
})
export class MyCmp {
    count = signal(0);
}

(我做了大声笑!在这里阅读更多:It’s ok to use function calls in Angular templates!

信号很棒!但是我们习惯了Angular的RXJS模式,我们的服务与RXJS主题,可观察者,操作员和其他所有内容相关联!

示例方案

我有一个 GalleryComponent 从API中检索一些数据,它取决于ID(从路由参数检索)和全局表单过滤器。

@Component({
    template: `
        <div *ngIf="data$ | async as data">
            {{ data | json }}
        </div>
    `
})
export class GalleryComponent {
    private route = inject(ActivatedRoute);
    private galleryService = inject(GalleryService);
    private filterService = inject(GlobalFilterService);

    galleryId$ = this.route.paramMap.pipe(map(p => p.get('id')!));

    data$ = combineLatest([
        this.filterService.filters$,
        this.galleryId$
    ]).pipe(
        switchMap(([filters, id]) => 
            this.galleryService.getGalleryItems(id, filters)
        )
    );

    favoritesCount$ = this.data$.pipe(map(data => getFavoritesCount(data)));
}

我想在模板中使用信号以提高性能,并且因为它们是未来吗?但是,我不想摆脱RXJ,我认为它们彼此补充!

角辅助功能

我要做的第一件事是使用toSignal来包装我的data$可观察。

data = toSignal(this.data$, { initialValue: [] });

现在,我可以直接在模板中使用我的数据作为信号,而无需异步管!伟大的!

因为favoritesCount$仅取决于data$,所以我也可以将其转换为信号。计算是完美的选择!

- favoritesCount$ = this.data$.pipe(map(data => getFavoritesCount(data)));
+ favoritesCount = computed(() => getFavoritesCount(this.data()));

完美!

我从BehaviorSubjectsRxAngular / ComponentStore更新我的GlobalFilterService以使用信号!< / p>

,现在我无法在combineLatest中使用我的filters(现在是一个信号)!让我们将其转换回可观察的可观察,以便再次在combineLatest中使用它!

让我们从Angular中使用toObservable

@Component()
export class GalleryComponent {
    ...
    private filterService = inject(GlobalFilterService);

+   filters$ = toObservable(this.filterService.filters);

    data$ = combineLatest([
-       this.filterService.filters$,
+       this.filters$,
        this.galleryId$
    ]).pipe(...);
}

直到现在!问题是,我需要在组件中添加输入!此输入将在API调用中使用!因此,我使用一个设置器和BehaviorSubject创建它。

showStars$ = new BehaviorSubject(false);

@Input({ required: true }) set showStars(x: boolean) {
    this.showStars$.next(x);
}

我们可以轻松地在combineLatest中使用它,因为它只是可观察到的!但是我还需要在模板中使用它,所以我再次使用toSignal将其转换!

showStars = toSignal(this.showStars$);

再次很棒!事实是Angular RFC向我们展示了一种新的输入可能会出现RFC;

因此,我们可能想在它们来时做准备,以便于对它们进行重构,因此我们可能想到的是,让Setter设置值将值设置为signal而不是BehaviorSubject,然后从那里将其转换为observable

我们得到了类似的东西:

showStars = signal(false);
showStars$ = toObservable(this.showStars);

@Input({ required: true }) set showStars(x: boolean) {
    this.showStars.set(x);
}

一切看起来都像这样:

@Component()
export class GalleryComponent {
    private route = inject(ActivatedRoute);
    private galleryService = inject(GalleryService);
    private filterService = inject(GlobalFilterService);

    galleryId$ = this.route.paramMap.pipe(map(p => p.get('id')!));
    filters$ = toObservable(this.filterService.filters);

    showStars = signal(false);
    showStars$ = toObservable(this.showStars);
    @Input({ required: true }) set showStars(x: boolean) {
        this.showStars.set(x);
    }

    data$ = combineLatest([
        this.filters$,
        this.galleryId$,
        this.showStars$
    ]).pipe(
        switchMap(([filters, id, showStars]) => 
            this.galleryService.getGalleryItems(id, filters, showStars)
        )
    );

    data = toSignal(this.data$, { initialValue: [] });

    favoritesCount = computed(() => getFavoritesCount(this.data()));
}

所有这些来回的和决策困境,因为我们只需要将价值放入combineLatest中,并为明天的角度做好准备。

如果我们有更好的替代方案,那也可以考虑信号?

Chau的首次镜头

Chau是我与新信号交谈的第一批开发人员之一,他真的很夸张!他经常考虑这些模式!并向我展示了他为将信号与可观察物相结合的解决方案。

用法(与初始示例不同)看起来像这样:

githubUsers = computed$(
    this.query,
    pipe(
      debounceTime(500),
      switchMap((query) => api.getUsers(query)),
      startWith([])
    )
);

// or with explicit initial value

readonly githubUsers = computed$(
    this.query,
    [], // initial value
    pipe(
      debounceTime(500),
      switchMap((query) => api.getUsers(query))
    )
);

computed$是一个原始的,第一个参数可以是信号,可观察到的,并且第二个arg可以是 inationvalue 或一个管道操作员,我们可以通过所有RXJS操作员。操作员每次都会更改信号或可观察到的时发射!

在这里找到整个实现:
https://gist.github.com/eneajaho/dd74aeecb877069129e269f912e6e472

computed$解决了什么?让我们重新评估我们拥有的和想要实现的目标:

  • 我们希望我们的githubUsers成为Signal,以便我们可以在没有AsyncPipe的模板上使用它
  • 我们有一个query,它是对搜索输入反应的Signalquery值用于调用用户的github api
  • 我们想签出我们称之为github api的速率 >请不要去创建debounceSignal()。使用RXJS进行异步操作。

@angular/core/rxjs-interop提供了另外两个原语:toSignal()toObservable()为我们解决这个问题

githubUsers = toSignal(
    toObservable(this.query).pipe(
        debounceTime(500),
        switchMap((query) => api.getUsers(query))
    ),
    { initialValue: [] }
);

这种方法效果很好,但是toSignaltoObservable之间的来回来回可能很快。这就是computed$的来源。但是它仅适用于单个来源。

我的镜头

因为我有多个具有不同类型的来源(信号和可观察到),所以我将Chau的计算并转换为最初的参数为一系列来源(这可能是信号或可观察到)。作为第一次修复打字稿类型,我认为它进展顺利!在此处找到实现。

https://gist.github.com/eneajaho/53c0eca983c1800c4df9a5517bdb07a3

如果我以之前的示例为例,并应用了更新的计算$(现在称为Comput From)方法,则看起来像这样:

@Component()
export class GalleryComponent {
    private route = inject(ActivatedRoute);
    private galleryService = inject(GalleryService);
    private filterService = inject(GlobalFilterService);

    galleryId$ = this.route.paramMap.pipe(map(p => p.get('id')!));

    showStars = signal(false);

    @Input({ required: true }) set showStars(x: boolean) {
        this.showStars.set(x);
    }

    data = computedFrom(
        [this.filterService.filters, this.showStars, this.galleryId$],
        [], // initial value
        pipe(
            switchMap(([filters, showStars, id]) =>
                this.galleryService.getGalleryItems(id, filters, showStars)
            )
        )
    );


    favoritesCount = computed(() => getFavoritesCount(this.data()));
}

正如我们所看到的,无需来回转换事物即可将信号和可观察到的信号组合在一起!

正如我之前说的,这是完全键入的。而且有一个接收器,如果我们不传递Intial值,我们的信号将为Signal<T | undefined>类型,如果我们传递初始值,则它将是Signal<T>类型。

我认为,在我们必须在模板或其他计算中使用此信号的情况下,这很重要(传递初始值),因为我们首先必须检查信号是否具有值。

favoritesCount的示例:

favoritesCount = computed(() => {
    const data = this.data();
    if (data === undefined) return 0;
    return getFavoritesCount(data);
});

乔的火箭筒

我带着解决方案回到乔,他最初喜欢它!并改善它!回来说我解决方案的缺陷是initialValue

因此,如果我不提供操作员,则计算的的Intial值将为类型。但是我们不想要那个,因为我们有价值,至少我们应该在数组中取回源的价值。

让我们以一个例子来更好地理解它:

// Signal with default value
const first = signal(1); 
// Observable that emits first value synchronously
const second$ = of(1); 
// Observable that emits first value asynchronously
const third$ = timer(5000); 

const combined = computedFrom(
    [first, second$, third$],
);

我的实现将返回Signal<any>,它的价值将不确定! 不好吧?是的,当然。

我们至少想得到这样的东西:

const combined = computedFrom(
    [first, second$, third$],
);

// typeof combined - Signal<[number, number, number]>

虽然另一个示例将返回Signal<number>,并且值为0。

const combined = computedFrom(
    [first, second$, third$],
    0, // initial value
);

但是,Chau认为明确的initialValue是多余的,因为:

  • 信号已经具有初始值
  • 可观察物可以同步发射值

他相信 Observables,它发出值异步应具有其初始值明确定义定义。在上面的示例中,只有third$源需要一个初始值。

const combined = computedFrom(
    [first, second$, third$.pipe(startWith(0))],
    pipe(map(([f, s, t]) => f + s + t))
);

// if we have only one operator we can write it directly like: 
const combined = computedFrom(
    [first, second$, third$.pipe(startWith(0))],
    map(([f, s, t]) => f + s + t)
);

最后但并非最不重要的一点是,Chau的computedFrom()版本也像combineLatest()一样接受Dictionary。在某些情况下,这是一个很大的好处。

const combined = computedFrom(
    {
        first: first, 
        second: second$, 
        third: third$.pipe(startWith(0))
    },
    map(({first, second, third}) => first + second + third)
);

在此处找到Chau的计算实现:https://gist.github.com/eneajaho/33a30bcf217c28b89c95517c07b94266

总结

我现在也喜欢Chau的解决方案!并且一直在某些地方使用它,这很棒!

*去尝试一下,在评论中让我们知道或鸣叫! ð *

有关信号的其他讨论,可观察物和Libs也会查看rxangular github repo。


谢谢阅读!

我在推文上发表了很多有关Angular的信息(最新新闻,信号,视频,播客,更新,RFC,拉动请求等等)。 ð

如果您想了解更多有关现代角度功能,例如独立的,信号,功能守卫,拦截器,SSR,水合作用,新注射方法,指令组成API,ngoptimatimizedImage,请随时查看我们的Modern Angular Workshop from Push-Based.ioð16ð

如果这篇文章对您很有趣并且有用,并且您想了解有关Angular的更多信息,请在@Enea_Jahollaridev.to上跟随我。 ð

如果您愿意在buying me a coffee ☕️上支持我,我将不胜感激。 预先感谢您ð