وقتی تغییری را در اشیاء خود به وجود میآورید، Angular بلافاصله متوجه آنها شده و viewها را به روز رسانی میکند. هدف از این مکانیزم، اطمینان حاصل کردن از همگام بودن اشیاء مدلها و viewها هستند. آگاهی از نحوهی انجام این عملیات، کمک شایانی را به بالابردن کارآیی یک برنامهی با رابط کاربری پیچیدهای میکند. یک شیء مدل در Angular عموما به سه طریق تغییر میکند:
- بروز رخدادهای DOM مانند کلیک
- صدور درخواستهای Ajax ایی
- استفاده از تایمرها (setTimer, setInterval)
ردیابهای تغییرات در Angular
تمام برنامههای Angular در حقیقت سلسله مراتبی از کامپوننتها هستند. در زمان اجرای برنامه، Angular به ازای هر کامپوننت، یک تشخیص دهندهی تغییرات را ایجاد میکند که در نهایت سلسله مراتب ردیابها را همانند سلسله مراتب کامپوننتها ایجاد خواهد کرد. هر زمانیکه ردیابی فعال میشود، Angular این درخت را پیموده و مواردی را که تغییراتی را گزارش دادهاند، بررسی میکند. این پیمایش به ازای هر تغییر رخ دادهی در مدلهای برنامه صورت میگیرد و همواره از بالای درخت شروع شده و به صورت ترتیبی تا پایین آن ادامه پیدا میکند:
این پیمایش ترتیبی از بالا به پایین، از این جهت صورت میگیرد که اطلاعات کامپوننتها از والدین آنها تامین میشوند. تشخیص دهندههای تغییرات، روشی را جهت ردیابی وضعیت پیشین و فعلی یک کامپوننت ارائه میدهد تا Angular بتواند تغییرات رخداده را منعکس کند. اگر Angular گزارش تغییری را از یک تشخیص دهندهی تغییر دریافت کند، کامپوننت مرتبط را مجددا ترسیم کرده و DOM را به روز رسانی میکند.
استراتژیهای تشخیص تغییرات در Angular
برای درک نحوهی عملکرد سیستم تشخیص تغییرات نیاز است با مفهوم value types و reference types در JavaScript آشنا شویم. در JavaScript نوعهای زیر value type هستند:
• Boolean
• Null
• Undefined
• Number
• String
و نوعهای زیر Reference type محسوب میشوند:
• Arrays
• Objects
• Functions
مهمترین تفاوت بین این دو نوع، این است که برای دریافت مقدار یک value type فقط کافی است از stack memory کوئری بگیریم. اما برای دریافت مقادیر reference types باید ابتدا در جهت یافتن شماره ارجاع آن، از stack memory کوئری گرفته و سپس بر اساس این شماره ارجاع، اصل مقدار آنرا در heap memory پیدا کنیم.
این دو تفاوت را میتوان در شکل زیر بهتر مشاهده کرد:
استراتژی Default یا پیشفرض تشخیص تغییرات در Angular
همانطور که پیشتر نیز عنوان شد، Angular تغییرات یک شیء مدل را در جهت تشخیص تغییرات و انعکاس آنها به View برنامه، ردیابی میکند. در این حالت هر تغییری بین حالت فعلی و پیشین یک شیء مدل برای این منظور بررسی میگردد. در اینجا Angular این سؤال را مطرح میکند: آیا مقداری در این مدل تغییر یافتهاست؟
اما برای یک reference type میتوان سؤالات بهتری را مطرح کرد که بهینهتر و سریعتر باشند. اینجاست که استراتژی OnPush تشخیص تغییرات مطرح میشود.
استراتژی OnPush تشخیص تغییرات در Angular
ایده اصلی استراتژی OnPush تشخیص تغییرات در Angular در immutable فرض کردن reference types نهفتهاست. در این حالت هر تغییری در شیء مدل، سبب ایجاد یک ارجاع جدید به آن در stack memory میشود. به این ترتیب میتوان تشخیص تغییرات بسیار سریعتری را شاهد بود. چون دیگر در اینجا نیازی نیست تمام مقادیر یک شیء را مدام تحت نظر قرار داد. همینقدر که ارجاع آن در stack memory تغییر کند، یعنی مقادیر این شیء در heap memory تغییر یافتهاند.
در این حالت Angular دو سؤال را مطرح میکند: آیا ارجاع به یک reference type در stack memory تغییر یافتهاست؟ اگر بله، آیا مقادیر آن در heap memory تغییر کردهاند؟ برای مثال جهت بررسی تغییرات یک آرایهی با 30 عضو، دیگر در ابتدای کار نیازی نیست تا هر 30 عضو آن بررسی شوند (برخلاف حالت پیشفرض بررسی تغییرات). در حالت استراتژی OnPush، ابتدا مقدار ارجاع این آرایه در stack memory بررسی میشود. اگر تغییری در آن صورت گرفته بود، به معنای تغییری در اعضای آرایهاست.
استراتژی OnPush در یک کامپوننت به نحو ذیل فعال و انتخاب میشود و مقدار پیشفرض آن ChangeDetectionStrategy.Default است:
import {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
// ...
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
// ...
}
استراتژی OnPush تغییرات یک کامپوننت را در حالتهای ذیل نیز ردیابی میکند:
- اگر مقدار یک خاصیت از نوع Input@ تغییر کند.
- اگر یک event handler رخدادی را صادر کند.
- اگر سیستم ردیابی به صورت دستی فراخوانی شود.
- اگر سیستم ردیاب تغییرات child component آن، اجرا شود.
نوعهای ارجاعی Immutable
همانطور که عنوان شد، شرط کار کردن استراتژی OnPush، داشتن نوعهای ارجاعی immutable است. اما نوع ارجاعی immutable چیست؟
Immutable بودن به زبان ساده به این معنا است که ما هیچگاه جهت تغییر مقدار خاصیتی در یک شیء، آن خاصیت را مستقیما مقدار دهی نمیکنیم؛ بلکه کل شیء را مجددا مقدار دهی میکنیم.
برای نمونه در مثال زیر، خاصیت foo شیء before مستقیما مقدار دهی شدهاست:
static mutable() {
var before = {foo: "bar"};
var current = before;
current.foo = "hello";
console.log(before === current);
// => true
}
اما اگر بخواهیم با آن به صورت Immutable «رفتار» کنیم، کل این شیء را جهت اعمال تغییرات، مقدار دهی مجدد خواهیم کرد:
static immutable() {
var before = {foo: "bar"};
var current = before;
current = {foo: "hello"};
console.log(before === current);
// => false
}
البته باید دقت داشت در هر دو مثال، شیءهای ایجاد شده در اصل mutable هستند؛ اما در مثال دوم، با این شیء به صورت immutable «رفتار» شدهاست و صرفا «تظاهر» به رفتار immutable با یک شیء ارجاعی صورت گرفتهاست.
معرفی کتابخانهی Immutable.js
جهت ایجاد اشیاء واقعی immutable کتابخانهی
Immutable.js توسط Facebook ایجاد شدهاست و برای کار با استراتژی تشخیص تغییرات OnPush در Angular بسیار مناسب است.
برای نصب آن دستور ذیل را صادر نمائید:
npm install immutable --save
یک نمونه مثال از کاربرد ساختارهای دادهی List و Map آن برای کار با آرایهها و اشیاء:
import {Map, List} from 'immutable';
var foobar = {foo: "bar"};
var immutableFoobar = Map(foobar);
console.log(immutableFooter.get("foo"));
// => bar
var helloWorld = ["Hello", "World!"];
var immutableHelloWorld = List(helloWorld);
console.log(immutableHelloWorld.first());
// => Hello
console.log(immutableHelloWorld.last());
// => World!
helloWorld.push("Hello Mars!");
console.log(immutableHelloWorld.last());
// => Hello Mars!
تغییر ارجاع به یک شیء با استفاده از spread properties const user = {
name: 'Max',
age: 30
}
user.age = 31
در این مثال تنها خاصیت age شیء user به روز رسانی میشود. بنابراین ارجاع به این شیء تغییر نخواهد کرد و اگر از روش changeDetection: ChangeDetectionStrategy.OnPush استفاده کنیم، رابط کاربری برنامه به روز رسانی نخواهد شد و این تغییر صرفا با بررسی عمیق تک تک خواص این شیء با وضعیت قبلی آن قابل تشخیص است (یا همان حالت پیش فرض بررسی تغییرات در Angular و نه حالت OnPush).
اگر علاقمند به استفادهی از یک کتابخانهی اضافی مانند Immutable.js در کدهای خود نباشید، روش دیگری نیز برای تغییر ارجاع به یک شیء وجود دارد:
const user = {
name: 'Max',
age: 30
}
const modifiedUser = { ...user, age: 31 }
در اینجا با استفاده از
spread properties یک شیء کاملا جدید ایجاد شدهاست و ارجاع به آن با ارجاع به شیء user یکی نیست.
نمونهی دیگر آن در حین کار با متد push یک آرایهاست:
export class AppComponent {
foods = ['Bacon', 'Lettuce', 'Tomatoes'];
addFood(food) {
this.foods.push(food);
}
}
متد push، بدون تغییر ارجاعی به آرایهی اصلی، عضوی را به آن آرایه اضافه میکند. بنابراین اعضای اضافه شدهی به آن نیز توسط استراتژی OnPush قابل تشخیص نیستند. اما اگر بجای متد push از
spread operator استفاده کنیم:
addFood(food) {
this.foods = [...this.foods, food];
}
اینبار this.food به یک شیء کاملا جدید اشاره میکند که ارجاع به آن، با ارجاع به شیء this.food قبلی یکی نیست. بنابراین استراتژی OnPush قابلیت تشخیص تغییرات آنرا دارد.
آگاه سازی دستی موتور تشخیص تغییرات Angular در حالت استفادهی از استراتژی OnPush
تا اینجا دریافتیم که استراتژی OnPush تنها به تغییرات ارجاعات به اشیاء پاسخ میدهد و به نحوی باید این ارجاع را با هر به روز رسانی تغییر داد. اما روش دیگری نیز برای وادار کردن این سیستم به تغییر وجود دارد:
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() data: string[];
constructor(private cd: ChangeDetectorRef) {}
refresh() {
this.cd.detectChanges();
}
}
در این کامپوننت از استراتژی OnPush استفاده شدهاست. در اینجا میتوان همانند قبل با اشیاء و آرایههای موجود کار کرد (بدون اینکه ارجاعات به آنها را تغییر دهیم و یا آنها را immutable کنیم) و در پایان کار، متد detectChanges سرویس ChangeDetectorRef را به صورت دستی فراخوانی کرد تا Angular کار رندر مجدد view این کامپوننت را بر اساس تغییرات آن انجام دهد (کل کامپوننت به عنوان یک کامپوننت تغییر کرده به سیستم ردیابی تغییرات معرفی میشود).
کار با Observables در حالت استفادهی از استراتژی OnPush
مطلب «
صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» را در نظر بگیرید. Observables نیز ما را از تغییرات رخداده آگاه میکنند؛ اما برخلاف immutable objects با هر تغییری که رخ میدهد، ارجاع به آنها تغییری نمیکند. آنها تنها رخدادی را به مشترکین، جهت اطلاع رسانی از تغییرات صادر میکنند.
بنابراین اگر از Observables و استراتژی OnPush استفاده کنیم، چون ارجاع به آنها تغییری نمیکند، رخدادهای صادر شدهی توسط آنها ردیابی نخواهند شد. برای رفع این مشکل، امکان علامتگذاری رخدادهای Observables به تغییر کرده پیشبینی شدهاست.
در اینجا کامپوننتی را داریم که قابلیت صدور رخدادها را از طریق یک BehaviorSubject دارد:
import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Component({ ... })
export class AppComponent {
foods = new BehaviorSubject(['Bacon', 'Letuce', 'Tomatoes']);
addFood(food) {
this.foods.next(food);
}
}
و کامپوننت دیگری توسط خاصیت ورودی data از نوع Observable در متد ngOnInit، مشترک آن خواهد شد:
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
@Input() data: Observable<any>;
foods: string[] = [];
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.data.subscribe(food => {
this.foods = [...this.foods, ...food];
});
}
در این حالت چون از ChangeDetectionStrategy.OnPush استفاده میشود و ارجاع به this.data این observable با هر بار صدور رخدادی توسط آن، تغییر نمیکند، سیستم ردیابی تغییرات آنرا به عنوان تغییر کرده درنظر نمیگیرد. برای رفع این مشکل تنها کافی است رخدادگردان آنرا با متد markForCheck علامتگذاری کنیم:
ngOnInit() {
this.data.subscribe(food => {
this.foods = [...this.foods, ...food];
this.cd.markForCheck(); // marks path
});
}
markForCheck به Angular اعلام میکند که این مسیر ویژه را در بررسی بعدی سیستم ردیابی تغییرات لحاظ کن.