Сигналы Angular приносят множество преимуществ, одним из которых является их способность легко интегрироваться с шаблонами и «автоматизировать» обнаружение изменений. Эта автоматизация означает, что компонент, настроенный со стратегией обнаружения изменений OnPush, будет перепроверен во время следующего цикла обнаружения изменений, что избавляет разработчиков от необходимости вручную внедрять ChangeDetectorRef и вызывать markForCheck. Чтобы проиллюстрировать это преимущество, рассмотрим следующий пример:

@Component({
  selector: 'app-foo',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `{{ num }}`,
})
export class FooComponent {
  num = 1;
  private cdr = inject(ChangeDetectorRef);

  ngOnInit() {
    setTimeout(() => {
      this.num = 2;
      this.cdr.markForCheck();
    }, 3000);
  }
}

Здесь мы явно вводим ChangeDetectorRef и вызываем markForCheck после обновления состояния компонента. Новый способ работы с сигналами:

@Component({
  selector: 'app-foo',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `{{ num() }}`,
})
export class FooComponent {
  num = signal(0)

  ngOnInit() {
    setTimeout(() => this.num.set(1), 3000);
  }
}

В этой улучшенной версии мы используем signal, и, что примечательно, представление по-прежнему обновляется без необходимости вручную вызывать markForCheck. В этой статье мы углубимся во внутреннюю работу этой функции, проливая свет на то, как сигналы упрощают обнаружение изменений в приложениях Angular.

На более высоком уровне абстракции вы можете представить текущий шаблон как consumer, а каждый сигнал - как producer. Когда сигнал подвергается изменению значения, он немедленно уведомляет шаблон, который впоследствии инициирует вызов markForCheck.

Angular использует класс ReactiveLViewConsumer, расширение класса ReactiveNode, для упрощения реактивной архитектуры. В этой архитектуре каждый реактивный узел соответствует узлу в реактивном графе. Эти узлы могут играть различные роли, выступая в качестве производителей реактивных значений, потребителей других реактивных значений или выполняя обе роли одновременно.

В нашем конкретном контексте ReactiveLViewConsumer служит потребителем для signals, который мы используем в наших шаблонах.

Когда Angular выполняет функцию компонента template, его начальный шаг включает получение ссылки на потребителя реактивного представления:

export function executeTemplate<T>(
    tView: TView, lView: LView<T>, 
    templateFn: ComponentTemplate<T>, 
    rf: RenderFlags, 
    context: T) {
  const consumer = getReactiveLViewConsumer(lView, REACTIVE_TEMPLATE_CONSUMER);
  ...
}

Функция getReactiveLViewConsumer просто извлекает текущий экземпляр потребителя реактивного представления из назначенного слота в LView или создает новый экземпляр, если он не существует.

Затем он переходит к вызову функции runInContext внутри класса ReactiveLViewConsumer. Эта функция отвечает за организацию следующих задач:

class ReactiveLViewConsumer {
  runInContext(
    fn: HostBindingsFunction<unknown> | ComponentTemplate<unknown>,
    rf: RenderFlags,
    ctx: unknown,
  ): void {
    const prevConsumer = setActiveConsumer(this);
    this.trackingVersion++;
    try {
      fn(rf, ctx);
    } finally {
      setActiveConsumer(prevConsumer);
    }
  }
}

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

Проще говоря, это означает, что наш потребитель реактивного представления фактически отметил и зарегистрировал каждый signal, используемый в шаблоне. Это происходит, когда мы обращаемся к signal, как вы можете видеть здесь:

class WritableSignalImpl<T> extends ReactiveNode { 
  // ...

  signal(): T {
    this.producerAccessed();
    return this.value;
  }
}

Вызывается метод producerAccessed, помечающий текущий сигнал как зависимость производителя для нашего потребителя реактивного представления.

Двигаясь дальше, когда signal подвергается обновлению значения, он вызывает метод producerMayHaveChanged, который впоследствии запускает метод onConsumerDependencyMayHaveChanged для каждого потребителя, зарегистрированного для этого производителя.

В нашем конкретном контексте класс ReactiveLViewConsumer переопределяет этот метод и при вызове, в свою очередь, инициирует операцию markViewDirty для текущего представления.

Наконец, процесс завершается вызовом функции commitLViewConsumerIfHasProducers. Эта функция служит для сохранения текущего экземпляра потребителя реактивного представления, но только если в шаблоне есть производители.

Следуйте за мной в Medium или Twitter, чтобы узнать больше об Angular и JS!