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

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

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

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

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

رصد التغيير في Inside 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. اضغط على "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. انقل العمليات الحسابية الثقيلة إلى مسارات نقية للسماح لإطار العمل بتنفيذ التخزين المؤقت للقيم المحسوبة.