Эффект углового сигнала не выполняется, если signal.set не вызывается как часть интервала

Я переписываю старую директиву, мне нужно использовать сигналы вместо раздутого RxJS, но я столкнулся с проблемой, которую не до конца понимаю. Это директива, которая напрямую прослушивает изменения медиа-запросов:

      @Directive({
  standalone: true,
})
export class SDKMediaQueriesMixin extends SDKSubscriptionsManager implements OnInit {

  private readonly breakpointSignal = signal<Nullable<SDKBreakpoint>>(null);

  public readonly breakpoints: LibBreakpointMap<string> = {};
  public readonly breakpointNames: LibBreakpointName[] = [];

  private matches$: Observable<(MediaQueryListEvent | Partial<MediaQueryListEvent>)[]> = EMPTY;

  constructor(@Inject(DOCUMENT) private document: Document, private applicationManager: SDKApplicationManager) {
    super();

    if (this.applicationManager.isBrowser) {
      this.breakpoints.mobile = getComputedStyle(this.document.documentElement).getPropertyValue('--mobile');
      this.breakpoints.tablet = getComputedStyle(this.document.documentElement).getPropertyValue('--tablet');
      this.breakpoints.desktop = getComputedStyle(this.document.documentElement).getPropertyValue('--desktop');
      this.breakpoints.fullhd = getComputedStyle(this.document.documentElement).getPropertyValue('--fullhd');

      this.breakpointNames = Object.keys(this.breakpoints) as LibBreakpointName[];

      const bpValues = Object.values(this.breakpoints);
      const queries = bpValues.map((bp) => `(min-width: ${bp})`);

      const matches$ = queries.map((query) => {
        const matchMedia = window.matchMedia(query);

        return fromEventPattern<Partial<MediaQueryListEvent>>(
          matchMedia.addListener.bind(matchMedia),
          matchMedia.removeListener.bind(matchMedia)
        ).pipe(startWith({ matches: matchMedia.matches, media: matchMedia.media }));
      });

      this.matches$ = combineLatest(matches$);
    }
  }

  ngOnInit() {

    if (this.applicationManager.isBrowser && this.matches$) {
      this.newSubscription = this.matches$.pipe(
        map((events) => events.map((event) => event.matches)),
        map((matches) => new SDKBreakpoint(this.breakpointNames[matches.lastIndexOf(true)]))
      )
      .subscribe((breakpoint) => {
        console.log('breakpoint is changing', breakpoint);
        this.breakpointSignal.set(breakpoint);
      });
    }
  }

  public get activeBreakpoint() {
    return this.breakpointSignal();
  };
}

В моемAppComponentЯ хочу прослушивать изменения активной точки останова, поэтому я добавилeffect:

      @Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  standalone: true,
  imports: [RouterModule, LibAnchorComponent],
  hostDirectives: [SDKMediaQueriesMixin]
})
export class AppComponent {

  constructor(private mediaQueriesMixin: SDKMediaQueriesMixin) {

    effect(() => {
      console.log('breakpoint changed', this.mediaQueriesMixin.activeBreakpoint);
    });
  }
}

Это выполняется дважды при загрузке, чтобы зарегистрировать следующее:

      app.component.ts:21 breakpoint changed null
media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'fullhd'}

Когда я меняю размер окна браузера, он регистрирует следующее:

      media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'desktop'}
media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'tablet'}
media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'fullhd'}

Однако, как вы можете видеть, я не запускаю журналы эффекта. Самое смешное, что если вместо этого я добавляю интервал, который просто рандомизирует точку останова, то все работает так, как ожидалось:

        interval(1000).pipe(
    map(() => new SDKBreakpoint(this.breakpointNames[Math.floor(Math.random() * this.breakpointNames.length)])),
  )
  .subscribe((breakpoint) => {
    console.log('breakpoint is changing', breakpoint);
    this.breakpointSignal.set(breakpoint);
  });

С помощью этого кода я получаю следующие журналы:

      media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'mobile'}
app.component.ts:21 breakpoint changed SDKBreakpoint {name: 'mobile'}
media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'fullhd'}
app.component.ts:21 breakpoint changed SDKBreakpoint {name: 'fullhd'}
media-queries.mixin.ts:67 breakpoint is changing SDKBreakpoint {name: 'tablet'}
app.component.ts:21

Сthis.breakpointSignal.set(breakpoint);в обоих сценариях работает одинаково. Я не понимаю, почему это работает только с интервалом... Что я делаю не так?

2 ответа

Как говорится в этом PR : «Раньше эффекты ставились в очередь по мере их загрязнения, и эта очередь очищалась на различных контрольных точках во время цикла обнаружения изменений. В результате обнаружение изменений выполняло функцию запуска эффектов, и без выполнения CD эффекты не выполнялись. "

Я создал для вас копию:https://stackblitz.com/edit/stackblitz-starters-aaf1qy?devToolsHeight=33&amp;amp;file=src%2Fmain.ts

Прежде чем PR будет получен, вам нужно будет запустить CD (строка 62:this.cdr.detectChanges();)

После этого эффекты будут запланированы через очередь микрозадач.

Хотя я полностью согласен с ответом ОЗ, который мне кажется верным; Могу принести еще один!

Говоря о подобной проблеме, Алекс Рикабо (один из вдохновителей реализации сигналов Angular) заявил в твиттере :

Сегодня Angular предполагает, что поток данных является однонаправленным и нисходящим, и что такое обратное распространение является ошибкой, а не сигналом о том, что обнаружение изменений необходимо запустить снова. Таким образом, обычноеsetTimeouthack здесь является «правильным ответом».

Другие вопросы по тегам