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

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

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

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

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

داخل ميزة رصد التغييرات في Angular

لفهم آلية عمل ميزة رصد التغييرات في Angular، لنلقِ نظرة على نموذج تطبيق.

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

يُدرج التطبيق الموظفين من قسمَين في شركة، وهما قسم المبيعات وقسم البحث والتطوير، ويتضمّن مكوّنين:

  • 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. اضغط على Ctrl ‏+ Shift ‏+ J (أو Command ‏+ Option ‏+ J على نظام التشغيل Mac) لفتح DevTools.
  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. نقل العمليات الحسابية المكثّفة إلى قنوات خالصة للسماح للإطار العملي بتنفيذ ميزة التخزين المؤقت للقيم المحسوبة