Angular 面试题

什么是 Angular

Angular 是一个开放源代码的前端 Web 框架。它是最流行的 JavaScript 框架之一,主要由 Google 维护。

它提供了一个轻松开发基于 Web 的应用程序的平台,并使前端开发人员能够管理跨平台应用程序。

它集成了强大的功能,例如声明性模板,依赖项注入以及各种其他使开发路径更流畅的最佳实践。

Angular 通常用于表示单页应用程序的 SPA 的开发。

Angular 提供了一组现成的模块,可简化单页应用程序的开发。

不仅如此,Angular 还具有内置数据流,类型安全性和模块化 CLI 的功能,被认为是成熟的 Web 框架。


Angular 有什么优势

可伸缩性:基于 RxJS 、immutable.js 和其他推送模型,能适应海量数据需求

跨平台:渐进式应用(高性能、离线使用、免安装),原生(Ionic),桌面端

生产率:模版(通过简单而强大的模版语法,快速创建 UI 视图),CLI(快速进入构建环节、添加组件和测试,然后立即部署)

测试:单元测试(支持 Karma、Jasmine 等工具进行单元测试),端到端测试(支持 Protractor 等工具进行端到端测试)

优点:

  1. 数据双向绑定
  2. 添加自定义的指令、管道
  3. 支持表单验证
  4. 支持依赖注入
  5. 强大的表单功能

缺点:

  1. 太过笨重
  2. 编译速度慢

Angular 和 jQuery 的区别

特征 jQuery Angular
DOM 操作
RESTful API 没有
动画支持
深层链接路由 没有
表格验证 没有
双向数据绑定 没有
AJAX / JSONP

Angular 和 Vue 的区别

  • vue和angular都可以进行动态绑定,例如动态绑定样式,绑定数据,条件式渲染等。vue中的动态绑定都以v开头,而angular以ng开头
  • Vue中冒号相当于angular中的中括号,可以单向绑定动态的值
  • angular中的属性型指令,ngclass,ngstyle,对应到vue中就是:class, :style,ngif, ngswitchcase, v-if v-else-if
  • 都有依赖注入的概念,angular是注入service,注入的是功能所需的服务或者对象,此时组件会从外部源请求依赖项而不是创建他们,service中一般都会存放一些抽离于视图,只处理业务的一些通用逻辑,通过provider提供,直接注入进构造器中。而vue是为了解决props逐级透传问题而提出的依赖注入
  • 都是以html为模板,而不是像react一样嵌套着js写,不同:angular是html,css,ts解耦的写法,也就是数据,视图,样式分离的写法,而vue是单文件类型的写法
  • angular的写法相当于vue3中的组合式api,因为方法,属性等都集中在一起,不需要像vue2的选项式api一样反复来回查看不同地方的代码,因此研发效率高
  • angular中ts是必选项,而vue中是可选项

Angular 依赖注入

Dependency Injection(DI)是一种设计模式,在这种设计模式中,类会从外部源请求依赖项而不是创建它们。

在项目中,有人提供服务,有人消耗服务,而依赖注入的机制提供了中间的接口,并替消费者创建并初始化处理。

消费者只需要知道拿到的是完整可用的服务就好,至于这个服务内部的实现,甚至是它又依赖了怎样的其他服务,都不需要关注。

Angular 通过 service 共享状态,而这些管理状态和数据的服务便是通过依赖注入的方式进行处理的。

Angular 的 service 的本质就是依赖注入,将 service 作为一个 Injector 注入到 component 中

很多时候我们创建服务就是为了维护公用的状态和数据,通过依赖注入的方式来规定哪些组件可共享。

优点:

  • 松耦合,增加了代码的可扩展性
  • 使代码可重用性更高

控制反转定义(IOC)

控制什么?

控制外部的一个资源获取

反转是什么?

我们需要知道什么是正转,正转就是通过new主动去创建一个对象。

反转:由反转容器去创建控制对象,将控制权从代码内部转入代码外部,这就是控制反转。

控制反转的手段叫做依赖注入,控制反转是目的。

传统应用程序都是由我们在类内部主动创建依赖对象,也就是所谓的new。

从而导致类与类之间高耦合,难于测试;

有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活

实现控制反转的容器叫做 ioc 容器,angular 就是 ioc 容器


Angular Service

服务(Service)充当着数据访问,逻辑处理的功能。

好处:把组件和服务区分开,保持业务逻辑模块组件内的纯净和高内聚,以提高模块性和复用性。

单例服务(singleton)

使用 Angular CLI 创建服务,默认会创建单例服务;

把 @Injectable () 的 providedIn 属性声明为 root, 即为单例服务。

单例服务(singleton)对象,可以用于临时存放全局变量。

对于复杂的全局变量,推荐使用状态管理组件(state management - Ngrx)。

forRoot () 模式

如果多个调用模块同时定义了 providers (服务),那么在多个特性模块中加载此模块时,这些服务就会被注册在多个地方。

这会导致出现多个服务实例,并且该服务的行为不再像单例一样 。有多种方式来防止这种现象:

  • 用 providedIn 语法代替在模块中注册服务的方式。
  • 把服务分离到它们自己的模块中。
  • 在模块中分别定义 forRoot () 和 forChild () 方法。

Angular Module

定义:相当于应用程序中的边界,用来分离应用程序之间的功能。

imports :属性告诉angular需要导入加载哪些模块

declarations:属性用于声明本模块中拥有的视图类,Angular有三种视图类:组件、指令和管道

providers:表明该模块可以提供的服务,管道等

bootstrap:当这个模块被引导时被引导的一组组件。这里列出的组件会自动添加到entryComponents中。告诉angular根组件是什么

entrycomponent:定义要编译的组件集,这样它们才可以动态加载到视图中


数据绑定的方式有哪些

  • 字符串插值(花括号绑定)
  • 属性绑定(单向绑定)
  • 事件绑定(output事件绑定)
  • 双向数据绑定(相当于属性绑定和事件绑定结合的语法糖)

Angular 双向绑定

单向数据绑定中,无论何时更改数据模型,“视图” 或 “ UI” 部分都不会自动更新。需要手动编写自定义代码,以便在每次视图更改时对其进行更新。

而在双向数据绑定中,一旦更改数据模型,则隐式更新 View 或 UI 部分。与单向数据绑定不同,这是一个同步过程。

Angular 的双向绑定,通过脏数据检查(Dirty checking)来实现。

脏值检测的基本原理是存储旧数值,并在进行检测时,把当前时刻的新值和旧值比对。

若相等则没有变化,反之则检测到变化,需要更新视图。


Angular 生命周期

生命周期函数通俗的讲就是组件创建、组件更新、组件销毁的时候会触发的一系列的方法

当 Angular 使用构造函数新建一个组件或指令后,就会按下面规定的顺序在特定时刻调用生命周期钩子:

  • constructor :构造函数永远首先被调用,一般用于变量初始化以及类实例化
  • ngOnChanges :被绑定的输入属性变化时被调用,首次调用一定在 ngOnInit 之前。输入属性发生变化是触发,但组件内部改变输入属性是不会触发的。注意:如果组件没有输入,或者使用它时没有提供任何输入,那么框架就不会调用 ngOnChanges
  • ngOnInit :组件初始化时被调用,在第一轮 ngOnChanges 完成之后调用,只调用一次。使用 ngOnInit 可以在构造函数之后马上执行复杂的初始化逻辑,同时在 Angular 设置完输入属性之后,可以很安全的对该组件进行构建
  • ngDoCheck :脏值检测时调用,在变更检测周期中 ngOnChanges 和 ngOnInit 之后
  • ngAfterContentInit :内容投影 ng-content 完成时调用,只在第一次 ngDoCheck 之后调用
  • ngAfterContentChecked: 每次完成被投影组件内容的变更检测之后调用(多次)
  • ngAfterViewInit :组件视图及子视图初始化完成时调用,只在第一次 ngAfterContentChecked 调用一次
  • ngAfterViewChecked: 检测组件视图及子视图变化之后调用(多次)
  • ngOnDestroy :当组件销毁时调用,可以反订阅可观察对象和分离事件处理器,以防内存泄漏

父子组件初始化时生命周期钩子调用顺序:

parent-ngOnChanges
parent-ngOnInit
parent-ngDoCheck
parent-ngAfterContent
parent-ngAfterContentked

child-ngChanges
child-ngOnInit
child-ngDoCheck
child-ngAfterContentInit
child-ngAfterContentChecked
child-ngAfterViewInit
child-ngAfterViewChecked

parent-ngAfterViewInit
parent-ngAfterViewChecked

再次检测时

parent-ngDoCheck
parent-ngAfterContentked
child-ngDoCheck
child-ngAfterContentChecked
child-ngAfterViewChecked
parent-ngAfterViewChecked


父子组件通信方式

  • 装饰器 input/output
  • 通过 service

管道的作用

Angular 管道是编写可以在 HTML 组件中声明的显示值转换的方法

管道将数据作为输入并将其转换为所需的输出

管道其实就是过滤器,用来转换数据然后显示给用户

管道将整数、字符串、数组和日期作为输入,用 | 分隔,然后根据需要转换格式,并在浏览器中显示出来


Angular 变更检测策略

Angular 有两种变更检测策略:Default 和 OnPush

可以通过在 @Component 元数据中设置 changeDetection: ChangeDetectionStrategy.OnPush 进行切换

Default:

优点:每一次有异步事件发生,Angular 都会触发变更检测,从根组件开始遍历其子组件,对每一个组件都进行变更检测,对 dom 进行更新。

缺点:有很多组件状态没有发生变化,无需进行变更检测。如果应用程序中组件越多,性能问题会越来越明显。

OnPush:

优点:组件的变更检测完全依赖于组件的输入 (@Input),只要输入值不变就不会触发变更检测,也不会对其子组件进行变更检测,在组件很多的时候会有明显的性能提升。

缺点:必须保证输入 (@Input) 是不可变的 (可以用 Immutable.js 解决),每一次输入变化都必须是新的引用。


常用到的装饰器

  • component 组件
  • directive 指令
  • pipe 管道
  • ngModule 模块
  • input 父传子 输入
  • output 子传父 传出

数据之间通信有哪些方式

  • 使用事件
  • 使用服务
  • 使用继承
  • 使用rxjs

AOT和JIT

AOT:ahead of time预编译

JIT: just in time即时编译

在Angular 8之前,JIT编译是默认的,现在默认是AOT。

当运行ng build(只构建)或ng serve(本地构建和服务)CLI命令时,编译的类型(JIT或AOT)取决于angular.json中构建配置中指定的AOT属性的值。缺省情况下,aot为true。

AOT的好处:

  • 更快的渲染速度:浏览器下载应用程序的预编译版本。因此它可以立即呈现应用程序而无需编译应用程序。
  • 更小的下载体积:不需要下载 Angular 编译器。因此,它大大减少了应用程序的有效负载。
  • 更早的检测模板错误
  • 更加安全:它将 HTML 模板和组件编译成 JavaScript。所以不会有任何注入攻击。

router 和 route

router: 从一个页面跳转到另一个页面的机制,里面有router-link、router-outlet

route: 一个路由器,定义了路由跳转规则,路由器必须配置有路由定义列表。可以通过 RouterModule.forRoot() 方法使用路由配置路由器,并将结果添加到 AppModule 的导入数组


Zone是什么

Zone 是跨异步任务持续存在的执行上下文。

当本机 JavaScript 操作引发事件时,Angular 依赖于 zone.js 来运行 Angular 的变更检测


如何提高 Angular 的性能

Angular 也还是网页应用,所以一般的提高网页西能的技巧都是通用的。

针对 Angular,还有一些特殊的优化技巧:

  • AOT 编译
  • 去掉不必要的 import 语句。如果有遗留,那么打包时也会打进来。
  • 确保应用中已经移除了不使用的第三方库
  • 项目较大时,考虑延迟载入 (Lazy Loading), 保证首页的加载速度
const routes: Routes = [
  {
    
    
    path: 'customers',
    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
  }
];

RxJS是什么?基本概念有哪些

RxJS 本质是个工具库,处理的是事件,这里的 events 可以称之为流

RxJS 结合了函数式编程、观察者模式(例如 DOM EventListener)、迭代器模式(例如 ES6 Iterater)

那么流是指什么呢?

举个例子,代码中每 1s 输出一个数字,用户每一次对元素的点击,就像是在时间这个维度上,产生了一个数据集。这个数据集不像数组那样,它不是一开始都存在的,而是随着时间的流逝,数据一个一个被输出。这种异步行为产生的数据,就可以被称之为一个流。

在 RxJS 中,称之为 ovservalbe(抛开英文,本质其实就是一个数据的集合,只是这些数据不一定是一开始就设定好的,而是随着时间而不断产生的)

而 RxJS,就是为了处理这种流而产生的工具,比如流与流的合并、流的截断、延迟、消抖等

基本概念

  • Observable: 相当于是数据源,一个可观察对象
  • Subscribe: 相当于去订阅监听这个数据源的流动
  • Observer: 观察者,相当于一个哨兵,推送通知的消费者的接口,里面有next,error,complete(单播,只会推送一次)。从行为上来看,无非就是定义了如何处理上述流产生的数据,称之为流的处理方法。
  • Subscription:就是表示Observable的执行,它的本质就是暂存了一个启动后的流,每一个启动后的流都是相互独立的,而这个启动后的流,就存储在 subscription 中,提供了 unsubscribe方法,来停止这个流。
  • subject: 它是一个代理对象,既是一个 Observable 又是一个 Observer,它可以同时接受 Observable 发射出的数据,也可以向订阅了它的 observer 发射数据。同时,Subject 会对内部的 observers 清单进行多播 (multicast),Subjects 是将任意 Observable 执行共享给多个观察者的唯一方式

从数据的角度来思考:

observable 定义了要生成一个什么样的数据

其 subscribe 方法接收一个 observer(定义了接收到数据如何处理),并开始产生数据

该方法的返回值 subscription, 存储了这个已经开启的流,同时具有 unscbscribe 方法,可以将这个流停止

整理成这个公式:

Subscription = Observable.subscribe (observer)

observable: 随着时间产生的数据集合,可以理解为流,其 subscribe 方法可以启动该流

observer: 决定如何处理数据

subscription: 存储已经启动过的流,其 unsubscribe 方法可以停止该流


promise和observable的区别

observable promise
计算在订阅之前不会开始,因此它们可以在您需要结果时运行 在创建时立刻执行
subscribe返回可以有多个值 then返回只能有一个值
订阅方法中可以进行错误处理,使错误处理集中且可预测 将错误推送给子promise
Observable 可以取消 Promise 不可以取消

RxJS常用的Operators操作符

  • from

从一个数组、类数组对象、Promise、迭代器对象或者类 Observable 对象创建一个 Observable。该方法就有点像 js 中的 Array.from 方法(可以从一个类数组或者可迭代对象创建一个新的数组),只不过在 RxJS 中是转成一个 Observable 给使用者使用。

const source = Rx.Observable.from([10, 20, 30]);
source.subscribe(v => console.log(v));

// 10
// 20
// 30
  • of

与 from 的能力差不多,只不过在使用的时候是传入一个一个参数来调用的,有点类似于 js 中的 concat 方法。同样也会返回一个 Observable,它会依次将你传入的参数合并并将数据以同步的方式发出。

const source = Rx.Observable.of(1, 2, 3);
source.subscribe(v => console.log(v));

// 1
// 2
// 3
  • take

只发出源 Observable 最初发出的 N 个值 (N = count)

用于控制只获取特定数目的值,跟 interval 这种会持续发送数据的配合起来就能自主控制要多少个值了。

  • skip

返回一个 Observable, 该 Observable 跳过源 Observable 发出的前 N 个值 (N = count)。

假设这个数据源发送 6 个值,可以使用 skip 操作符来跳过前多少个。

const source = Rx.Observable.from([1, 2, 3, 2, 4, 3]);
const result = source.skip(2);
result.subscribe(x => console.log(x));

// 打印结果为:3、2、4、3,跳过了前面两个数。
  • debounceTime

功能与 debounce 防抖函数差不多,只有在特定的一段时间经过后并且没有发出另一个源值,才从源 Observable 中发出一个值。

假设一个数据源每隔一秒发送一个数,而我们使用了 debounceTime 操作符,并设置了延时时间,那么在数据源发送一个新数据之后,如果在延时时间内数据源又发送了一个新数据,这个新的数据就会被先缓存住不会发送,等待发送完数据之后并等待延时时间结束才会发送给订阅者。不仅如此,在延时时间未到的时候并且已有一个值在缓冲区,这个时候又收到一个新值,那么缓冲区就会把老的数据抛弃放入新的,然后重新等待延时时间到达然后将其发送。

const source = Rx.Observable.interval(1000).take(3);
const result = source.debounceTime(2000);
result.subscribe(x => console.log(x));

// 程序启动之后的前三秒没有数据打印,等到五秒到了之后,打印出一个2,接着就没有再打印了
// 数据源会每秒依次发送三个数 0、1、2,由于我们设定了延时时间为 2 秒,那么也就是说,我们在数据发送完成之前都是不可能看到数据的,因为发送源的发送频率为 1 秒,延时时间却有两秒,也就是除非发送完,否则不可能满足发送源等待两秒再发送新数据,每次发完新数据之后要等两秒之后才会有打印,所以不论我们该数据源发送多少个数,最终订阅者收到的只有最后一个数。

猜你喜欢

转载自blog.csdn.net/weixin_46232841/article/details/128904998