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