2021-08-25 angular9学习(一)—— ChangeDetection和属性型指令

萧树
2023-12-01

1.构建并运行angular应用

官方文档讲解:构建并运行angular应用

2.ChangeDetection

作用:检测程序内部状态,然后反映到UI上。

引起状态变化:Events(事件)、XHR(异步请求)、Timers(定时器)。
ApplicationRef监听NgZoneonTurnDone,然后执行检测。

默认策略

在angular中,每个组件渲染的时候都有对应的状态变化。每个组件树对应着一个ChangeDetection(状态)树。当angular组件发生状态变化时,angular的默认策略会把整个ChangeDetection(状态)树跑一遍,去检测到底谁发生了变化,并且应该把这个变化反映到UI的哪个地方。这种默认的全局检测策略,在大型的项目中是很消耗性能的。

OnPush策略

因此这里引入OnPush策略:哪里状态发生了变化才检测哪里,其他没有发生状态变化的地方,除非手动触发检测机制,否则不会进行检测,避免了把整个树跑一遍。

使用方式:在组件对应的.ts文件引入

import {ChangeDetectionStrategy} from '@angular/core';

@Component({
  selector: 'app-hello',
  templateUrl: './hello.component.html',
  styleUrls: ['./hello.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush // 使用
})

3. ChangeDetectorRef

angular 会在我们的组件发生变化的时候,对我们的组件执行变化检测,如果检测到我们的数据发生了变化,就会执行某些操作,如修改绑定数据的时候更新视图。这样一来,当我们的组件数据比较多的时候,angular就会有很多操作在静悄悄地进行,因此,就需要ChangeDetectorRef来实时检测数据的变化并更新视图数据。

1、 引入ChangeDetectorRef模块

import { ChangeDetectorRef } from “angular”;

2、声明

constructor(private cd:ChangeDetectorRef) {}

3、使用

this.cd.detectChanges(); // 实时检测页面及其子元素的变化

ChangeDetectorRef的全部检测方法

  1. markForCheck() : 当输入已更改或视图中发生了事件时,组件通常会标记为脏的(需要重新渲染)。调用此方法会确保即使那些触发器没有被触发,也仍然检查该组件。在组件的 metadata 中如果设置了 changeDetection: ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法。

  2. detach() : 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。

  3. reattach() : 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测。

  4. detectChanges() -:从该组件到各个子组件执行一次变化检测,检查该视图及其子视图。

4. 属性型指令(改变宿主行为)

Renderer2 和 ElementRef

Angular不提倡直接操作DOM,对于DOM的操作应该通过Renderer2 来进行。ElementRef可以理解成指向DOM元素的引用。

以下通过一个自定义拖拽指令来理解属性型指令的使用及自定义:
1、通过ng g directive drag新建一个拖的指令文件

import {Directive, ElementRef, HostListener, Input, Renderer2} from '@angular/core';
import {DragDropService} from "./drag-drop.service";

@Directive({
  selector: '[app-draggable]' //默认为appDrag,可自定义修改对外的指令属性名称
})
export class DragDirective {
  private _isDraggable = false;

  /**
   * 这里通过 @Input('app-draggable') 让 app-draggable 指令也变为一个可输入的指令(通过true/false来控制指令是否起作用)
   * 例如,当html页面里的元素使用该属性指令时,[app-draggable] = "true",
   * 相当于调用了 set isDraggable(val: boolean) 方法,set方法接收的参数就为[app-draggable]指令设置的值
   * set/get 方法不是必须的,只是当你要做额外的操作时,可以在对应的输入属性的set/get 方法中进行操作
   */
  @Input('app-draggable')
  set isDraggable(val: boolean) {
    this._isDraggable = val;
    // 要让元素可拖拽,必须给目标元素设置一个 可拖拽 的属性,通过传入的 val 决定是否可拖拽
    this.rd.setAttribute(this.el.nativeElement, 'draggable', `${val}`)
  }

  get isDraggable() {
    return this._isDraggable;
  }

  // 可以给属性指令绑定多个额外的输入属性,这里绑定了一个class名称
  @Input() draggedClass: string = '';
  @Input() dragTag: string = '';
  @Input() dragData: any;

  constructor(
    private el: ElementRef,
    private rd: Renderer2,
    private service: DragDropService,
  ) { }

  @HostListener('dragstart', ['$event'])
  onDragStart(ev: Event) {
    // 事件为当前的目标元素触发
    if (this.el.nativeElement === ev.target) {
      // 给目标元素添加class
      this.rd.addClass(this.el.nativeElement, this.draggedClass);
      this.service.setDragData({tag: this.dragTag, data: this.dragData});
    }
  }

  @HostListener('dragend', ['$event'])
  onDragEnd(ev: Event) {
    if (this.el.nativeElement === ev.target) {
      // 移除目标元素的class
      this.rd.removeClass(this.el.nativeElement, this.draggedClass);
    }
  }

}

2、通过ng g directive drop新建一个拖的指令文件

import {Directive, ElementRef, HostListener, Input, Renderer2, Output, EventEmitter} from '@angular/core';
import {DragData, DragDropService} from "./drag-drop.service";
import {take} from "rxjs/operators";

@Directive({
  selector: '[app-droppable]'
})
export class DropDirective {
  @Output() dropped = new EventEmitter<DragData>();
  @Input() dragEnterClass: string = '';
  @Input() dropTags: string[] = [];
  private data$: any;

  constructor(
    private el: ElementRef,
    private rd: Renderer2,
    private service: DragDropService,
  ) {
    // 加上操作符 take(1),防止拖拽被触发多次,导致dragData已经清空后再次触发指定定义的事件导致控制台报错
    this.data$ = this.service.getDragData().pipe(take(1));
  }

  @HostListener('dragenter', ['$event'])
  onDragEnter(ev: Event) {
    ev.preventDefault();
    ev.stopPropagation();
    if (this.el.nativeElement === ev.target) {
      this.data$.subscribe((dragData: DragData) => {
        if (this.dropTags.indexOf(dragData.tag) > -1) {
          this.rd.addClass(this.el.nativeElement, this.dragEnterClass);
        }
      });

    }
  }

  @HostListener('dragover', ['$event'])
  onDragOver(ev: Event) {
    ev.preventDefault();
    ev.stopPropagation();
    if (this.el.nativeElement === ev.target) {
      this.data$.subscribe((dragData: DragData) => {
        if (this.dropTags.indexOf(dragData.tag) > -1) {
          this.rd.setProperty(ev, 'dataTransfer.effectAllowed', 'all');
          this.rd.setProperty(ev, 'dataTransfer.dropEffect', 'move');
        } else {
          this.rd.setProperty(ev, 'dataTransfer.effectAllowed', 'none');
          this.rd.setProperty(ev, 'dataTransfer.dropEffect', 'none');
        }
      });

    }
  }

  @HostListener('dragleave', ['$event'])
  onDragLeave(ev: Event) {
    ev.preventDefault();
    ev.stopPropagation();
    if (this.el.nativeElement === ev.target) {
      this.data$.subscribe((dragData: DragData) => {
        if (this.dropTags.indexOf(dragData.tag) > -1) {
          this.rd.removeClass(this.el.nativeElement, this.dragEnterClass);
        }
      });

    }
  }

  @HostListener('drop', ['$event'])
  onDrop(ev: Event) {
    ev.preventDefault();
    ev.stopPropagation();
    if (this.el.nativeElement === ev.target) {
      this.data$.subscribe((dragData: DragData) => {
        if (this.dropTags.indexOf(dragData.tag) > -1) {
          this.rd.removeClass(this.el.nativeElement, this.dragEnterClass);
          // this.dropped.emit(dragData);
          this.service.clearDragData();
        }
      });

    }
  }
}

3、html文件及对应的scss

<div class="wra">
  <div class="left" [app-draggable]="true" [dragTag]="'task-list'"
       [dragData]="[1,2,3,4,5]" [draggedClass]="'drag-start'"
       app-droppable [dragEnterClass]="'drag-enter'" [dropTags]="['task-item1', 'task-list1']">
    <div class="item" *ngFor="let item of [1, 2, 3, 4, 5]" [app-draggable]="true" [dragTag]="'task-item'"
         [dragData]="item" [draggedClass]="'drag-start'">
      This list item is {{item}}
    </div>
  </div>
  <div class="right" [app-draggable]="true" [dragTag]="'task-list1'"
       [dragData]="[6,7,8,9]" [draggedClass]="'drag-start'"
       app-droppable [dragEnterClass]="'drag-enter'" [dropTags]="['task-item', 'task-list']">
    <div class="item" *ngFor="let item of [6, 7, 8, 9]" [app-draggable]="true" [dragTag]="'task-item1'"
         [dragData]="item" [draggedClass]="'drag-start'">
      This list item is {{item}}
    </div>
  </div>
</div>

// ===========================
.wra {
  width: 100%;
  display: flex;
}

.left, .right {
  margin-left: 20px;
  border: 1px solid pink;
  padding: 10px;
  border-radius: 4px;
}

.item {
  line-height: 30px;
  width: 200px;
  padding: 0 10px;
  border: 1px solid green;
  margin: 10px 0;
  border-radius: 4px;
}

.drag-start {
  opacity: 0.5;
  border: 2px dashed #ff525b;
}

.drag-enter {
  background-color: rgba(0, 0, 0, 0.4);
}

4、指令里面使用到的service文件

import { Injectable } from '@angular/core';
import {BehaviorSubject, Observable} from "rxjs";

export interface DragData {
  tag: string; // 标记是哪个元素触发的拖拽,唯一性
  data: any; // 传递的数据
}

@Injectable({
  providedIn: 'root'
})
export class DragDropService {

  private _dragData = new BehaviorSubject<DragData | null>(null);

  constructor() { }

  setDragData(data: DragData) {
    this._dragData.next(data);
  }

  getDragData(): Observable<DragData | null> {
    return this._dragData.asObservable();
  }

  clearDragData() {
    this._dragData.next(null);
  }
}
 类似资料: