欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Angular性能优化之脏检测

程序员文章站 2022-06-01 11:17:41
angular性能优化之脏检测   当我们在使用 angular 框架搭建项目时,随着越来越多,页面也来越复杂,性能会越来越低,主要表现在 cpu 使用率 很高。所以...

angular性能优化之脏检测

 

当我们在使用 angular 框架搭建项目时,随着越来越多,页面也来越复杂,性能会越来越低,主要表现在 cpu 使用率 很高。所以我们要对项目做一定的优化。

angular脏检查(change detection)机制

angular 的脏检测主要是指 zone.js,这是一个开源的第三方库,github地址。

关于 zone.js 的定义,官方解释为:

a zone is an execution context that persists across async tasks, and allows the creator of the zone to observe and control execution of the code within the zone.

简单来说,一个 zone 可作为多个异步任务执行的上下文,并能够控制这些异步任务。详细可以查看这篇文章。

我们回归 angular 框架,angular 团队通过对 zone.js 封装,实现了 脏检查(change detection)机制。

当优化性能的时候,我们首先要考虑,哪些方面会影响程序的性能?总结主要有以下几点:

逻辑代码的复杂程度
这主要是受js/ts运算效率低的影响,所以项目中尽量减少使用js/ts做复杂运算。即使有 牛x 的 v8 引擎加持,还是要慎重,毕竟 javascript 是脚本语言,执行效率与生俱来的低,哪怕你可以用它写一个操作,哈哈哈!!

减少event handler
event handler的运行时间无疑对我们的性能有着重要的影响,比如 scrolbar和鼠标移动事件,窗口resize事件,这些都是触发频率很高的事件,极其影响性能,可以对其做 节流与 防抖,以提升性能。

dom 树复杂
如果你的项目是一个大型项目,且 dom元素错综复杂,那么你应该考虑如何拆解并将其组件化。

angular dom视图更新
大家都知道, angular的牛逼之处就是友好的双向绑定机制,但是绑定的值何时发生变化?何时更新?这也是这篇文章要达到的目的。

接下来我们简单介绍一下 脏检测(change detection),简称 cd。angular 默认是脏检查方法是从根组件开始,遍历所有的子组件进行脏检查。我们看一个 检测前 和 检测时 的模型。

脏检测之前:

Angular性能优化之脏检测

图中为组件树

变化检测时:

Angular性能优化之脏检测

那么何时触发脏检测?主要有以下几个方面:

ajax请求 timeout 延迟事件 鼠标事件

触发脏检测的目的就是 检测视图(dom) 有没有发生变化,方法就是比较 双向绑定中 view 和 model 是否一致。

可能看到这的童鞋还是有一些晕叉叉,接下来我们通过一个例子来分析。

demo

创建一个新的 angular 项目:
ng new testzone
  
创建一个子组件 view:
ng g c view
view.component.html文件

{{user.getname()}}
{{user.getage()}}
view.component.ts
import { component, input, docheck } from '@angular/core';

@component({
  selector: 'app-view',
  templateurl: './view.component.html',
  styleurls: ['./view.component.css'],
  changedetection: changedetectionstrategy.onpush
})
export class viewcomponent implements docheck {

  @input() user: any;

  index = 0;
  constructor() { }

  ngdocheck() {
    this.index++;
    console.log('view被执行', this.index);
  }
}
修改app.component.ts组件
import { component, docheck } from '@angular/core';

class user {
  _age = 25;
  _name = 'vincent';

  getage() {
    console.log('执行获取age');
    return this._age;
  }

  getname() {
    console.log('执行获取name');
    return this._name;
  }
}


@component({
  selector: 'app-root',
  templateurl: './app.component.html',
  styleurls: ['./app.component.css']
})
export class appcomponent implements docheck {

  index = 0;

  user = new user;
  change() {
  }

  ngdocheck() {
    this.index++;
    console.log('app被执行', this.index);
  }
}
app.component.html

click
运行
npm start
访问 https://localhost:4200/
Angular性能优化之脏检测
chrome 控制台:
Angular性能优化之脏检测
图中子组件 view模板内的user.getname()和user.getage()分别被执行四次,这里要介绍一下,由于我们是开发环境,所以user.getname()和user.getage()会被执行两次,当初始化的时候,触发 change detection脏检测,(angular 内部会调用 detectchanges 这个方法),开发模式下,angular 还会执行 checknochanges 这个方法去检验之前运行的change detection是否导致了别的改动。
Angular性能优化之脏检测
但是为何 ngdocheck 钩子也被执行了两次?
Angular性能优化之脏检测
这是因为在 组件初始化的时候,ngdocheck被调用了一次,
又因为执行了一次 脏检测 又调用了一次 ngdocheck
对比编译后的结果:
Angular性能优化之脏检测
编译后还是执行了两次,在这方面,我认为 angular 这种检测机制实在是很影响性能,有点鸡肋了。。。。更鸡肋的往下看。
此时我们 点击 按钮,再查看一下chrome控制台:
Angular性能优化之脏检测
Angular性能优化之脏检测
我们可以发现,这个按钮并没有任何实际作用,但是 angular 还是将getname()与getage()又执行了一遍,设想一下,如果你有很多组件,点击按钮后,检测了所有组件,那么这个效率?你认为会很高吗?

优化检测

类似的效率问题,主要有三种优化方法,逐一介绍。

使用changedetectionstrategy.onpush

在view.component.ts文件增加changedetectionstrategy.onpush,如下:
import { component, oninit, input, docheck, changedetectionstrategy } from '@angular/core';

@component({
  selector: 'app-view',
  templateurl: './view.component.html',
  styleurls: ['./view.component.css'],
  changedetection: changedetectionstrategy.onpush
})
export class viewcomponent implements oninit, docheck {

  @input() user: any;

  index = 0;
  constructor() { }

  ngoninit() {
  }

  ngdocheck() {
    this.index++;
    console.log('view被执行', this.index);
  }
}

重新运行结果如图:
Angular性能优化之脏检测
虽然 ngdocheck 钩子被执行两次,但是getname()与getage()函数只执行了一次, nice!!!,钩子函数在生产环境中要避免使用,即使使用也要尽量简单。
此时再点击 按钮,结果如图:
Angular性能优化之脏检测
只是 ngdocheck 钩子被执行了,这样就起到了优化的作用。
那么我们介绍一下changedetectionstrategy.onpush。angular 提供了相关机制可以让我们去“告诉”它应当在什么时候去运行 change detection。我们可以对应用中的每一个component设置change detection的策略。每个组件默认情况下的设置是changedetectionstrategy.default。意思就是任何事件都会导致组件被重新检测。
changedetectionstrategy还有一种策略叫做onpush,如果当前component设置成了onpush,那么当由当前component之外的事件触发的change detection在准备检查当前component之前,会先去检查该component的input,如果发现input没有变化,change detection会跳过这个component和其child component。
预警--------------------------!!!!!!!
在使用changedetectionstrategy.onpush时需要注意,只有两种情况下change detection会在该component内部运行检测:
input发生变化 由component内部的事件引起的change detection
所以在开发的时候,要清楚组件发生变化的情况。
----------------------------------------
我们还是通过一个例子来感受一下:
新创建一个view.service.ts服务。
ng g s view/view
内容为:
import { injectable } from '@angular/core';
import { subject } from 'rxjs';
@injectable({
  providedin: 'root'
})
export class viewservice {

  subject = new subject();
  constructor() { }

  send(count: number) {
    this.subject.next(count);
  }
}
修改app.component.ts和app.component.html
import { component, docheck } from '@angular/core';
import { viewservice } from './view/view.service';
@component({
  selector: 'app-root',
  templateurl: './app.component.html',
  styleurls: ['./app.component.css']
})
export class appcomponent implements docheck {
  index = 0;
  constructor(private viewservice: viewservice) {
  }
  change() {
    this.viewservice.send(100);
  }

  ngdocheck() {
    this.index++;
    console.log('app被执行', this.index);
  }
}
click
修改view.component.ts和view.component.html
import { component, oninit, docheck, changedetectionstrategy } from '@angular/core';
import { viewservice } from './view.service';

@component({
  selector: 'app-view',
  templateurl: './view.component.html',
  styleurls: ['./view.component.css'],
  changedetection: changedetectionstrategy.onpush
})
export class viewcomponent implements oninit, docheck {

  index = 0;
  count = 0;
  constructor(private viewservice: viewservice) {
    this.viewservice.subject.asobservable().subscribe(
      (count) => {
        this.count = count;
      }
    );
  }

  ngoninit() {
  }

  ngdocheck() {
    this.index++;
    console.log('view被执行', this.index);
  }
}

{{count}}
上一个例子我们是通过@input来触发检测,现在我们在使用了changedetectionstrategy.onpush后,通过rxjs订阅的方式来改变显示,运行如下:
chrome控制台结果:
Angular性能优化之脏检测
此时我们点击 按钮,并没有改变count的数值,不是100而是0,这时为什么?
还记得上面的预警吧,只有在下面两种情况下才会检测:
input发生变化 由component内部的事件引起的change detection
所以现在该怎么解决?
幸运的是,angular提供了changedetectorref这个类。我们可以将其注入需要调用的component,自己控制change detection的发生。
修改view.component.ts文件如下:
import { component, oninit, docheck, changedetectionstrategy, changedetectorref } from '@angular/core';
import { viewservice } from './view.service';

@component({
  selector: 'app-view',
  templateurl: './view.component.html',
  styleurls: ['./view.component.css'],
  changedetection: changedetectionstrategy.onpush
})
export class viewcomponent implements oninit, docheck {

  index = 0;
  count = 0;
  constructor(private viewservice: viewservice,
              private cdref: changedetectorref) {
    this.viewservice.subject.asobservable().subscribe(
      (count) => {
        this.count = count;
        this.cdref.detectchanges();
      }
    );
  }

  ngoninit() {
  }

  ngdocheck() {
    this.index++;
    console.log('view被执行', this.index);
  }
}
重新运行后,点击 按钮发生了变化。
除了detectchanges之外,changedetectorref中的markforcheck也可以解决我们的问题。不过和detectchanges不同的是,markforcheck会对在整个应用范围内都进行change detection。具体使用哪一个,取决于我们的实际需求了。

运用pipe

pipe是angular提供的又一个非常有用的功能,我们可以在模板中通过pipe对数据进行转换。
首先要说明的是angular有两种pipe:
pure pipe:如果传入pipe的参数没有变,会直接返回之前一次的结果 inpure pipe:每一次change detection都会重新运行pipe内部的逻辑并返回结果
所以我们可以不使用changedetectionstrategy.onpush,只要把例子中 user.getname()和user.getage()两个方法放到 pipe 中即可,个人认为不是很好用。。

在ngfor循环中使用trackby

这个优化本文并没有用到,相信使用过angular的你一定需要知道。
angular在更新dom的时候会删除所有和ngfor中数据相关的dom,然后再去根据新数据去重新挨个创建dom。在列表很大时,这无疑是一个非常昂贵又耗时的操作。我们想要的行为,应当保留新老数组中都存在的数据,对于其他数据进行删除或添加。
angular提供了trackby方法去帮我们实现这样的效果。trackby方法的第一个参数是当前元素在数组中的index,第二个是该元素本身。方法的返回值是当前数据的唯一标识。
例如有一个books数组,数组内是book对象,里面有唯一的name属性,唯一就是每个book的name是不一样的,这样我们就可以做优化。
... trackbyfn(index, book) { return book.name; }
这样在books数组内某个book发生变化时,angular会检测它的name属性是否发生变化,没有发生变化保留dom。被修改的name的book才会删除修改dom。