1.ElementRef、TemplateRef、ViewContainerRef
ElementRef 相当于获取一个 DOM,其nativeElement属性就是DOM
TemplateRef 用来获取一个 template 模板
ViewContainerRef 用来获取一个视图容器(作用就是获取一个DOM,并把它当插入其他DOM的容器,只不过插入是插入到其后面,成为其兄弟元素)
场景:想获取当前组件内的某个DOM。可使用ElementRef或者@ViewChild
// 使用ElementRef,直接注入ElementRef,可获得当前组件的 ElementRef
export class AboutComponent implements OnInit {
constructor(private ele: ElementRef) {}
ngOnInit(): void {
const btn = this.ele.nativeElement.querySelector('#btn');
}
}
// 使用ViewChild
export class AboutComponent implements OnInit {
@ViewChild('#btn', {static: true}) btnEle: ElementRef
}
2.ViewChild、ViewChildren、ContentChild、ContentChildren
ViewChild 获取页面元素 \ 组件
export declare interface ViewChildDecorator {
(selector: Type | Function | string, opts: {
read?: any;
static: boolean;
}): any;
}
这个 api 的语义话非常好:可以理解为"表示查询一个子元素,并从它身上获取(read)什么"
selector
一个组件或一个指令
@ViewChild(ChildComponent)
一个模板引用变量的字符串
@ViewChild('cmp')
一个子组件上注册的提供商
@ViewChild(TOKEN)
一个TemplateRef
@ViewChild(TemplateRef) tpl: TemplateRef
函数形式怎么用不知道
read (从查询到的元素中读取另一个令牌)
read的内容才是希望获取的东西,比如:
// 查询子元素ChildComponent,并从它身上读取ElementRef
@ViewChild(ChildComponent, {static: true, read: ElementRef} ) ele: ElementRef
// 查询子元素ChildComponent,并从它身上读取一个视图容器
@ViewChild(ChildComponent, {static: true, read: ViewContainerRef} ) vcref: ViewContainerRef
// 查询子元素ChildComponent,并从它身上读取一个服务
@ViewChild(ChildComponent, {static: true, read: MsgService} ) msg: MsgService
read是可选的,当不指明的时候,应该是查询什么,就获取什么。比如
// 二者作用一样
@ViewChild(ChildComponent, {static: true, read: ChildComponent} ) component: ChildComponent
@ViewChild(ChildComponent, {static: true} ) component: ChildComponent
static (指定查询元素的时机,查询只会执行一次,其结果会缓存)
true 在变更检测前查询元素(ngChanges之前)
false 在变更检测后查询元素(ngDoCheck之后,ngAfterViewInit之前)
@Component({
template: `
`,
})
export class AboutComponent implements OnInit, AfterViewInit {
@ViewChild('div1', { static: true }) div1: ElementRef;
@ViewChild('div2', { static: false }) div2: ElementRef;
ngOnInit() {
console.log(this.div1);
console.log(this.div2);
}
ngAfterViewInit() {
console.log(this.div1);
console.log(this.div2);
}
}
上述过程是:
div1生成 -> 查询div1 -> ngChanges -> ngOnInit -> ngDoCheck -> div2生成 -> 查询div2 -> ngAfterViewInit...
3.zone.js
angular 和 angularjs 中双向绑定的实现方式是脏值检查,当某些可能导致值发生变化的事情发生之后,就去检查值是否变化、更新视图......可能导致值发生变化的事情包括setTimeout、setInterval、XHR、dom事件。
angularjs中,脏检查就存在问题,不能让异步事件完毕后自动调用触发检测。比如:
function foo() {
$scope.user = '张三'
}
setTomeout(foo, 0)
$apply() // 更新视图
当使用原生setTimeout去改变值后,无法让foo执行完毕之后,自动调用 $apply() 或者 $degist()。解决办法是开发者手动去调用 $apply() 触发新一轮的变更检测
function foo() {
$scope.user = '张三';
$apply() // 更新视图
}
setTomeout(foo, 0)
或者用封装的 $timeout 代替 setTimeout
function foo() {
$scope.user = '张三';
}
$timeout(foo, 0)
$timeout可能就长这样:
var $timeout = function(fn, time) {
var that = this;
return setTimeout(function() {
fn.apply(that);
$apply();
}, time)
}
angular2 改进了这个问题:
zone.js 使用mokey patch(称猴子补丁或动态补丁)的方式覆盖掉了原生的setTimeout等异步方法,其具体实现很复杂,反正效果是使得异步任务进入执行栈、执行完毕等过程都可以被监听到。
*zone.js不仅覆盖掉了 setTomeout 等原生API,还专门覆盖掉了其 toString 方法,使得直接在控制台 setTomeout 或者 setTomeout.toString 得到的是 native code,可以直接 console.dir(window) 去控制台查看 setTomeout *
这个例子展示了zone.js中onScheduleTask、onInvokeTask两个钩子监听到异步任务进入执行栈、执行完毕。
let timer;
const zone = Zone.current.fork({
name: 'z',
onScheduleTask(delegate, currentZone, targetZone, task) {
const result = delegate.scheduleTask(targetZone, task);
const name = task.callback.name;
console.log(
Date.now() - timer,
`task with callback '${name}' is added to the task queue`
);
return result;
},
onInvokeTask(delegate, currentZone, targetZone, task, ...args) {
const result = delegate.invokeTask(targetZone, task, ...args);
const name = task.callback.name;
console.log(
Date.now() - timer,
`task with callback '${name}' is removed from the task queue`
);
return result;
}
});
function a1() {}
function a2() {}
function b() {
timer = Date.now();
setTimeout(a1, 2000);
setTimeout(a2, 4000);
}
zone.run(b);
在监听到异步执行完毕之后,再由 ngZone 去执行启动变更检测等一系列调度。
所以zone.js是和angular框架完全解耦的,可以单独拿到其他项目中去用,去监听异步任务。
ngZone,还可以用来对项目进行一些优化。
比如有个频繁变动的值,将导致频繁执行变更检测。可以用ngZone使angular暂时不跟踪其变化
constructor(private zone: NgZone) { }
this.zone.runOutsideAngular(() => {
for (let i = 0; i < 100; i++) {
setInterval(() => this.counter++, 10);
}
});
稳定后,重新跟踪变化
this.zone.run(() => {
setTimeout(() => this.foo = this.foo, 1000);
});
4.变更检测
结论:
- 可以使用ChangeDetectionStrategy.OnPush 对项目进行优化。OnPush策略会使组件及其子组件在发生外部异步事件时,不再执行变更检测。对于其输入属性发生变化,或其自己内部发生异步事件,还是会正常进行检测并更新视图
- 输入属性改变是指 oldValue !== newValue,所以修改了对象的属性不算输入属性改变。这算是一个“小缺点”
- 对于上面这个"小缺点",可以使用Immutable 变量避免,或使用bservable作为输入属性
- ChangeDetectorRef 是当前组件的变更检测器的引用。可用来分离当前检测器、重新恢复变更检测、手动执行检测等等操作
- OnPush策略下,ngDoCheck钩子函数依然会执行,可在ngDoCheck调用ChangeDetectorRef
以下是一些具体的例子或叙述说明:
angular应用是由组件组成的树状结构。每当异步事件发生后,angular都会从上而下地检测当前"存活"的每个组件
@Component({
selector: 'parent',
template: ` `
})
export class ParentComponent implements OnInit {
person = { name: '张三' };
ngOnInit(): void {
setTimeout( () => {
this.person.name = '李四';
}, 2000);
}
}
@Component({
selector: 'child',
template: `{{user | json}}`
})
export class ChildComponent {
@Input() user: any;
}
可以看到2秒后child组件的视图更新
当某个组件设置 ChangeDetectionStrategy.OnPush 后,无关的异步事件发生时,这个组件及其子组件不再执行变更检测。只有其输入属性改变,或其本身内部发生异步事件才检测。
输入属性改变,指的是 oldValue !== newValue,所以以下情况就不会触发变更检测。
@Component({
selector: 'parent',
template: ` `
})
export class ParentComponent implements OnInit {
person = { name: '张三' };
ngOnInit(): void {
setTimeout( () => {
this.person.name = '李四';
}, 2000);
}
}
@Component({
selector: 'child',
template: `{{ user | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() user: any;
}
可以看到两秒后child组件的视图并不更新
child组件的输入属性被判定为“没有”发生变化,所以其变更检测不会执行。为避免这种情况,需使用 Immutable (不可变)变量。Immutable 变量的不是说这个变量无法修改,是指我们遵守不修改原有的数据模型,而是创建一个新的数据模型的原则。
@Component({
selector: 'parent',
template: ` `
})
export class ParentComponent implements OnInit {
person = { name: '张三' };
ngOnInit(): void {
setTimeout( () => {
this.person = { '李四' };
}, 2000);
}
}
@Component({
selector: 'child',
template: `{{ user | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() user: any;
}
可以看到两秒后视图更新
除了Immutable 方式外,还可以使用 Observable作为输入属性
export class CounterComponent implements OnInit {
counter: number = 0;
@Input() addStream: Observable;
constructor(private cdRef: ChangeDetectorRef) { }
ngOnInit() {
this.addStream.subscribe(() => {
this.counter++;
this.cdRef.markForCheck();
});
}
}
组件自己内部发生异步事件,依然可以触发变更检测
@Component({
selector: 'child',
template: `{{ user | json }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
@Input() user: any;
ngOnInit(): void {
setTimeout( () => {
this.user.name = '李四';
}, 2000);
}
}
参考资料
翻阅源码后,我终于理解了Zone.js