Angular常用的组件间通信的几种方式 (一)

764 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

虽然工作中用的开发框架是Angular,但是我写Angular的文章比较少,所以打算写一写Angular相关的文章,也算是做一些总结吧。

父子组件通信

组件的关系有很多,比如父子组件,兄弟组件,爷孙组件,无直接关系组件。在实际的工作中,组件以树形的结构进行关联,所以组件间的关系主要是以下几种:

  • 父子组件
  • 兄弟组件
  • 无直接关系组件

我的日常工作中,接触的父子组件比较多,我们就以父子组件来看组件间的通信问题。

模板本地变量

我们可以通过本地变量来访问子组件中的方法或属性值,但是它有很强的局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行,而父组件本身的代码对子组件没有访问权。

例如:子组件User中,有一个print方法:

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

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.scss']
})
export class UserComponent {

  constructor() { }

  print(){
    console.log('Hello!');
  }
}

父组件template:

  <app-user #user [options]="userInfo" [age]="age"></app-user>
  <button (click)="user.print()">访问子组件</button>

运行效果:

@ViewChild

先来看一下英文介绍:
Property decorator that configures a view query. The change detector looks for the first element or the directive matching the selector in the view DOM. If the view DOM changes, and a new child matches the selector, the property is updated.

View queries are set before the ngAfterViewInit callback is called.

之前写过一篇文章介绍了Angular的装饰器,有属性装饰器,类装饰器和方法装饰器,感兴趣的小伙伴可以去看下哦。

获取DOM元素

如果要访问模板中的DOM元素,是需要给相应的DOM元素起一个模板变量的。

<div #domLabel>计数器: {{count}}</div>
@ViewChild('domLabel') domLabelElement: ElementRef;    //找到第一个符合条件的节点

 ngOnInit(): void {
    // console.log('ngOnInit', this.p1.nativeElement.innerHTML); // 报错
 }

ngAfterViewInit(): void {
   console.log(this.domLabelElement.nativeElement);
}

如果一个元素是静态的,你又想尽早的拿到该元素,可以设置它的static属性为true。

<!--静态节点-->
<div #domLabel>计数器</div>

<!--非静态节点-->
<div #domLabel>计数器: {{count}}</div>
@ViewChild('caption', {
  static: true
})

count: number = 0;

ngOnInit(): void {
  // console.log('ngOnInit', this.p1.nativeElement);
  console.log('ngOnInit', this.p1.nativeElement.innerHTML);
}

ngAfterViewInit(): void {
  console.log('ngAfterViewInit', this.p1.nativeElement.innerHTML);
}

如果节点并非是静态的,但是我把他标记成静态的,在ngOnInit中并不会报错,但是取到的值并非预期的。

建议:
如果目标从一开始就显示在模板上,就直接开启static: true。

静态节点这个概念并非是Angular专属的,在Vue中也是有的,Vue3相对于Vue2中做了很多优化,其中一个优化的点就是标记了静态节点,那在DOM DIFF的时候就可以跳过静态节点了,提高了patch的效率。

获取子组件实例

如果父组件的类需要依赖于子组件,就不能使用本地变量的方法。那我如果想在父组件的代码中访问子组件怎么办呢?那就需要用到 @ViewChild() ,也就是把子组件作为ViewChild,注入到父组件里面。

访问子组件:

父组件:

<p>parent works!</p>
<p>第一个:</p>
<app-child></app-child>
{{count}}
<button (click)="handleClick()">访问子组件</button>
<p>第二个:</p>
<app-child></app-child>
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit {
  @ViewChild(ChildComponent)
  childComp1!: ChildComponent;
  
  count: number = 0;

  constructor() { }

  ngOnInit(): void {
     
  }

  handleClick(): void {
    this.childComp1.print(this.childComp1.name);
  }

}

也可以这样写:

<app-child #child1></app-child>
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit {
  @ViewChild('child1', {
     read: ChildComponent,
     static: true
  })
  childComp1!: ChildComponent;

  handleClick(): void {
    this.childComp1.print(this.childComp1.name);
  }

}

子组件:

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

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit {
  name: string = 'Tom';

  constructor() { }

  ngOnInit(): void {
  }

  print(name: string){
    console.log(`Hello, I am ${name}.`);
  }
}

注意,@ViewChild和@ViewChildren会在父组件钩子函数ngAfterViewInit调用之前赋值。所以,在父组件中并不是可以随意的去访问子组件的实例this.childComp1,你需要等子组件的实例生成后才能进行访问。那如果我想在第一时间访问一次子组件,然后在点击button的时候在访问,要怎么实现呢?

这时候,就需要用到一个生命周期钩子函数ngAfterViewInit,这个生命周期函数说明组件视图已经完成,那就可以愉快的访问子组件实例了。

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

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit, AfterViewInit {
  @ViewChild(ChildComponent)
  childComp1!: ChildComponent;
  
  count: number = 0;

  constructor() { }

  ngOnInit(): void {
    // console.log(this.childComp1.print('哈哈'));  // 报错
  }

  ngAfterViewInit(): void {
    this.childComp1.print('Jerry');
  }

  handleClick(): void {
    this.childComp1.print(this.childComp1.name);
  }

}

问题:
生命周期钩子函数ngAfterViewInit里,如果我想改变一下模板中的变量值,可以不可以呢?当然是可以的,但是,Angular的单项数据流规则会阻止在同一个周期内更新父组件视图。也就是说,你要是更新了,会报错。

ngAfterViewInit(): void {
  this.count++;
}

那如果我想实现这样的功能怎么办呢?那就把更新推迟到下一轮。

  ngAfterViewInit(): void {
    setTimeout(() => {
      this.count++;
    }, 0);
  }

@ViewChildren

@ViewChildren和@ViewChild类似,它可以批量获取模板上相同选择器的元素,并存放到QueryList类中。

需要注意的是:ViewChildren没有static属性。

<p>parent works!</p>
<p>第一个:</p>
<app-child></app-child>
{{count}}
<button (click)="handleClick()">访问子组件</button>
<p>第二个:</p>
<app-child></app-child>
export class ParentComponent implements OnInit, AfterViewInit {
  ...

  @ViewChildren(ChildComponent)
  appChild!: QueryList<ChildComponent>;

  @ViewChildren('caption')
  appChild2!: QueryList<ChildComponent>;
}

QueryList

QueryList是模板元素的集合。

  ngAfterViewInit(): void {
    this.appChild.forEach((child: ChildComponent, index: number) => {
        child.print(index.toString());
    });
  }

鉴于篇幅有限,先写到这里吧,后续会在更新后面的几种通信方式,请敬期待。