Эффект углового сигнала не выполняется, если 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;file=src%2Fmain.ts
Прежде чем PR будет получен, вам нужно будет запустить CD (строка 62:this.cdr.detectChanges();
)
После этого эффекты будут запланированы через очередь микрозадач.
Хотя я полностью согласен с ответом ОЗ, который мне кажется верным; Могу принести еще один!
Говоря о подобной проблеме, Алекс Рикабо (один из вдохновителей реализации сигналов Angular) заявил в твиттере :
Сегодня Angular предполагает, что поток данных является однонаправленным и нисходящим, и что такое обратное распространение является ошибкой, а не сигналом о том, что обнаружение изменений необходимо запустить снова. Таким образом, обычное
setTimeout
hack здесь является «правильным ответом».