تحسين رصد التغيير في Angular

تنفيذ ميزة رصد التغييرات بشكلٍ أسرع لتحسين تجربة المستخدم

يشغّل Angular آلية رصد التغيير بشكل دوري لكي تظهر التغييرات التي يتم إجراؤها على نموذج البيانات في عرض التطبيق. يمكن بدء اكتشاف التغيير إما يدويًا أو من خلال حدث غير متزامن (على سبيل المثال، تفاعل مستخدم أو إكمال XHR).

يُعتبر رصد التغيير أداة فعّالة، ولكن إذا تم تشغيله كثيرًا، قد يؤدي إلى إجراء العديد من العمليات الحسابية وحظر سلسلة التعليمات الرئيسية في المتصفِّح.

ستتعرّف في هذه المشاركة على كيفية التحكّم في آلية اكتشاف التغيير وتحسينها من خلال تخطي أجزاء من تطبيقك وتشغيل اكتشاف التغيير عند الضرورة فقط.

اكتشاف التغيير في Angular

لفهم كيفية عمل اكتشاف التغيير في Angular، لنلقِ نظرة على نموذج تطبيق!

يمكنك العثور على رمز التطبيق في مستودع جيت هب هذا.

يسرد التطبيق الموظفين من قسمين في الشركة - المبيعات والبحث والتطوير - ويحتوي على مكونين:

  • AppComponent، وهو المكوّن الجذر للتطبيق
  • هناك مثالان من EmployeeListComponent، أحدهما للمبيعات والآخر للبحث والتطوير.

نموذج تطبيق

يمكنك الاطّلاع على مثيلَي EmployeeListComponent في نموذج AppComponent:

<app-employee-list
  [data]="salesList"
  department="Sales"
  (add)="add(salesList, $event)"
  (remove)="remove(salesList, $event)"
></app-employee-list>

<app-employee-list
  [data]="rndList"
  department="R&D"
  (add)="add(rndList, $event)"
  (remove)="remove(rndList, $event)"
></app-employee-list>

لكل موظف هناك اسم وقيمة رقمية. يمرر التطبيق القيمة الرقمية للموظف إلى حساب الأعمال ويصور النتيجة على الشاشة.

يمكنك الآن إلقاء نظرة على "EmployeeListComponent":

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Component(...)
export class EmployeeListComponent {
  @Input() data: EmployeeData[];
  @Input() department: string;
  @Output() remove = new EventEmitter<EmployeeData>();
  @Output() add = new EventEmitter<string>();

  label: string;

  handleKey(event: any) {
    if (event.keyCode === 13) {
      this.add.emit(this.label);
      this.label = '';
    }
  }

  calculate(num: number) {
    return fibonacci(num);
  }
}

تقبل ميزة "EmployeeListComponent" قائمة بالموظفين واسم القسم كإدخالات. عندما يحاول المستخدم إزالة موظف أو إضافته، يؤدي المكون إلى تشغيل مخرجات مقابلة. يحدّد المكوّن أيضًا طريقة calculate التي تنفذ حساب النشاط التجاري.

إليك نموذج EmployeeListComponent:

<h1 title="Department">{{ department }}</h1>
<mat-form-field>
  <input placeholder="Enter name here" matInput type="text" [(ngModel)]="label" (keydown)="handleKey($event)">
</mat-form-field>
<mat-list>
  <mat-list-item *ngFor="let item of data">
    <h3 matLine title="Name">
      {{ item.label }}
    </h3>
    <md-chip title="Score" class="mat-chip mat-primary mat-chip-selected" color="primary" selected="true">
      {{ calculate(item.num) }}
    </md-chip>
  </mat-list-item>
</mat-list>

وتتكرر هذه التعليمة البرمجية عبر جميع الموظفين في القائمة، وتعرض عنصر قائمة لكل موظف. ويتضمّن أيضًا توجيه ngModel لربط البيانات ثنائي الاتجاه بين الإدخال والسمة label التي تم تعريفها في EmployeeListComponent.

في حالتَي EmployeeListComponent، يكوّن التطبيق شجرة المكوّنات التالية:

شجرة المكوّنات

AppComponent هو المكوّن الجذر في التطبيق. مكوّناته الفرعية هي مثيلا EmployeeListComponent. يحتوي كل مثيل على قائمة بالعناصر (E1 ، E2، إلخ.) التي تمثّل الموظفين الفرديين في القسم.

عندما يبدأ المستخدم في إدخال اسم موظف جديد في مربّع الإدخال في EmployeeListComponent، تفعِّل Angular رصد التغيير في شجرة المكوّنات بالكامل بدءًا من AppComponent. وهذا يعني أنه أثناء كتابة المستخدم في إدخال النص، يعيد Angular حساب القيم الرقمية المرتبطة بكل موظف بشكل متكرر للتحقق من أنها لم تتغير منذ آخر فحص.

ولمعرفة مدى بطء التطبيق، افتح الإصدار غير المحسَّن من المشروع على StackBlitz وجرِّب إدخال اسم الموظف.

يمكنك التأكّد من أنّ نشاط تبطيء الرسائل ناجم عن الدالة fibonacci من خلال إعداد نموذج المشروع وفتح علامة التبويب الأداء في "أدوات مطوري البرامج في Chrome".

  1. اضغط على "Control+Shift+J" (أو "Command+Option+J" على نظام التشغيل Mac) لفتح "أدوات مطوّري البرامج".
  2. انقر على علامة التبويب الأداء.

مرة أخرى لإيقاف التسجيل. بعد أن تعالج "أدوات مطوري البرامج في Chrome" كل بيانات التحليل التي جمعتها، ستظهر لك على النحو التالي:

تحليل الأداء

وإذا كان هناك العديد من الموظفين في القائمة، قد تحظر هذه العملية مؤشر ترابط واجهة المستخدم الخاصة بالمتصفّح وتؤدي إلى حالات انخفاض في الإطارات، ما يؤدي إلى ترك انطباع سيئ لدى المستخدم.

تخطي الأشجار الفرعية للمكونات

عندما يكتب المستخدم مُدخلاً نصيًا لفريق المبيعات EmployeeListComponent، تكون على عِلم بأنّ البيانات في قسم البحث والتطوير لن تتغيّر، لذا ليس هناك سبب لتفعيل ميزة رصد التغيير في مكوّنه. للتأكّد من أنّ مثيل البحث والتطوير لا يشغّل رصد التغيير، اضبط changeDetectionStrategy من EmployeeListComponent على OnPush:

import { ChangeDetectionStrategy, ... } from '@angular/core';

@Component({
  selector: 'app-employee-list',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrls: ['employee-list.component.css']
})
export class EmployeeListComponent {...}

الآن عندما يكتب المستخدم إدخالاً نصيًا، لا يتم بدء رصد التغيير إلا للقسم المقابل:

تغيير الرصد في شجرة فرعية للمكوِّن

يمكنك العثور على هذا التحسين المطبَّق على التطبيق الأصلي هنا.

يمكنك الاطّلاع على المزيد من المعلومات عن استراتيجية رصد التغييرات في OnPush في مستندات Angular الرسمية.

لمعرفة تأثير هذا التحسين، أدخِل موظفًا جديدًا في الطلب على StackBlitz.

استخدام أنابيب نقية

على الرغم من ضبط استراتيجية رصد التغيير في EmployeeListComponent الآن على OnPush، لا يزال Angular يعيد احتساب القيمة الرقمية لجميع الموظفين في أحد الأقسام عندما يكتب المستخدم إدخال النص المقابل.

لتحسين هذا السلوك، يمكنك الاستفادة من الممرات الخالصة. تقبل كل من الممرات النقية وغير النقية المدخلات وتعرض النتائج التي يمكن استخدامها في قالب. الفرق بين الاثنين هو أن الممر الخالص لن يعيد حساب نتيجته إلا إذا تلقى مدخلات مختلفة عن استدعائه السابق.

تذكَّر أنّ التطبيق يحسب القيمة التي سيتم عرضها بناءً على القيمة الرقمية للموظف، ويستدعي طريقة calculate المحدّدة في EmployeeListComponent. إذا نقلت العملية الحسابية إلى ممر نقي، فسيعيد Angular حساب تعبير الممر فقط عند تغيير وسيطاته. سيحدد إطار العمل ما إذا كانت وسيطات الممر قد تغيرت عن طريق إجراء فحص مرجعي. هذا يعني أن Angular لن يقوم بإجراء أي عمليات إعادة حسابية ما لم يتم تحديث القيمة الرقمية لموظف.

في ما يلي كيفية نقل حساب النشاط التجاري إلى ممر يسمى CalculatePipe:

import { Pipe, PipeTransform } from '@angular/core';

const fibonacci = (num: number): number => {
  if (num === 1 || num === 2) {
    return 1;
  }
  return fibonacci(num - 1) + fibonacci(num - 2);
};

@Pipe({
  name: 'calculate'
})
export class CalculatePipe implements PipeTransform {
  transform(val: number) {
    return fibonacci(val);
  }
}

تستدعي طريقة transform للممر الدالة fibonacci. لاحظ أن الممر نقي. تعتبر Angular جميع الممرات نقية ما لم تحدد خلاف ذلك.

أخيرًا، عدِّل التعبير داخل النموذج لـ EmployeeListComponent:

<mat-chip-list>
  <md-chip>
    {{ item.num | calculate }}
  </md-chip>
</mat-chip-list>

أكملت هذه الخطوة. عندما يكتب المستخدم الآن إدخال النص المرتبط بأي قسم، لن يعيد التطبيق حساب القيمة الرقمية للموظفين الفرديين.

في التطبيق أدناه، يمكنك أن ترى مدى سلاسة الكتابة!

لمعرفة تأثير التحسين الأخير، يُرجى تجربة هذا المثال على StackBlitz.

إنّ الرمز البرمجي الذي تم تحسينه فقط من خلال التطبيق الأصلي متوفّر هنا.

الخلاصة

عند مواجهة بطء في وقت التشغيل في تطبيق Angular:

  1. حدِّد التطبيق باستخدام "أدوات مطوري البرامج في Chrome" لمعرفة مصدر حالات البطء في الأداء.
  2. قدِّم استراتيجية رصد التغيير في OnPush لقص الأشجار الفرعية لأحد المكوّنات.
  3. انقل العمليات الحسابية الثقيلة إلى أنابيب نقية للسماح لإطار العمل بإجراء تخزين مؤقت للقيم المحسوبة.