אנגולר: המדריך המקיף
למחזור Change Detection

מדריך אנגולר

אנגולר: המדריך המקיף למחזור Change Detection

כברירת מחדל, שפת התכנות אנגולר משתמשת בשיטת Change Detection של ChangeDetectionStrategy.Default.

שיטת ברירת המחדל אינה מניחה דבר על היישום, ולכן בכל פעם בה משהו משתנה ביישום, כתוצאה מהתנהלות שונה של המשתמשים,

טיימרים XHR, או הבטחות, זיהוי שינוי יפעל על כל הרכיבים. המשמעות היא שכל הקלקה על נתונים שהתקבלו מקריאת AJAX, מובילה להופעת זיהוי השינוי.

לדוגמא :

@Component({

  template: `

    <h1>Hello {{name}}!</h1>

    {{runChangeDetection}}

})

export class HelloComponent {

  @Input() name: string;

 

  get runChangeDetection() {

    console.log('Checking the view');

    return true;

  }

}

 

@Component({
  template: `
    <hello></hello>
    <button (click)="onClick()">Trigger change detection</button>
  `
})
export class AppComponent  {
  onClick() {}
}
 

 

לאחר הפעלת הקוד, כל לחיצה על הלחצן, יגרום לאנגולר להפעיל מחזור זיהוי שינויים, והתוצאה תהיה שני יומנים של "בדיקת התצוגה במסוף (או יומן רישום אחד). שיטה זו נקראת בדיקה מלוכלכת. כדי לדעת אם יש להציג את התצוגה, אנגולר צריכה לגשת לערך החדש, להשוות אותו עם הישן, ולקבל את ההחלטה האם יש להציג את התצוגה.

עכשיו, דמיינו יישום גדול עם אלפי ביטויים. אם נאפשר לאנגולר לבדוק כל אחד מהם כשמחזור זיהוי השינוי פועל, אנו עשויים להיתקל בבעיה בביצועים. למרות שאנגולר היא תכנה מהירה מאוד, ככל שהאפליקציה שלכם גדלה, התוכנה תאלץ לעבוד קשה יותר על מנת לעקוב אחר כל השינויים.

מה נוכל לעשות כדי לעזור לאנגולר ולהעניק לה אינדיקציה טובה יותר לגבי המועד בו ניתן לבדוק את הרכיבים שלנו?

שינוי אסטרטגיית איתור השינוי

ניתן להגדיר את ChangeDetectionStrategy של הרכיב ל- ChangeDetectionStrategy.OnPus. פירוש הדבר הוא שהרכיב של אנגולר תלוי רק ב-@inputs() (כלומר, בערכים  הטהורים שלה, ויש לבדוק רק במקרים אלו ):

הפניה לקלט משתנה

באמצעות הגדרת אסטרטגיית זיהוי השינוי של onush, אנו חותמים על חוזה עם אנגולר, המחייב אותנו לעבוד עם אובייקטים בלתי ניתנים לשינוי (או נצפים כפי שנראה בהמשך).

יתרון העבודה עם השתנות בהקשר זיהוי שינוי הוא שאנגולר יכולה לבצע בדיקת התייחסות פשוטה כדי לדעת אם יש לבדוק תצוגה. בדיקות כאלו הן בדרך כלל זולות יותר מאשר בדיקת השוואה עמוקה.

בואו ננסה :

@Component({
  selector: 'tooltip',
  template: `
    <h1>{{config.position}}</h1>
    {{runChangeDetection}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent  {
 
  @Input() config;
 
  get runChangeDetection() {
    console.log('Checking the view');
    return true;
  }
}
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: 'top'
  };
 
  onClick() {
    this.config.position = 'bottom';
  }
}

בלחיצה לא נראה את ה Log  . הסיבה לכך היא כי Angular משווה את הערך הישן עם הערך החדש על ידי הפניה, משהו כמו:

/** Returns false in our case */
if( oldValue !== newValue ) {
  runChangeDetection();
}
 

נזכיר רק שמספרים, בוליאנים, מחרוזות, strings, null ובלתי מוגדרים הם סוגים פרימיטיביים, וכולם מועברים לפי ערך. אובייקטים, מערכים ופונקציות מועברים גם הם לפי ערך, אך הערך הוא עותק של הפניה. אז על מנת להפעיל זיהוי שינוי ברכיב שלנו, עלינו לשנות את ההתייחסות לאובייקט.

@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: 'top'
  };
 
  onClick() {
    this.config = {
      position: 'bottom'
    }
  }
}
 

אירוע שמקורו ברכיב או באחד מילדיו

רכיב יכול להיות מצב פנימי המתעדכן כשמופעל אירוע מהרכיב או מאחד מילדיו.

לדוגמא :

@Component({
  template: `
    <button (click)="add()">Add</button>
    {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;
 
  add() {
    this.count++;
  }
 
}

כשאנו לוחצים על הלחצן, אנגולר מפעילה מחזור זיהוי שינוי והתצוגה מתעדכנת כצפוי.

אתם עשויים לחשוב שזה צריך לעבוד עם כל API אסינכרוני שמפעיל זיהוי שינוי, כפי שלמדנו בהתחלה, אך לא כך המצב. מסתבר שהכלל חל רק על אירועי DOM,

ולכן ממשקי ה-API הבאים לא יפעלו.

@Component({
  template: `…`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;
 
  constructor() {
    setTimeout(() => this.count = 5, 0);
 
    setInterval(() => this.count = 5, 100);
 
    Promise.resolve().then(() => this.count = 5);
    
    this.http.get('https://count.com').subscribe(res => {
      this.count = res;
    });
  }
 
  add() {
    this.count++;
  }
 
}
 

הפעלת זיהוי שינוי מפורש

אנגולר מספקת שלוש שיטות להפעלת זיהוי השינוי בעצמנו בעת הצורך. השיטה הראשונה היא detectChanges () שפירושה שאנגולר מפעילה זיהוי שינוי על הרכיב וילדיו.

 

@Component({
  selector: 'counter',
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;
 
  constructor(private cdr: ChangeDetectorRef) {
 
    setTimeout(() => {
      this.count = 5;
      this.cdr.detectChanges();
    }, 1000);
 
  }
 
}
 

השנייה היא ApplicationRef.tick, אשר מורה לאנגולר להריץ זיהוי שינוי עבור היישום כולו.

 

tick() {

  try {
    this._views.forEach((view) => view.detectChanges());
    …
  } catch (e) {
    …
  }
}
 

השלישית היא MarkForCheck (), שאינו גורם לזיהוי שינוי, אלא מסמן את כל אבות onPush כפי שנבדקו פעם אחת, או כחלק מהמחזור הנוכחי או השינוי הבא.

markForCheck(): void {
  markParentViewsForCheck(this._view);
}
 
export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}
 

Async Pipe

ה  async pipe אמון על הבטחה נצפית או שהוא מחזיר את הערך העדכני ביותר שנפלט.

@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-list [items$]="items$"></app-list>
  `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);
 
  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
 

@Component({
  template: `
     <div *ngFor="let item of items">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items: Observable<Item>;
  _items: Item[];
 
  ngOnInit() {
    this.items.subscribe(items => {
      this._items = items;
    });
  }
 
}
 

כשלוחצים על הכפתור, לא ניתן לראות את התצוגה מעודכנת. הסיבה לכך היא שאף אחד מהתנאים שהוזכרו לעיל לא התרחש, כך שאנגולר לא בודקת את הרכיב על מחזור זיהוי שינוי הנוכחי.

בואו נבצע את השינוי :

 

@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}
 

כעת ניתן לראות שהתצוגה מתעדכנת כשלוחצים על הכפתור. הסיבה לכך היא שכשערך חדש נפלט, הצינור הסינכרוני מסמן את הרכיב המיועד לבדיקת שינויים.

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}
 

אנגולר קוראת ל- markForCheck () עבורנו ולכן התצוגה מתעדכנת למרות שההתייחסות לא השתנתה.

טיפ: זהו Antu Patterns לחשיפת הנושא שלכם לעולם החיצון. הקפידו תמיד לחשוף את הנצפה, באמצעות שיטת asObservable ().

 

 

לקבלת מאמרים נוספים , ניוזלטר , הזמנות לוובינר ועוד …. הצטרף לרשימת התפוצה שלנו  : 

 

[caldera_form id="CF5b73b828e0f72"]

 

העתיד שלך בהייטק מתחיל כאן
צור איתי קשר עוד היום

דילוג לתוכן