####输入属性
父组件传递数据给子组件,这是单向传递
//父html
<div>父组件</div>
<div>
<input type='text' [(ngModel)]='stock'
//引用子组件
<app-order [stockCode]='stock' [amount]='100></app-order>
</div>
//父ts:
stock='';
//子html
<div>子组件</div>
<div>买{{amount}}手{{stockCode}}股票</div>
//子ts:
@Input()
stockCode: string;
@Input()
amount: number;
输出属性
//父html
<div>父组件,获取价格为:{{price}}</div>
//changePrice在子组件output时被命名
<app-price-quote (changePrice)="priceHandler($event)"></app-price-quote>
//父ts
stock='';
price:number = 0;
//event接受的是子组件传递过来的参数,此处指子组件的price.
priceHandler(event:number) {
this.price = event;
}
//子html
<div>子组件</div>
<div>股票代码是{{stockCode}},股票价格是{{price | number:'2.2-2'}}</div>
//子ts
stockCode:tring='IBM';
price:number;
//定义输出数据,changePrice为捕获事件的名称
@Output('changePrice')
lastPrice: EventEmitter<number> = new EventEmitter();
constructor() {
setInterval(()=> {
this.price = 100*Math.random();
//发射数据
this.lastPrice.emit(this.price);
});
}
中间人模式
两个无关联的组件可通过两者的共有父组件来通信。app.component.ts是所有组件的中间组件。
注:若两组件间无中间组件,应借助于服务来共享数据。
//中间人html
<app-price-quote (buy)='buyHandler($event)'></app-price-quote>
<app-order [price]='price'></app-order>
//中间ts
price: number=0;
buyHandler(event:number) {
this.price = event;
}
//组件1 html
<div>下单组件,数据来源于组件2</div>
<div>买入价格是{{price}}</div>
//组件1 ts
@Input()
price:number;
//组件2 html
<div>报价组件,我会将数据传递给组件1</div>
<div>
<input type='button' value='立即购买' (click)='buyStock($event)'>
</div>
//组件2 ts
@Output()
buy:EventEmitter<number> = new EventEmitter();
price:number = 20;
buyStock(event) {
this.buy.emit(this.price);
}
组件生命周期
OnChanges:
父组件在初始化或修改子组件的输入参数(input)时被调用
变量分为可变变量与不可变变量
可变变量:
var user:{name:string} = {name: ‘TOM’}
user.name=’senai’ //对象的引用地址未变
不可变变量:
name:string = ‘hello’
name = ‘world’ //name指向了world这个字符串的地址,但hello字符串地址仍存在
ngChanges监控的是输入参数的不可变变量
//父html
<div class="parent">
<h2>我是父组件</h2>
<div>
问候语:<input type="text" [(ngModel)]="greeting">
</div>
<div>
姓名:<input type="text" [(ngModel)]="user.name">
</div>
<app-child [greeting]="greeting" [user]="user"></app-child>
</div>
//父ts
greeting:string = "Hello";
user:{name:string} = {name: "Tom"};
//子html
<div class="child">
<h2>我是子组件</h2>
<div>问候语:{{greeting}}</div>
<div>姓名:{{user.name}}</div>
<div>消息: <input [(ngModel)]="message"></div>
</div>
//子ts
@Input()
greeting:string;
@Input()
user:{name:string};
message:string = "初始化消息";
//首次初始化时输出greeting与user的值的变化,由空至有值
//当父组件的输入框值变化时,仅输出greeting值变化,因onchange仅监控不可变变量
//onchange未捕捉到User值的变化,但页面中子组件的user值随父组件的user值发生了变化,这个变化是由变更检测机制决定的。
ngOnChanges(changes: SimpleChanges): void {
console.log(JSON.stringify(changes, null, 2));
}
变更检测和DoCheck
由zone.js控制,目的是保证组件的属性变化和页面的变化是同步的,浏览器的任何异步事件都会触发变更检测(eg:点击按钮、输入数据、数据从服务器返回调用setModel方法等等)
angular有两种变更检测: default策略:如果所有组件都使用default策略,
不管变更发生在哪个组件上,zone.js都会检查整个组件树
onPush策略:如果有一个特定的组件声明自己的变更检测为onPush,只有当这个组件的OnPush发生变化时,zone.js才会检查这个组件及其子组件。
view钩子:ngAfterViewChecked与ngAfterViewInit
ngAfterViewInit在ngAfterViewChecked之前调用
这两个勾子是在组件的视图被组装完毕以后去调用
如果组件有子组件,只有当子组件的所有视图都组装完毕后才会调用父组件的。
不要在这两个勾子内改变视图中的值,如果想改变,应写在timeOut循环中。
父组件调用子组件的方法
//父html
<app-child #child1></app-child>
<app-child #child2></app-child>
//在模板中直接使用本地变量来调用子组件的方法。
<button (click)="child2.greeting('jerry')">调用</button>
//父ts
//定义子组件的引用,使用此引用可调用子组件的方法
@ViewChild('child1');
child1:ChildComponent;
message: string;
ngOninit():void {
setInterval(()=> {
this.child1.greeting('tom');
}, 5000);
}
ngAfterViewInit(): void {
console.log('父组件的视图初始化完毕');
//会抛出异常,因为在变更检测周期中,angular禁止在一个视图已经被组装好后再去更新此视图,而view勾子是在组件被组装好后触发 所以这两个view勾子都不可直接为其直接变更值,若想改变应将代码放至另一个指令循环中
//this.message='hello';
//应写为:
setTimeout(()=> {
this.message='hello';
},0);
}
ngAfterViewChecked(): void {
console.log('父组件的视图变更检测完毕');
//子html
//子ts
greeting(name: string) {
console.log('hello'+name);
}
//仅被调用一次
ngAfterViewInit(): void {
console.log('子组件的视图初始化完毕');
}
ngAfterViewChecked(): void {
console.log('子组件的视图变更检测完毕');
打印结果为:
- 子组件的视图初始化完毕
- 子组件的变更检测完毕
- 父组件的视图初始化完毕
- 父组件的 变更检测完毕
ngContent指令
投影:在不使用路由的情况下,将父组件的内容映射到子组件中,以达到动态加载子组件的目的。使用ng-content
//父html
<div class='wrapper'>
<h2>父组件</h2>
//将要投影到子组件的内容定义在子组件的引用之间。
<app-child>
//header,footer样式用于为子组件指定位置
<div class='header'>父组件投影到子组件的header,title为{titile}</div>
<div class='footer'>父组件投影到子组件的footer</div>
</app-child>
//通过属性绑定来加载DOM
<div [innerHTML]='divContent'></div>
</div>
//父ts
title:string = 'senai';
divContent= '<div>插入到指定div中</div>';
//子html
<div class='wrapper'>
<h2>子组件</h2>
//使用ng-content来标记投影点,为投影内容占位。
<ng-content select='.header'></ng-content>
<ng-content select='.footer'></ng-content>
</div>
//子ts
ngAfterContentChecked和ngAfterContentInit
被投影进来的内容组装完成后进行调用
先组装投影进来的内容,然后组装子组件中视图的内容。最后组装父组件自身的视图内容。
与view勾子不同,可在afterContent勾子中修改属性内容,因为此勾子被调用时整个视图未组装完毕,只是投影部分的内容被组装完毕。所以可在此处修改属性。
父子模板同ng-content
//父ts
message: string = 'hello';
ngAfterContentInit():void {
console.log('父组件投影内容初始化完毕');
this.message = 'hello senai';
}
ngAfterContentChecked():void {
console.log('父组件投影内容变更检测完毕');
}
ngAfterViewInit(): void {
console.log('父组件视图内容初始化完毕');
}
//子ts
ngAfterContentInit():void {
console.log('子组件投影内容初始化完毕');
}
ngAfterContentChecked():void {
console.log('子组件投影内容变更检测完毕');
}
执行结果为:
- 父组件投影内容初始化完毕
- 父组件投影内容变更检测完毕
- 子组件投影内容初始化完毕
- 子组件投影内容变更检测完毕
父组件视图内容初始化完毕
生命周期总结:
属性初始化阶段: constructor, ngOnChanges, ngOnInit, ngDoCheck
constructor(实例化对象),
ngOnChanges(控制输入属性),
ngOnInit(初始化除输入属性外的其他所有属性)
ngDoCheck(检测所有属性的变更)
此四项完成后,视图所需的值都被初始化完毕。视图渲染阶段:
ngAfterContetentInit与ngAfterContentChecked:
投影进来的内容渲染完毕后调用此两个勾子,如果有子组件,调用子组件的投影内容渲染,直到所有子组件调用完毕后,调用下面两个勾子:ngAfterViewInit与ngAfterViewChecked: 视图组件渲染完毕后调用,先调用子组件的再调用父组件的。 ngOnDestroy: 销毁组件,组件在路由时被被销毁,当从一个 路由跳转至另一路由时,前一路由的组件会被销毁,而后一个路由地址的组件会被创建。
组件交互总结:
1. 父子组件之间应该避免直接访问彼此的内部,而应该通过输入输出属性来通讯。
2. 组件可以通过输出属性发射自定义事件,这些事件可以携带任何你想携带的数据。
3. 在没有父子关系的组件之间,尽量使用中间人模式进行通讯。
4. 父组件可以在运行时投影一个或多个模板片段到子组件中。
5. 每个angular组件都提供了一组生命周期钩子,供你在某些特定的事件发生时执行相应的逻辑。
6. angular的变更检测机制会监控组件属性的变化并自动更新视图。这个检测非常频繁并且默认是针对整个组件树的,所以实现相关钩子时要慎重。
7. 你可以标记你的组件树中的一个分支,使其被排除在变更检测机制之外。