[angular] CDK之observers 观察者

107 阅读2分钟

observers提供了基于原生 Web 平台的观察者 API(MutationObserver) 的便捷指令,这里使用是对元素内容的的观察监测

cdkObserveContent

用于观察宿主元素的内容何时发生变化的指令。当观察到内容变化时,它就会发出一个事件

/**
 * 提供一个工厂,创建了一个新的MutationObserver对象,便于单测
 * @docs-private
 */
@Injectable({providedIn: 'root'})
export class MutationObserverFactory {
  create(callback: MutationCallback): MutationObserver | null {
    return typeof MutationObserver === 'undefined' ? null : new MutationObserver(callback);
  }
}

// 监听元素内容是否发生更改
@Injectable({providedIn: 'root'})
export class ContentObserver implements OnDestroy {
  /** 跟踪现有的mutationobserver,这样也可以重用. */
  private _observedElements = new Map<
    Element,
    {
      observer: MutationObserver | null;
      readonly stream: Subject<MutationRecord[]>;
      count: number; // 记录当前元素创建了多少个观察者 同一元素共用同一个subject, 一对多,这个多就是count个数
    }
  >();

  constructor(private _mutationObserverFactory: MutationObserverFactory) {}

  ngOnDestroy() {
    this._observedElements.forEach((_, element) => this._cleanupObserver(element));
  }

  /**
   * 观察元素的内容变化
   */
  observe(element: Element): Observable<MutationRecord[]>;

  observe(element: ElementRef<Element>): Observable<MutationRecord[]>;

  observe(elementOrRef: Element | ElementRef<Element>): Observable<MutationRecord[]> {
    const element = coerceElement(elementOrRef);

    return new Observable((observer: Observer<MutationRecord[]>) => {
      const stream = this._observeElement(element);
      const subscription = stream.subscribe(observer);
      // 取消当前订阅及断开元素观察
      return () => {
        subscription.unsubscribe();
        this._unobserveElement(element);
      };
    });
  }

  /**
   * 使用现有的MutationObserver观察给定的元素. 有就用现有的,没有就创建
   */
  private _observeElement(element: Element): Subject<MutationRecord[]> {
    if (!this._observedElements.has(element)) {
      const stream = new Subject<MutationRecord[]>();
      const observer = this._mutationObserverFactory.create(mutations => stream.next(mutations));
      if (observer) {
        observer.observe(element, {
          characterData: true,
          childList: true,
          subtree: true,
        });
      }
      this._observedElements.set(element, {observer, stream, count: 1});
    } else {
      this._observedElements.get(element)!.count++;
    }
    return this._observedElements.get(element)!.stream;
  }

  /**
   *
   */
  private _unobserveElement(element: Element) {
    // 当元素存在时,count 减1
    if (this._observedElements.has(element)) {
      this._observedElements.get(element)!.count--;
      // 当count 不存在时,说明没有订阅了,没有订阅也就没有必要还监测元素MutationObserver,所以要消除掉元素
      if (!this._observedElements.get(element)!.count) {
        this._cleanupObserver(element);
      }
    }
  }

  /** 清除指定元素的MutationObserver并删除观察元素 . */
  private _cleanupObserver(element: Element) {
    if (this._observedElements.has(element)) {
      const {observer, stream} = this._observedElements.get(element)!;
      if (observer) {
        observer.disconnect();
      }
      stream.complete();
      this._observedElements.delete(element);
    }
  }
}

/**
 * 指令,当它的相关元素发生了变化时,触发一个回调
 */
@Directive({
  selector: '[cdkObserveContent]',
  exportAs: 'cdkObserveContent',
})
export class CdkObserveContent implements AfterContentInit, OnDestroy {
  /** 在元素内每一个内容更改都会发送事件. */
  @Output('cdkObserveContent') readonly event = new EventEmitter<MutationRecord[]>();

  /**
   * 是否禁用观察内容
   */
  @Input('cdkObserveContentDisabled')
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: BooleanInput) {
    this._disabled = coerceBooleanProperty(value);
    this._disabled ? this._unsubscribe() : this._subscribe();
  }
  private _disabled = false;

  /** 防抖时间. */
  @Input()
  get debounce(): number {
    return this._debounce;
  }
  set debounce(value: NumberInput) {
    this._debounce = coerceNumberProperty(value);
    this._subscribe();
  }
  private _debounce: number;

  private _currentSubscription: Subscription | null = null;

  constructor(
    private _contentObserver: ContentObserver,
    private _elementRef: ElementRef<HTMLElement>,
    private _ngZone: NgZone,
  ) {}

  ngAfterContentInit() {
    if (!this._currentSubscription && !this.disabled) {
      this._subscribe();
    }
  }

  ngOnDestroy() {
    this._unsubscribe();
  }

  private _subscribe() {
    this._unsubscribe();
    const stream = this._contentObserver.observe(this._elementRef);

    this._ngZone.runOutsideAngular(() => {
      this._currentSubscription = (
        this.debounce ? stream.pipe(debounceTime(this.debounce)) : stream
      ).subscribe(this.event);
    });
  }

  private _unsubscribe() {
    this._currentSubscription?.unsubscribe();
  }
}

注意:cdkObserveContent回调内容是在angular工程检测外的,没有变更检测

使用

用于监听ng-content里面的内容 通过指令cdkObserveContent 回调监听内容变化

<div class="projected-content-wrapper" (cdkObserveContent)="projectContentChanged()">
  <ng-content></ng-content>
</div>