阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路
本文基于 Top Common Mistakes of Angular Developers 这篇文章的内容进行整理和扩展,建议有兴趣的读者直接阅读原文。若是你刚接触 Angular,也能够参考一下 Angular 常见问题汇总 这篇文章。javascript
Angular 1.x 版本统称为 AngularJS,Angular 2+ (4/5) 统称为 Angular。前端
第三方库的命名也有必定的规则。假设早期版本的命名以 ng-
做为前缀,当 Angular 2 发布后,该库名称会使用 ng2-
做为前缀。但当 Angular 4 发布之后,新的命名规则就随之出现了。新的术语是使用 ngx-
做为前缀,由于 Angular 使用语义版本,每六个月会发布一个新版本。所以,举个例子,当咱们把 ng2-bootstrap
改名为 ngx-bootstrap
后,从此就不须要再频繁更换库的名称了。java
AngularJS 使用 watcher
和 listener
的概念。watcher 是一个函数,返回被监测的值。一般状况下,这些值是对象模型的属性值。但也不老是数据模型的属性 - 咱们能够跟踪组件的状态、计算新值等。若是该函数返回的值与前一次的值不同,Angular 就会调用 listener
,一般它用来更新 UI 状态。jquery
Angular 移除了 watch
和 scope
,如今咱们将使用组件的输入属性。除此以外,Angular 为咱们提供了 ngOnChanges
生命周期钩子。为了提升变化检测的性能,对于对象比较,Angular 内部直接使用 ===
运算符进行值比较。所以当输入属性是引用类型,当改变对象内部属性时,是不会调用 ngOnChanges
生命周期钩子的。git
// JS has NaN !== NaN export function looseIdentical(a: any, b: any): boolean { return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b); }
许多开发人员不知道这一点,陷入这个陷阱。为了解决这个问题,有各类解决方案:github
ngDoCheck
生命周期钩子subscriptions
使用 ngDoCheck
生命周期挂钩是解决此问题的经常使用方法。当变化检测运行时会自动调用此钩子。在使用今生命周期钩子时,你要当心控制该钩子的内部逻辑,由于一般每分钟会触发屡次变化检测 (能够参考下面的源码)。typescript
ngStyle
指令内部也实现了DoCheck
接口,而后利用KeyValueDiffer
对象来检测对象的变化 (如内部属性的新增、修改、移除操做)。
// packages/core/src/view/provider.ts // 变化检测: // checkAndUpdateView -> Services.updateDirectives(view, CheckType.CheckAndUpdate) function checkAndUpdateDirectiveInline( view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any, v8: any, v9: any): boolean { const providerData = asProviderData(view, def.index); const directive = providerData.instance; let changed = false; let changes: SimpleChanges = undefined !; const bindLen = def.bindings.length; // 判断输入属性值是否改变,若发生改变则更新changes对象相应的属性。 if (bindLen > 0 && checkBinding(view, def, 0, v0)) { changed = true; changes = updateProp(view, providerData, def, 0, v0, changes); } // ... if (bindLen > 9 && checkBinding(view, def, 9, v9)) { changed = true; changes = updateProp(view, providerData, def, 9, v9, changes); } // 若输入属性发生变化才会调用ngOnChanges生命周期钩子 if (changes) { directive.ngOnChanges(changes); } // 若首次执行变化检测及实现OnInit生命周期钩子,则调用ngOnInit生命周期钩子 if ((view.state & ViewState.FirstCheck) && (def.flags & NodeFlags.OnInit)) { directive.ngOnInit(); } // 若实现DoCheck接口,则调用ngDoCheck生命周期钩子 if (def.flags & NodeFlags.DoCheck) { directive.ngDoCheck(); } return changed; // 返回SimpleChanges对象 }
你可能知道当你订阅 Observable 对象或设置事件监听时,在某个时间点,你须要执行取消订阅操做,进而释放操做系统的内存。不然,你的应用程序可能会出现内存泄露。json
@Component({ ... }) export class HeroComponent implements OnInit, OnDestroy { heroForm: FormGroup; valueChanges$: Observable; ngOnInit() { this.valueChanges$ = this.heroForm.valueChanges.subscribe(...); } ngOnDestroy() { this.valueChanges$.unsubscribe(); } }
大多数状况下,当你在组件类中执行订阅操做,你能够在 ngOnDestroy
生命周期钩子中,执行取消订阅的操做。bootstrap
上面介绍了在某些场景下须要手动执行取消订阅操做,进而释放相应的资源。但有些场景下,无需咱们开发者手动执行额外的取消订阅操做。由于在这些场景下,Angular 内部会自动执行取消订阅操做。好比,使用 async
的场景:segmentfault
@Component({ selector: 'heroes-garden', template: `<hero [hero]="heroes$ | async"></todos>` }) export class HeroesGardenComponent implements OnInit, OnDestroy { heroesChanged$: Observable; ngOnInit() { this.heroesChanged$ = this.store.select('heroes'); } ngOnDestroy() { this.heroesChanged$.unsubscribe(); } }
除了使用 async
的场景外,还有如下场景会自动取消订阅:
若想进一步了解手动释放资源和自动释放资源的场景,能够参考专栏 Angular 中什么时候取消订阅 这篇文章。
分层依赖注入做为 Angular 的新机制的一部分,让咱们能够灵活地控制依赖注入。在 AngularJS 中,服务都是单例的,而 Angular 2.x 以上的版本,咱们能够屡次实例化一个服务。
假设咱们已经定义了一个 HeroesService
服务,用来获取英雄信息:
@Injectable() export class HeroesService { heroes: Hero[] = []; constructor(private http: Http) { this.http.get('http://give-me-heroes.com') .map(res => res.json()) .subscribe((heroes: Hero[]) => { this.heroes = heroes; }); } getHeroes() { return this.heroes; } }
正如你所见,咱们在构造函数中获取英雄的数据,此外咱们定义了 getHeroes()
方法,用来获取英雄信息。
如今咱们来使用刚建立的 HeroesService
服务:
@Component({ selector: 'hero', template: '...', providers: [HeroesService] }) export class HeroComponent { constructor(private heroesService: HeroesService) {} } @NgModule({ declarations: [HeroComponent] } export class HeroesModule { ... }
在 HeroComponent
中,咱们在 @Component.providers
数组中声明 HeroesService
服务,而后在 HeroComponent
组件类的构造函数中注入该服务。使用这种方式会有问题,每当实例化新的 HeroComponent
组件时,都会建立一个新的 HeroService
实例,这会致使发送屡次 Http 请求。
解决上述问题的一种方案是在 @NgModule.providers
中声明服务。
@Component({ selector: 'hero', template: '...' }) export class HeroComponent { constructor(private heroesService: HeroesService) {} } @NgModule({ declarations: [HeroComponent], providers: [HeroesService] } export class HeroesModule { ... }
采用这种方式的话,对于多个 HeroComponent
组件,HeroesService
服务只会被实例化一次。由于,当在模块中声明 provider
,它所相关的依赖对象,将是单例的,其它的模块都可以使用它。咱们不须要经过 @NgModule.exports
数组来导出对应的 provider
,它会被自动导出。
Angular 再也不是简单的 Web 框架,Angular 是一个平台,它的一个优势是容许咱们将应用程序代码与渲染器分离,从而编写能够在浏览器、服务器上运行的应用程序,甚至能够编写原生应用。
此外解耦后,也为咱们提供更多的能力,如使用 AOT (Ahead of time) 或 Web Worker。AOT 意味着在构建阶段进行模板编译,AOT 编译模式的开发流程:
运行 ngc 编译应用程序
除此以外 AOT 还有如下优势:
若是咱们如今或未来要使用这种功能,咱们须要遵照必定的规则。其中一个规则是不能使用 jQuery,document 对象或 ElementRef.nativeElement 来直接操做 DOM。具体示例以下:
@Component({ ... }) export class HeroComponent { constructor(private _elementRef: ElementRef) {} doBadThings() { $('.bad-with-jquery').click(); this._elementRef.nativeElement.xyz = 'bad with native element'; document.getElementById('bad-with-document'); } }
如你所见,doBadThings()
方法中有三行代码,这三行代码演示了直接操做 DOM 的三种方式。在 Angular 中咱们推荐经过 Renderer2
服务执行 DOM 操做 (Angular 2 中使用 Renderer)。
@Component({ ... }) export class HeroComponent { constructor( private _renderer2: Renderer2, private _elementRef: ElementRef) {} doGoodThings() { this._renderer2.setElementProperty(this._elementRef, 'some-property', true); } }
上面代码中,咱们经过依赖注入方式注入 Renderer2
和 ElementRef
实例,而后在 doGoodThings()
方法中调用 Renderer2
实例提供的 setElementProperty()
方法来设置元素的属性。 此外,为了方便开发者获取视图中的元素,Angular 为咱们提供了 @ViewChild
、@ViewChildren
、@ContentChild
和 @ContentChildren
等装饰器。
渲染器是视图层的封装。当咱们在浏览器中时,将使用默认渲染器。当应用程序在不一样平台 (如 WebWorker ) 上运行时,渲染器将被替换为平台对应的渲染器。此渲染器须要实现 Renderer2
抽象类,并利用 DI (依赖注入) 机制做为默认的 Renderer 对象注入到组件或服务中。
若想深刻了解 Angular 渲染器,能够参考专栏 Angular Renderer (渲染器) 这篇文章。
组件是 Angular 应用程序中的常见构建块。每一个组件都须要在 @NgModule.declarations
数组中声明,才可以使用。
在 Angular 中是不容许在多个模块中声明同一个组件,若是一个组件在多个模块中声明的话,那么 Angular 编译器将会抛出异常。例如:
@Component({ selector: 'hero', template: '...', }) export class HeroComponent { ... } @NgModule({ declarations: [HeroComponent] } export class HeroesModule { ... } @NgModule({ declarations: [HeroComponent] } export class AnotherModule { ... }
如你所见,HeroComponent
组件在 HeroesModule 以及 AnotherModule 中进行声明。在多个模块中使用同一个组件是容许的。但当这种状况发生时,咱们应该考虑模块之间的关系是什么。若是一个模块做为另外一个模块的子模块,那么针对上面的场景解决方案将是:
@NgModule.declaration
中声明 HeroComponent
组件@NgModule.exports
数组中导出该组件@NgModule.imports
数组中导入子模块而对于其它状况,咱们能够建立一个新的模块,如 SharedModule
模块。具体步骤以下:
NgModule({ declarations: [HeroComponent], exports: [HeroComponent] } export class SharedModule { ... } NgModule({ imports: [SharedModule] } export class HeroesModule { ... } @NgModule({ imports: [SharedModule] } export class AnotherModule { ... }