组件和组件之间一定得是松耦合的这样可重用性才高,想象一下这样的场景,A和B两个兄弟组件,但是A调用B组件的一个方法,那么这两个组件就紧密的联系在了一起,这其实是并不可取的。
组件我们应该看作是一个黑盒,组件只需要"输入"和"输出"的功能就行,组件并不关心到底是谁给他输入的,也不关心他会输出给谁。
父向子传递数据
首先我们需要在子组件中,定义我们想要接受的参数,如下:
@Input()
private stockCode: string;
@Input()
private amount: number;
@Input()是输入属性的关键所在,而且需要注意的是使用注解的时候,一定要注意后面的小括号,不要落了,之前我就犯过这样的错误。
我们这里只是接受的,那么入口在哪里呢,当然在父组件中了,如下:
<app-order [stockCode] = "test" [amount] = "5"></app-order>
这两个参数的用法,和我们之前说过的"属性绑定"一样,都是通过"[ ]"扩起来的,之前是又html到ts文件,这次是由父组件到子组件。
子向父传递数据
子组件中我们需要向父组件发射事件,核心代码如下:
@Output()
lastChangesPrice: EventEmitter<PriceQuote> = new EventEmitter();
this.lastChangesPrice.emit(new PriceQuote('IBM', price));
父组件接受事件的html代码如下:
<app-price-quote (lastChangesPrice) = "lastChanged($event)"></app-price-quote>
和之前父向子传递不同,这里需要事件绑定(我们可以这样想,因为刚刚我们发送了一个事件,所以这里需要一个事件进行承接)。小括号中的名称需要与@Output对应上,后面的方法是需要在ts文件中进行具体的实现。
js代码如下:
public lastChanged(event: PriceQuote) {
//TODO
}
Tips: 相信看到这里,你已经对@Input和@Output()有了一定的了解,在这里我要说一个实用的技巧,这个技巧将会打通输入与输出的任督二脉:一个组件,我们有一个输入的属性name。这个组件作为一个子组件被父组件调用,父组件可以将一个默认值传递给子组件。但是现在我们希望子组件可以把实时变化的数据传递出来,这个时候就需要@Output了,定义一个EventEmitter,把值emit出来,父组件接收后,再把值赋给初始化的变量这个过程其实相当的麻烦,那么我们应该如何避免呢?就拿刚刚举的例子说,输入属性为name那么我们将输出属性定义为nameChange,没错就是在输入属性后面加上一个Change。然后父组件调用的地方写成双向绑定,angular就会帮我们做自动的绑定了,是不是很简单呢?
中间人模式
A组件和B组件之间是兄弟组件,他们同样属于C组件。
如果我们直接让A和B之间相互调用方法,那么他们之间形成紧密的耦合,所以我们引入中间人模式来实现松耦合:A需要调用B做的事情我们通过输出事件,先让C知道,然后C会马不停蹄的去通知B去执行。这个C可以理解为一个中间商衔接了A和B组件。
组件生命周期
-
组件初始化
- constructor1⃣️
- ngOnChanges
- ngOnInit1⃣️
- ngDoCheck
- ngAfterContentInit1⃣️
- ngAfterContentChecked
- ngAfterViewInit1⃣️
- ngAfterViewChecked
-
变化监测
- ngOnChanges
- ngDoCheck
- ngAfterContentChecked
- ngAfterViewChecked
-
组件销毁
- ngOnDestroy1⃣️
Tips:带有1⃣️标示的是只能执行一次的
Changes钩子
这里重点说一下ngOnChanges:
发生在ngOnInit之前,并且是只有在有输入属性的情况下,会被调用。
但是又了输入属性的情况之后,其实也是未必会调用的,这里要从可变对象 和不可变对象说起
可变对象:
对象的地址直接指向了一块内存,例如说定义一个string,然后将string的值进行修改,string对象的地址就发生了改变,这个时候是会触发ngOnChanges的。
不可变对象:
修改了对象中的属性,只是修改了对象中属性的地址,但是对象本身指向的地址不发生改变,例如说一个object对象,我们修改这个object中的一个id属性,那么其实是不会触发ngOnChanges的,但是子组件,依然可以捕获到这次改变,然后将值进行改变,主要根据的就是angular的变更检测机制
变更检测机制
因为有zone.js所以才有了对原生事件监测的机制。
变更检测机制可以分为两种:
- 默认策略
- onPush策略
他俩的差别就在于,父组件发生了变化,默认策略会一直向子组件,及其孙子组件传递,而如果子组件采用onPush的策略,那么传递将会停止。
那我们应该如何使用变更检测机制呢?angular为我们提供了DoCheck钩子,这个钩子,就可以在刚刚说过的不可变对象发生变化之后,得到响应。
Tips: 当我们使用带有Checked的钩子函数时,一定有极其小心,因为这类函数,会被极其频繁的调用,所以在这种函数中实现一些逻辑的时候,一定要轻量级,否则会造成性能问题。
View钩子
在某些场景上,我们还需要使用父组件调用子组件的方法,这个时候我们可以在ts文件中使用@ViewChild("子组件别名"),这个子组件别名是什么呢?首先看一下如下代码
<app-a #aComponent></app-a>
<app-b #bComponent></app-b>
这里的app-a和app-b是两个子组件,aComponent和bComponent就是子组件的别名。
我们还可以在模版中使用子组件的方法,看下代码:
<app-a #aComponent></app-a>
<app-b #bComponent></app-b>
<button (click)="aComponent.test()"></button>
在按钮中使用了一个点击事件,去触发test方法。
View相关的一共有两个钩子,这两个钩子都是在组件加载完之后执行的 :
- ngAfterViewInit
- ngAfterViewChanged
如果一个父组件有两个子组件,他们都实现了这两个钩子,那么执行顺序会是
- 子组件A ngAfterViewInit
- 子组件A ngAfterViewChanged
- 子组件B ngAfterViewInit
- 子组件B ngAfterViewChanged
- 父组件 ngAfterViewInit
- 父组件 ngAfterViewChanged
这就说明父组件在声明之前,一定是先对子组件进行组装,等着组装好了之后,才会组装父组件。
Content钩子
首先介绍一个投影的概念,什么是投影呢,投影相当于子组件给父组件掏了个壁橱,也就是说这篇区域,子组件不知道父组件们都会用来做什么,所以暂时就空在那了。这种思想是不是很像angular的路由。有些人就好想了,为什么在这个时候不用路由呢?因为这种方法比路由更简单一些,在真正的业务中自己需要权衡一下。
content钩子的使用具体来看下代码:
子组件:
<div>
<h1>
Test Test
</h1>
<ng-contnet></ng-contnet>
</div>
子组件的ng-contnet就相当于一个占位符
父组件:
<app-child>
<p>
这里就是传说中掏的壁橱
</p>
</app-child>
在调用子组件的标签中写上我们需要的代码。
这样做的好处就是,子组件可以被重用的更灵活,不会一点不同,就重新构建一个组件。
那么让我们在思考另一个问题,如果在子组件我想掏两个壁橱怎么办?
别慌,angular早都为我们都想好了。看代码:
子组件:
<div>
<h1>
Test Test
</h1>
<ng-contnet select=".classA"></ng-contnet>
<ng-contnet select=".classB"></ng-contnet>
</div>
使用select属性,决定了到底哪一个代码块属于第一个content,哪一个属于第二个。
父组件:
<app-child>
<p class="classA">
这里就是传说中掏的壁橱A,{{testValue}}
</p>
<div class="classB">
这里就是传说中的壁橱B
</div>
</app-child>
这样就能很好的区分出来了。
Tips:
注意我在第一个壁橱中添加了一个插值表达式,这个插值表达式的值,只可以在使用当前父组件的值。
然后引出今天的两个主角:
- ngAfterContentInit 当投影初始化完毕
- ngAfterContentChanged 当投影变更检测完毕