Angular基础总结(三)

376 阅读4分钟

组件投影

📝shadow.component.html

<p>	shadow works!
    <ng-content select=".head"></ng-content>
    <ng-content select="[attr-content]"></ng-content>
    <ng-content select="section"></ng-content>
</p>

📝app.component.html

<!-- 组件投影 -->
<app-shadow>
    <p class="head">头部投影内容</p>
    <section>标签选择器投影内容</section>
    <p attr-content>自定义属性选择器投影内容</p>
</app-shadow>

✅结果展示

ViewChild

📝parent.component.ts

import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ChildComponent } from './child/child.component';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
        export class ParentComponent implements OnInit,AfterViewInit {

  // @ViewChild('box',{static:true}) private box:ElementRef //获取dom
  // @ViewChild(ChildComponent,{static:true}) private instance:ChildComponent  //获取子组件方式一
  @ViewChild('childcomponent',{static:true}) private childInstance:ChildComponent //获取子组件实例方式二

  constructor() { //执行秩序(1)
    // console.log('constructor',this.box);
    // console.log('constructor',this.instance?.childdata);
    // console.log('constructor',this.childInstance?.childdata);
    console.log('constructor',this.childInstance);
    
  }
  ngOnInit(): void {//(2)
    // console.log('ngOnInit',this.box.nativeElement);
    // console.log('ngOnInit',this.instance.childdata);
    console.log('ngOnInit',this.childInstance);
    // console.log('ngOnInit',this.childInstance.childdata);
    
  }
  ngAfterViewInit(): void { //(3)在变更检测后执行,能保证获取到元素
    //  console.log('ngAfterViewInit',this.box.nativeElement);
    //  console.log('ngAfterViewInit',this.instance.childdata);
     console.log('ngAfterViewInit',this.childInstance.childdata);
     
  }

ViewChildren

📝parent.component.html

<!-- ViewChildren批量获取元素和组件 -->
<div class="boxs" #boxs>mybox</div>
<div class="boxs" #boxs>mybox</div>
<div class="boxs" #boxs>mybox</div>
<app-child></app-child>
<app-child></app-child>
<app-child></app-child>

📝parent.component.ts

import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ChildComponent } from './child/child.component';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit,AfterViewInit {

  // ViewChildren批量获取元素和组件
  @ViewChildren(ChildComponent) private childs:QueryList<ChildComponent>
  @ViewChildren('boxs') private childs1:QueryList<ChildComponent>
  constructor() { 
  }
  ngOnInit(): void {
  }
  ngAfterViewInit(): void { //(3)
     console.log(this.childs);
     console.log(this.childs1);  //输出QueryList数组 
      //QueryList中监听子组件变化的回调函数
     this.childs1.changes.subscribe((changes)=>{
       console.log(changes);
       
      })
  }
}

ContentChild

获取投影中的dom元素、指令、组件

📝content-parent.html

<app-content-child>
    <div class="head" #head>head</div>
    <div class="main">main</div>
    <div class="footer">footer</div>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
</app-content-child>

📝content-child.html

<p>content-child works!</p>
<ng-content></ng-content>

📝content-child.ts

import { AfterViewInit, Component, ContentChild, ElementRef, OnInit } from '@angular/core';
import { ContentBoxComponent } from '../content-box/content-box.component';

@Component({
  selector: 'app-content-child',
  templateUrl: './content-child.component.html',
  styleUrls: ['./content-child.component.css']
})
export class ContentChildComponent implements OnInit,AfterViewInit {
  @ContentChild('head',{static:true}) private head:ElementRef //获取投影中的dom元素
  @ContentChild(ContentBoxComponent) private contentbox:ElementRef //获取组件
  @ContentChild('cbox',{static:true}) private cbox:ElementRef // 有多个只会获取第一个
  constructor() { }

  ngOnInit(): void {
  }
  ngAfterViewInit(): void {
    console.log(this.head);
   console.log(this.contentbox);
    console.log(this.cbox);
    
  }
}

ContentChildren

用法类似ViewChildren, 批量获取投影中到组件或指令。

默认只批量获取直属组件,获取所有组件需开启:{ descendants: true }

📝content-parent.html

<p>content-parent works!</p>
<app-content-child>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
    <app-content-box #cbox></app-content-box>
    <div class="container">
        <app-content-box #cbox></app-content-box>
        <app-content-box #cbox></app-content-box>
    </div>
</app-content-child>

📝content-child.ts

import { AfterViewInit, Component,ContentChildren,OnInit, QueryList } from '@angular/core';
import { ContentBoxComponent } from '../content-box/content-box.component';

@Component({
  selector: 'app-content-child',
  templateUrl: './content-child.component.html',
  styleUrls: ['./content-child.component.css']
})
export class ContentChildComponent implements OnInit,AfterViewInit {
	//ContentChildren没有{static:true}属性
  @ContentChildren(ContentBoxComponent) private cboxs:QueryList<ContentBoxComponent>
  @ContentChildren('cbox',{descendants: true}) private cboxss:QueryList<ContentBoxComponent>
   constructor() { }

  ngOnInit(): void {
  }
  ngAfterViewInit(): void {
    console.log(this.cboxs);//只获取直属投影组件
    console.log(this.cboxss);//获取全部投影组件   
  }
}

自定义管道

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'pipepow'
})
export class PipepowPipe implements PipeTransform {

  transform(value: number, exponent?: number): number {
      //value:底数	exponent:指数
    return Math.pow(value,isNaN(exponent) ? 1: exponent);
  }
}

在相应的Module中导入并使用

<div> {{8| pipepow}}</div>
<div> {{8| pipepow:3}}</div>
<div> {{value | pipepow:2}}</div>
<div>Boost factor: <input [(ngModel)]="factor"></div>
<div>Normal power: <input [(ngModel)]="power"></div>
<div> {{factor | pipepow:power}}</div>

非纯管道

默认的管道都是纯的,Angular 会忽略复合对象中的变化,即管道只会检查原始值或对象引用

可如果数组中的元素变化,增删改,由于引用没有变化,不会执行变更检测,所以不会执行管道的逻辑

Pipe2Pipe

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'pipe2',
  pure: false //非纯管道开启方式一
})
export class Pipe2Pipe implements PipeTransform {

  transform(allheros: any): any { //赛选会飞的英雄
    return allheros.filter((item) => item.canFly);
  }

}

AppComponent

import { Component } from '@angular/core';
interface Hero {
  id: string;
  name: string;
  canFly?: boolean;
}

const HEROES = [
              {
                id: 'hero_0',
                name: '盖伦',
                canFly: false
              },
              {
                id: 'hero_1',
                name: '赵信',
                canFly: false
              },
              {
                id: 'hero_2',
                name: '嘉文',
                canFly: false
              },
              {
                id: 'hero_3',
                name: '易大师',
                canFly: false
              },
              {
                id: 'hero_3',
                name: '泰达米尔',
                canFly: true
              }
          ];
@Component({
  selector: 'app-root',
  template: `
     <input type="text" #box (keyup.enter)="addHero(box.value)" placeholder="hero name" />
     <button (click)="reset()">Reset</button>
     <div *ngFor="let hero of (heroes | pipe2)">
       {{hero.name}}
     </div>
  `,
})
export class AppComponent {
    heroes: Hero[] = [];
     canFly = true;
     constructor() {
       this.reset();
     }
   
     ngOnInit(): void {
     }
     addHero(name: string) {
       name = name.trim();
       if (name) {
         // 不改变引用没有用
         this.heroes.push({ id: 'flier_' + Date.now(), name, canFly: this.canFly });
        // 开启非纯管道方式二、改变引用,触发变更检测
        // this.heroes = [...this.heroes,{id:'flier'+Date.now(),name,canFly:this.canFly}]
       }
     }
   
   reset() { this.heroes = HEROES.slice(); }
}

生命周期

生命周期函数:组件创建、组件更新、组件销毁的时候会触发的一系列的方法。

当 Angular 使用构造函数新建一个组件或指令后,就会按下面的顺序在特定时刻调用这些 生命周期钩子函数。

所有生命周期钩子函数执行顺序

import { AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, 
        DoCheck, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit,OnChanges,DoCheck,AfterContentInit,
    	AfterContentChecked,AfterViewInit,AfterViewChecked,OnDestroy{
  constructor(){} //组件初始化,写简单的逻辑和数据初始化操作,(获取不到最新输入属性值)
  ngOnChanges(changes: SimpleChanges): void {
    console.log('changes ', changes); 
      //可最早获取到输入属性最新值,输入属性更新时触发,但组件内部改变输入属性是不会触发的
  }
  ngOnInit(): void {
    console.log('ngOnInit '); //适合请求数据初始化组件,只调用一次
  }
  ngDoCheck(): void {
    console.log('ngDoCheck ');
  }

  ngAfterContentInit(): void {
      //在组件内容初始化之后调用,只调用一次。
    console.log('ngAfterContentInit');
  }

  ngAfterContentChecked(): void {
      //组件每次检查内容时调用
    console.log('ngAfterContentChecked ');
  }

  ngAfterViewInit(): void {
      //组件相应的视图初始化之后调用,只调用一次。
    console.log('ngAfterViewInit ');
  }

  ngAfterViewChecked(): void {
      //组件每次检查视图时调用
    console.log('ngAfterViewChecked');
    
  }
 
  ngOnDestroy(): void {
    //指令销毁前调用,适合理一些残存的状态操作:
    //取消订阅可观察对象和 DOM 事件、停止 interval 计时器、
    //反注册该指令在全局或应用服务中注册过的所有回调
    console.log('ngOnDestroy');
  }
 
}

当组件、父组件发生变更检测后都会调用这三个钩子:

模板中的DOM事件触发就会进行变更检测(<input (input)="$event">

ngDoCheck ngAfterContentChecked ngAfterViewChecked

  • 指令与组件共有的钩子
    • ngOnChanges
    • ngOnInit
    • ngDoCheck
    • ngOnDestroy
  • 组件特有的钩子
    • ngAfterContentInit
    • ngAfterContentChecked
    • ngAfterViewInit
    • ngAfterViewChecked

变更检测

默认策略下触发变更检测的时机

changeDetection:ChangeDetectionStrategy.Default
  • 事件:页面 click、submit、mouse down……
  • XHR:从后端服务器拿到数据
  • 定时器:setTimeout()、setInterval()

只要某个组件触发了以上中的一个,就会从顶级组件从上至下开始进行变更检测,每个组件都会进行变更检测,

检测组件中的值是否应该改变

注意

已经检测完的组件,不允许在被子组件修改,(子组件不能修改检测完的父组件数据),这就是单向数据流

onPush下触发变更检测时机

changeDetection:ChangeDetectionStrategy.OnPush

onPush策略会把组件从组件树中剥离出去,他和他的子组件都不会检测了;

定时器会触发变更检测,但是依然会跳过onPush策略组件。

  • 组件的@Input引用发生变化。
  • 组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down。
  • Observable 订阅事件,同时设置 Async pipe。
  • 手动调用:ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick()方法
markForCheck() //把该视图显式标记为已更改(脏的),以便它下一轮再次进行检查。
detectChanges() //检查该视图及其子视图。与 detach 结合使用可以实现局部变更检测。(强行检测)

动态组件

如果说,之前在模版中调用的组件为静态组件(比如:app-xxx)

那么不用在模版里声明,而是通过ts动态插入到dom中到组件,可以视为动态组件

📝alert.component.ts:

import {Component, OnInit, ChangeDetectionStrategy, Output, EventEmitter} from '@angular/core';
type AlertTheme = 'primary' | 'danger' | 'warning';

export interface AlertOption {
  content: string;
  theme?: AlertTheme;
}

@Component({
  selector: 'app-alert',
  template: `
    <div [class]="wrapCls" role="alert">
      <span class="content">{{ options.content }}</span>
      <i class="close" (click)="closed.emit()">&times;</i>
    </div>
  `,
  styles: [`
    .close {
      display: block;
      width: 20px;
      height: 20px;
      position: absolute;
      right: 10px;
      top: 50%;
      margin-top: -10px;
      cursor: pointer;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AlertComponent implements OnInit {
  options: Required<AlertOption> = {
    content: '',
    theme: 'primary'
  }

  @Output() closed = new EventEmitter<void>();
  constructor() { }

  ngOnInit(): void {}

  get wrapCls(): string {
    return 'alert alert-' + this.options.theme + ' fixed-top';
  }

  setOptions(options: AlertOption) {
    this.options = { ...this.options, ...options };
  }
}

调用 alert.component

import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injector, OnInit} from '@angular/core';
import {AlertComponent} from '../../components/alert/alert.component';

@Component({
  selector: 'app-show-data',
  templateUrl: './show-data.component.html',
  styleUrls: ['./show-data.component.scss']
})
export class ShowDataComponent implements OnInit {
  private container: AlertComponent;
  private componentRef: ComponentRef<AlertComponent>;
  constructor(
    private cfr: ComponentFactoryResolver,
    private inject: Injector,
    private appRef: ApplicationRef
  ) {}

  ngOnInit(): void {

  }

  showAlert() {
    if (!this.container) {
      this.container = this.getContainer();
    }
  
    // 调用组件的某个方法执行逻辑,比如下面这个传参
    this.container.setOptions({ content: '一段提示', theme: 'warning' });
  }

  private getContainer(): AlertComponent {
    // 创建指定类型的组件工厂(生产指定类型的组件)
    const factory = this.cfr.resolveComponentFactory<AlertComponent>(AlertComponent);

    // 根据指定的类型,创建组件的示例
    this.componentRef = factory.create(this.inject);

    // 将组件试图添加到试图树中,以激活变更检测
    this.appRef.attachView(this.componentRef.hostView);

    // 将组件到模版(包括app-alert标签),添加到body最后
    document.body.appendChild((this.componentRef.hostView as EmbeddedViewRef<{}>).rootNodes[0] as HTMLElement);
    
    // 监听组件销毁事件
    this.componentRef.onDestroy(() => {
      console.log('componentRef destory');
    });

    // 获取组件实例,相当于用@ViewChild获取子组件一样
    const { instance } = this.componentRef;

    // 监听组件到output事件
    instance.closed.subscribe(() => {
      this.componentRef.destroy();
      this.container = null;
    });

    return instance;
  }
}

v9和v10,动态组件都不需要entryComponents了,当然写了也没有问题 从v11开始,entryComponents可能被删除 v8及以前,动态组件一定要声明entryComponents

NgTemplateOutlet指令

NgTemplateOutlet是一个结构型指令

📝app.component.html

<app-tmp-outlet [render]="mycontent"></app-tmp-outlet>
<ng-template #mycontent>
    <div>一段父组件传入的内容</div>
</ng-template>

<!--使用tem-outlet组件中的数据-->
<ng-template #mycontent let-context let-val="value">
    <div>一段父组件传入的内容</div>
    <div>使用outlet中的context:{{context}}</div>
    <div>使用outlet中的value:{{val}}</div>
</ng-template>

📝tmp-outlet.component.html

<p>tmp-outlet works!</p>
<ng-container [ngTemplateOutlet]="render || default"></ng-container>

<!--传递出去tem-outlet组件中的数据-->
<ng-container [ngTemplateOutlet]="render || default" [ngTemplateOutletContext]="myContext"></ng-container>
<!-- <ng-container *ngTemplateOutlet="render || default ;context:myContext"></ng-container>简写 -->

<!--ng-template使用效果一样 -->
<ng-template [ngTemplateOutlet]="render || default" [ngTemplateOutletContext]="myContext"></ng-template>
<ng-template *ngTemplateOutlet="render || default ;context:myContext"></ng-template>
<ng-template #default>
    <div>一段组价默认的内容</div>
</ng-template>

<!-- context在内部ng-template也可以绑定 -->
<ng-template #default let-context let-val="value">
    <div>一段组价默认的内容</div>
    <div> context:{{context}}</div>
    <div> value:{{val}}</div>
</ng-template>

📝tmp-outlet.component.ts

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-tmp-outlet',
  templateUrl: './tmp-outlet.component.html',
  styleUrls: ['./tmp-outlet.component.css']
})
export class TmpOutletComponent implements OnInit {

  @Input()  render 
  myContext = {$implicit: 'tmp-outlet组件里的context', value: 'tmp-outlet组件里的value'};
  constructor() { }

  ngOnInit(): void {
  }

}