فریم ورک Angular دو استراتژی را پیاده سازی می کند که رفتار change detection را در سطح کامپوننت ها کنترل می کند. این استراتژی ها به صورت زیر تعریف می شوند :Default - OnPush
Export enum ChangeDetectionStrategy { OnPush = 0 , Default = 1 }
فریم ورک Angular از این استراتژیها برای تعیین اینکه آیا کامپوننت فرزند باید هنگام اجرای change detection برای کامپوننت والد بررسی شود یا خیر، استفاده میکند . استراتژی تعریف شده برای یک کامپوننت بر همه directiveهای فرزند تأثیر می گذارد زیرا آنها به عنوان بخشی از host component بررسی می شوند. یک استراتژی تعریف شده را نمی توان در زمان اجرا لغو کرد.
استراتژی default، که بعنوانCheckAlways
به آن اشاره میشود ، change detection خودکار برای یک کامپوننت است، مگر اینکه view مشخصا جدا شده باشد. آنچه به عنوانOnPush
استراتژی شناخته می شود، به این معنی است که اگر یک کامپوننت به عنوان dirty علامت گذاری شود، change detection برای آن اجرا می شود . Angular مکانیسم هایی را برای علامت گذاری خودکار یک کامپوننت به عنوان dirty پیاده سازی می کند. در صورت نیاز، یک کامپوننت را می توان به صورت دستی با استفاده از متد markForCheck که در ChangeDetectorRef
قرار دارد ، علامت گذاری کرد .
وقتی که این استراتژی را با استفاده از دکوراتور ()Component@
تعریف می کنیم، کامپایلر Angular آن را در تعریف یک کامپوننت از طریق تابع defineComponent ثبت می کند . به عنوان مثال، برای کامپوننتی مانند زیر:
@Component({ selector: 'a-op', template: `I am OnPush component`, changeDetection: ChangeDetectionStrategy.OnPush }) export class AOpComponent {}
کد تولید شده توسط کامپایلر به صورت زیر است:
وقتی Angular یک کامپوننت را نمونهسازی میکند، از این تعریف برای تنظیم یک flag بر روی نمونه ساخته شده ازLView
استفاده میکند که view کامپوننت را نشان میدهد:
به این معنی است که همه نمونههایLView
ایجاد شده برای این کامپوننت دارای یکی از دو پرچم CheckAlways یا Dirty هستند. برای استراتژیOnPush
، پرچمDirty
به طور خودکار پس از اولین مرحله change detection تنظیم نمی شود.
هنگامی که Angular تعیین می کند که آیا یک کامپوننت باید بررسی شود ، پرچم های تنظیم شده رویLView
در داخل تابع refreshView بررسی می شوند :
function refreshComponent(hostLView, componentHostIdx) { // Only attached components that are CheckAlways or OnPush and dirty // should be refreshed if (viewAttachedToChangeDetector(componentView)) { const tView = componentView[TVIEW]; if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { refreshView(tView, componentView, tView.template, componentView[CONTEXT]); } else if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) { // Only attached components that are CheckAlways // or OnPush and dirty should be refreshed refreshContainsDirtyView(componentView); } } }
بیایید اکنون این استراتژی ها را با جزئیات بیشتر بررسی کنیم.
فرایند change detection در استراتژی default زمانی اجرا میشود که:
استراتژی default change detection به این معنی است که اگر کامپوننت والد بررسی شده باشد، کامپوننت فرزند همیشه بررسی میشود . تنها استثنای این قانون این است که change detector کامپوننت فرزند را مانند زیر جدا کنید:
@Component({ selector: 'a-op', template: `I am OnPush component` }) export class AOpComponent { constructor(private cdRef: ChangeDetectorRef) { cdRef.detach(); } }
توجه داشته باشید که اگر کامپوننت والد بررسی نشده باشد، انگولار change detection را برای کامپوننت فرزند اجرا نمیکند، حتی اگر از استراتژی default change detection استفاده کند. این از این واقعیت ناشی می شود که Angular این مکانیسم را برای کامپوننت فرزند به عنوان بخشی از بررسی والد آن اجرا می کند .
فریم ورک Angular هیچ workflow را بر روی دولوپرها اعمال نمی کند تا تشخیص دهد که چه زمانی وضعیت یک کامپوننت تغییر می کند، به همین دلیل رفتار پیش فرض این است که همیشه کامپوننت ها را بررسی کند. یک مثال از workflow ، انتقال یک شی تغییرناپذیر از طریقInput@
است. این چیزی است که برای استراتژیOnPush
استفاده می شود و در ادامه آن را بررسی خواهیم کرد.
در اینجا ما یک سلسله مراتب ساده از دو کامپوننت داریم:
@Component({ selector: 'a-op', template: ` <button (click)="changeName()">Change name</button> <b-op [user]="user"></b-op> `, }) export class AOpComponent { user = { name: 'A' }; changeName() { this.user.name = 'B'; } } @Component({ selector: 'b-op', template: `<span>User name: {{user.name}}</span>`, }) export class BOpComponent { @Input() user; }
وقتی روی دکمه کلیک می کنیم، Angular یک event handler را اجرا می کند که در آن ما user.name
را بروز رسانی می کنیم . به عنوان بخشی از اجرای حلقه change detection بعدی، کامپوننتB
فرزند بررسی می شود و صفحه به روز می شود:
در حالی که ارجاع به شیuser
تغییر نکرده است، در داخل آن تغییراتی داشته است، اما همچنان میتوانیم نام جدید را روی صفحه نمایش ببینیم. به همین دلیل است که رفتار پیش فرض این است که همه کامپوننت ها را بررسی کند. بدون محدودیت object immutability در Angular، نمیتوان متوجه تغییر ورودیها و بهروزرسانی وضعیت کامپوننت شد.
فرایند change detection در استراتژی default زمانی اجرا میشود که:
در حالی که Angular محدودیت object immutability را به ما تحمیل نمی کند، اما مکانیزمی به ما می دهد تا یک کامپوننت را دارای ورودی های تغییرناپذیر اعلام کنیم تا تعداد دفعات بررسی کامپوننت را کاهش دهیم. این مکانیزم استراتژی OnPush
change detectionاست و یک تکنیک بهینه سازی بسیار رایج است. در درون کدها این استراتژیCheckOnce
نامیده می شود ، زیرا به این معنی است که از اجرای change detection برای یک کامپوننت صرفنظر می شود تا زمانی که به عنوان dirty علامت گذاری شود، سپس یک بار بررسی می شود و سپس دوباره رد می شود. یک کامپوننت را می توان به صورت خودکار یا دستی با استفاده از متدmarkForCheck
علامت گذاری dirty کرد.
بیایید از مثال بالا استفاده کنیم و استراتژی OnPush
change detectionرا برای کامپوننتB
اعلام کنیم:
@Component({...}) export class AOpComponent { user = { name: 'A' }; changeName() { this.user.name = 'B'; } } @Component({ selector: 'b-op', template: ` <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BOpComponent { @Input() user; }
وقتی برنامه را اجرا می کنیم Angular دیگر تغییری در user.name
ایجاد نمی کند :
میتوانید ببینید که کامپوننتB
هنوز یک بار در طول بوت استرپ بررسی میشود - مقدار اولیهA
را ارائه میکند . اما در طول اجراهای بعدی change detection بررسی نمیشود، بنابراین با کلیک روی دکمه، تغییر نام از A
به بهB
را مشاهده نمیکنید. این به این دلیل اتفاق میافتد که ارجاع به شیuser
که از طریقInput@
به کامپوننتB
منتقل میشود، تغییر نکرده است.
قبل از اینکه نگاهی به راههای مختلف علامتگذاری یک کامپوننت به عنوان dirty بیندازیم، در اینجا فهرستی از سناریوهای مختلفی وجود دارد که Angular برای تست رفتار OnPush
استفاده میکند:
باید کامپوننت های OnPush را در هنگام بهروزرسانی وقتی
dirtyنیستند، نادیده گرفت.
نباید کامپوننت های OnPush را در هنگام بهروزرسانی وقتی که رویدادهای والد رخ میدهند ، بررسی کنید.
باید کامپوننت های OnPush را در هنگام مقداردهی اولیه بررسی کنید.
باید doCheck را فراخوانی کنید، حتی زمانی که
کامپوننتهای OnPush ما
dirtyنیستند.
باید کامپوننت های OnPush را موقع بهروزرسانی بررسی کنید وقتی inputها تغییر میکنند.
باید کامپوننت های OnPush را موقع به روز رسانی بررسی کنید هنگامی که رویدادهای
کامپوننترخ می دهد.
باید کامپوننت های OnPush را موقع بروز رسانی بررسی کنید هنگامی که رویدادهای فرزند رخ می دهد.
باید کامپوننت های OnPush والد را بررسی کنید هنگامی که یک دایرکتیو فرزند در یک template یک ایونت را منتشر می کند.
آخرین دسته از سناریوهای تستی نشان میدهد که فرآیند خودکار علامتگذاری یک کامپوننت dirty در سناریوهای زیر رخ میدهد:
Input@
تغییر می کند.حال بیایید اینها را بررسی کنیم.
در بیشتر موقعیتها، تنها زمانی که ورودیهای آن کامپوننت تغییر میکند، باید کامپوننت فرزند را بررسی کنیم. این امر مخصوصاً در مورد کامپوننت های pure presentational که ورودی آنها صرفاً از طریق اتصالات میآید صادق است.
بیایید مثال قبلی را در نظر بگیریم:
@Component({...}) export class AOpComponent { user = { name: 'A' }; changeName() { this.user.name = 'B'; } } @Component({ selector: 'b-op', template: ` <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BOpComponent { @Input() user; }
همانطور که در بالا دیدیم، وقتی روی دکمه کلیک می کنیم و نام را تغییر می دهیم، نام جدید روی صفحه به روز نمی شود. دلیل آن این است که Angular مقایسه سطحی را برای پارامترهای ورودی انجام می دهد و ارجاع به شیuser
تغییر نکرده است. تغییر مستقیم یک خاصیت شی منجر به ایجاد یک مرجع جدید نمی شود و به طور خودکار کامپوننت را علامت گذاری dirty نمی کند.
ما باید مرجع شیuser
را برای Angular تغییر دهیم تا تفاوتInput@
را تشخیص دهد.اگر به جای تغییر در نمونه موجود، یک نمونه جدید ایجاد کنیم ، همه چیز همانطور که انتظار می رود کار خواهد کرد:
@Component({...}) export class AOpComponent { user = { name: 'A' }; changeName() { this.user = { ...this.user, name: 'B', } } }
بله، همه چیز خوب است:
به دلیل اینکه با تغییر خاصیت شی این کامپوننت بررسی نمی شود، با اجرای Object.freeze می توانید به راحتی تغییر ناپذیری را بر روی اشیاء اعمال کنید :
export function deepFreeze(object) { const propNames = Object.getOwnPropertyNames(object); for (const name of propNames) { const value = object[name]; if (value && typeof value === 'object') { deepFreeze(value); } } return Object.freeze(object); }
به طوری که وقتی کسی سعی می کند شی را تغییر دهد، این خطا را می دهد:
احتمالاً بهترین روش استفاده از یک کتابخانه تخصصی مانند immer است :
import { produce } from 'immer'; @Component({...}) export class AOpComponent { user = { name: 'A' }; changeName() { this.user = produce(this.user, (draft) => { draft.name = 'B'; }); } }
این نیز به خوبی کار خواهد کرد.
همه رویدادهای native، هنگامی که بر روی کامپوننت فعلی اجرا می شوند، همه اجداد کامپوننت تا کامپوننت ریشه را dirty می کنند. پیشفرض بر این است که یک رویداد می تواند باعث تغییر در درخت کامپوننت ها شود. Angular نمی داند که آیا والدین تغییر خواهند کرده اند. به همین دلیل است که Angular همیشه هر کامپوننت اجدادی را پس از اجرا شدن یک رویداد بررسی می کند.
تصور کنید که یک سلسله مراتب درخت کامپوننت ها مانند کامپوننت هایOnPush
دارید :
AppComponent HeaderComponent ContentComponent TodoListComponent TodoComponent
اگر یک event listener را در داخل TodoComponent
templateضمیمه کنیم :
@Component({ selector: 'todo', template: ` <button (click)="edit()">Edit todo</button> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class TodoComponent { edit() {} }
فریم ورک Angular قبل از اجرای event handler، همه کامپوننت های اجداد این کامپوننت را dirty می کند:
از این رو سلسله مراتب کامپوننت ها به صورت زیر می شود :
Root Component -> LViewFlags.Dirty | ... | ContentComponent -> LViewFlags.Dirty | | TodoListComponent -> LViewFlags.Dirty | | TodoComponent (event triggered here) -> markViewDirty() -> LViewFlags.Dirty
در طول چرخه change detection بعدی، Angular کل درخت کامپوننت های اجدادیTodoComponent
را بررسی می کند:
AppComponent (checked) HeaderComponent ContentComponent (checked) TodosComponent (checked) TodoComponent (checked)
توجه داشته باشید که HeaderComponent
به دلیل اینکه از اجداد TodoComponent
نیست بررسی نشده است .
بیایید به مثالی برگردیم که در آن هنگام بهروزرسانی نام، ارجاع به شیuser
را تغییر دادیم. این کار به Angular امکان میدهد تا تغییرات را دریافت کرده و بهطور خودکار کامپوننتB
را به عنوان dirty علامتگذاری کند. فرض کنید می خواهیم نام را به روز کنیم اما نمی خواهیم مرجع را تغییر دهیم. در این صورت، می توانیم به صورت دستی کامپوننت را به عنوان dirty علامت گذاری کنیم.
برای این کار می توانیم changeDetectorRefرا
تزریق کرده و از متدmarkForCheck
آن استفاده کنیم تا برای Angular نشان دهیم که این کامپوننت باید بررسی شود:
@Component({...}) export class BOpComponent { @Input() user; constructor(private cd: ChangeDetectorRef) {} someMethodWhichDetectsAndUpdate() { this.cd.markForCheck(); } }
کجا می توانیم از someMethodWhichDetectsAndUpdate
در کد بالا استفاده کنیم ؟ هوک NgDoCheck
کاندیدای بسیار خوبی است.این هوک قبل از اینکه Angular مکانیزم change detection را برای این کامپوننت اجرا کند و در حین بررسی کامپوننت والد اجرا می شود. اینجا جایی است که کد مقایسه مقادیر و همینطور علامت گذاری دستی کامپوننت به عنوان dirty هنگام change detection را قرار می دهیم.
اجرای این کار درNgDoCheck
حتی اگر یک کامپوننت OnPush
باشد اغلب باعث سردرگمی می شود. اما این عمدی است و اگر بدانید که به عنوان بخشی از بررسی کامپوننت والد اجرا می شود، تناقضی وجود ندارد. به خاطر داشته باشید که ngDoCheck
فقط برای بالاترین کامپوننت فرزند فعال می شود. اگر کامپوننت دارای تعدادی فرزند باشد و Angular این کامپوننت را بررسی نکند، ngDoCheck
برای آنها فعال نمی شود.
ازngDoCheck
برای log کردن مراحل بررسی کامپوننت استفاده نکنید . درعوض، از تابع Accessor در داخل template مانند این استفاده کنید{{ ()logCheck }}
.
بنابراین بیایید منطق مقایسه خود را در داخل NgDoCheck
معرفی کنیم و وقتی تغییر را تشخیص دادیم، کامپوننت را dirty علامت گذاری کنیم:
@Component({...}) export class AOpComponent {...} @Component({ selector: 'b-op', template: ` <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BOpComponent { @Input() user; previousUserName = ''; constructor(private cd: ChangeDetectorRef) {} ngDoCheck() { if (this.user.name !== this.previousUserName) { this.cd.markForCheck(); this.previousUserName = this.user.name; } } }
به یاد داشته باشید کهmarkForCheck
تضمینی برای اجرای change detection نیست. برای جزئیات بیشتر به بخش manual control مراجعه کنید.
حالا بیایید مثال خود را کمی پیچیده تر کنیم. بیایید فرض کنیم کامپوننت فرزندB
ما بر اساس RxJ قابل مشاهده است که بهروزرسانیها را به صورت ناهمزمان منتشر میکند. این مشابه چیزی است که ممکن است در معماری مبتنی بر NgRx داشته باشید:
@Component({ selector: 'a-op', template: ` <button (click)="changeName()">Change name</button> <b-op [user$]="user$.asObservable()"></b-op> `, }) export class AOpComponent { user$ = new BehaviorSubject({ name: 'A' }); changeName() { const user = this.user$.getValue(); this.user$.next( produce(user, (draft) => { draft.name = 'B'; }) ); } }
بنابراین ما این جریان از شیuser
را در کامپوننت فرزندB
دریافت می کنیم . ما باید subscribe کنیم، بررسی کنیم که آیا مقدار به روز شده است یا خیر و در صورت نیاز کامپوننت را به عنوان dirty علامت گذاری کنیم:
@Component({ selector: 'b-op', template: ` <span>User name: {{user.name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BOpComponent { @Input() user$; user = null; constructor(private cd: ChangeDetectorRef) {} ngOnChanges() { this.user$.subscribe((user) => { if (user !== this.user) { this.cd.markForCheck(); this.user = user; } }); } }
منطق داخل ngOnChanges
تقریباً همان کاری است که async pipe انجام می دهد :
export class AsyncPipe { transform() { if (obj) { this._subscribe(obj); } } private _updateLatestValue(async, value) { if (async === this._obj) { this._latestValue = value; this._ref!.markForCheck(); } } }
به همین دلیل است که رویکرد متداول این است که منطق subscription و مقایسه را به async pipe واگذار کنیم . تنها محدودیت این است که اشیا باید تغییر ناپذیر باشند.
در اینجا اجرای کامپوننتB
که از async pipe استفاده می کند، آمده است:
@Component({ selector: 'b-op', template: ` <span>User name: {{(user$ | async).name}}</span> `, changeDetection: ChangeDetectionStrategy.OnPush }) export class BOpComponent { @Input() user$; }
مجموعه ای از test casesها وجود دارد که async pipe و تعامل آن با انواع مختلف را آزمایش می کنند:
describe('Observable', () => { describe('transform', () => { it('should return null when subscribing to an observable'); it('should return the latest available value'); it('should return same value when nothing has changed since the last call'); it('should dispose of the existing subscription when subscribing to a new observable'); it('should request a change detection check upon receiving a new value'); it('should return value for unchanged NaN'); }); }); describe('Promise', () => {...}); describe('null', () => {...}); describe('undefined', () => {...}); describe('other types', () => {...});
این تست برای موردی است که در اینجا بررسی کردیم:
it('should request a change detection check upon receiving a new value', done => { pipe.transform(subscribable); emitter.emit(message); setTimeout(() => { expect(ref.markForCheck).toHaveBeenCalled(); done(); }, 10); });
در اینجا pipe به observable درون transform
متصل شده است و هنگامی که observable یک message
جدید منتشر می کند ، کامپوننت را به عنوان dirty علامت گذاری می کند.
این مقاله برگردان شده یک مقاله معتبر درباره این موضوع است.برای دسترسی به مقاله منبع اینجا کلیک کنید.
برای مشاهده پست های بیشتر و ارتباط با من از طریق لینکدین اینجا کلیک کنید.
امیدوارم براتون مفید واقع شده باشه.