持续创作,加速成长!这是我参与「掘金日新计划 · 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());
});
}
复制代码
鉴于篇幅有限,先写到这里吧,后续会在更新后面的几种通信方式,请敬期待。