این مقاله با توضیح درباره خطای ExpressionChangedAfterItHasBeenChecked شروع می شود و از آن برای بررسی دقیق مکانیسم change detection و جزئیات پیاده سازی داخلی آن استفاده می کند.
برنامه های کاربردی وب مدرن تعاملی هستند. وضعیت یک برنامه می تواند در هر زمان در نتیجه کلیک دکمه یا درخواستی از سرور تغییر کند. و همانطور که وضعیت تغییر می کند، کد باید آن را شناسایی کند و تغییر را در رابط کاربری منعکس کند. این کار اصلی مکانیزم change detection است .
در طول سال گذشته، من مقالات زیادی در مورد مکانیزم change detection در Angular نوشتم. آنها توضیحات مفصلی ارائه می دهند و بسیاری از جزئیات داخلی را پوشش می دهند. اما، آنها همچنین به زمان زیادی برای خواندن کامل نیاز دارند. برای کسانی از شما که وقت ندارید اما با این وجود کنجکاو هستید این مقاله توضیحی «سبکتر» از مکانیسم change detection ارائه میدهد. این یک دید کلی و سطح بالا از اجزای تشکیل دهنده و مکانیزم آن و ساختارهای داخلی که برای نمایش یک component استفاده شده است، قوانین binding ها و عملیات های انجام شده را توضیح میدهد. من همچنین به zone ها را اشاره میکنم و به شما نشان میدهم که دقیقاً چگونه این عملکرد، تشخیص خودکار تغییرات را در Angular فعال میکند.
وقتی همه چیز خراب می شود ، دانش داخلی change detection به شما کمک می کند تا خطاها را به طور موثرتر اشکال زدایی کنید ، مثل خطایExpressionChangedAfterItHasBeenCheckedError
و همچنین از برخی سردرگمی های رایج جلوگیری کنید . در این مقاله من چند اقدام را نشان میدهم که باعث ایجاد خطا میشوند و از آنها برای توضیح آنچه درون change detection اتفاق میفتد استفاده میکنم.
بیایید با این کامپوننت ساده Angular شروع کنیم. ساعت را در لحظه ای که change detection در برنامه اتفاق می افتد، ارائه می دهد. نشانگر ساعت دارای دقت میلی ثانیه است. با کلیک بر روی دکمه change detection فعال می شود:
در اینجا پیاده سازی آن آورده شده است :
@Component({ selector: 'my-app', template: ` <h3> Change detection is triggered at: <span [textContent]="time | date:'hh:mm:ss:SSS'"></span> </h3> <button (click)="0">Trigger Change Detection</button> ` }) export class AppComponent { get time() { return Date.now(); } }
همانطور که می بینید مثالی ساده است. دریافت کننده ای به نام time
وجود دارد که زمان فعلی را بر می گرداند و من آن را به عنصرspan
در HTML متصل می کنم.
در Angular اجازه استفاده از empty expressions داده نمی دهد، بنابراین من مقدار صفر را به عنوان مقدار بازگشتی به رویداد کلیک داده ام.
اینجا می توانید این قطعه کد را اجرا کنید . هنگامی که Angular فرآیند change detection را اجرا می کند، مقدار ویژگیtime
را می گیرد ، آن را از date pipe عبور می دهد و از نتیجه برای به روز رسانی DOM استفاده می کند. همه چیز همانطور که انتظار می رود کار می کند. با این حال، وقتی کنسول را بررسی می کنم، این ExpressionChangedAfterItHasBeenCheckedError
خطا را می بینم:
این در واقع بسیار تعجب آور است. معمولاً این خطا در پیاده سازی های بسیار پیچیده تر ظاهر می شود. بنابراین چگونه ممکن است که ما آن را با چنین عملکرد ساده ای دریافت کنیم؟ نگران نباشید، ما اکنون آن را بررسی می کنیم.
بیایید با پیام خطا شروع کنیم:
عبارت بعد از بررسی تغییر کرده است. مقدار قبلی: “textContent: 1542375826274”. مقدار فعلی: “textContent: 1542375826275”.
به ما می گوید که مقادیر تولید شده برای textContent
متفاوت است. بله، میلیثانیهها واقعاً متفاوت هستند. بنابراین Angular عبارت time | date: hh:mm:ss:SSS
را دو بار ارزیابی کرد و نتایج را مقایسه کرد. تفاوت را تشخیص داد و این همان چیزی است که باعث خطا شد.
اما چرا Angular این مقایسه را انجام می دهد؟ یا دقیقا چه زمانی این کار را انجام می دهد؟
اینها سوالاتی بود که کنجکاوی من را برانگیخت و در نهایت مرا به درون change detection کشاند. زیرا، برای یافتن پاسخ این سؤالات باید شروع به اشکال زدایی کنم. و من داشتم debugging می کردم و خب، فکر می کنم حدود ... چند ماه طول کشید. بیایید با سوال دوم شروع کنیم که خطا در چه زمانی اتفاق میفتد. اما ابتدا باید برخی از یافتههایم را با شما به اشتراک بگذارم که به ما در درک رفتاری که در بالا مشاهده کردیم کمک میکند.
دو بلوک اصلی برای change detection در Angular وجود دارد:
هر کامپوننت در Angular دارای یک template با عناصر HTML دارد. وقتی Angular گرههای DOM را برای نمایش محتوای template روی صفحه ایجاد میکند، به مکانی برای ذخیره ارجاعات به آن گرههای DOM نیاز دارد. برای این منظور، یک ساختار داده درونی به نام View وجود دارد. همچنین برای ذخیره ارجاع به نمونه های ساخته شده از component ها و مقادیر قبلی عبارات bind شده استفاده می شود. یک رابطه یک به یک بین یک component و یک view وجود دارد. در اینجا نموداری است که view را نشان می دهد:
همانطور که کامپایلر template را تجزیه و تحلیل می کند، property های عناصر DOM را شناسایی می کند که ممکن است نیاز به بروز رسانی در طول change detection داشته باشند. برای هر یک از این ویژگی ها، کامپایلر یک binding ایجاد می کند . Binding نام ویژگی برای به روز رسانی و عبارتی را که Angular برای بدست آوردن مقدار جدید استفاده می کند، تعریف می کند.
در این نمونه، ویژگیtime
برای خاصیت textContent
استفاده می شود . بنابراین Angular یک اتصال ایجاد می کند و آن را با عنصرspan
مرتبط می کند:
در پیاده سازی واقعی، یکviewDefinition
پیوندهایی را برای عناصر template و property ها برای روز رسانی تعریف می کند. عبارت مورد استفاده برای bind کردن در تابعupdateRenderer
قرار می گیرد.
همانطور که می دانید در Angular فرآیند change detection برای هر کامپوننت انجام می شود. اکنون که می دانیم کامپوننت ها در داخل به صورت viewهایی نمایش داده می شوند، می توان گفت که change detection برای هر view انجام می شود.
هنگامی که Angular یک view را بررسی می کند، بر روی تمام اتصالات ایجاد شده برای view توسط کامپایلر اجرا می شود. عبارات را ارزیابی می کند و نتیجه آنها را با مقادیر ذخیره شده در آرایهoldValues
روی view مقایسه می کند. نام dirty checking از همین جا می آید. اگر تفاوت را تشخیص دهد، ویژگی DOM مربوط به binding را به روز می کند. و همچنین باید مقدار جدید را در آرایهoldValues
در view قرار دهد. اکنون یک رابط کاربری به روز شده دارید. هنگامی که Angular بررسی component فعلی را انجام داد، دقیقاً همان مراحل را برای componentهای فرزند تکرار میکند.
در برنامه ما، فقط یک اتصال به ویژگی textContent عنصر span در کامپوننت App وجود دارد. بنابراین در حین change detection در برنامه، Angular مقدار time را می خواند،پایپ date را اعمال می کند و آن را با مقدار قبلی ذخیره شده در view مقایسه می کند. اگر تفاوتی را تشخیص دهد، Angular ویژگی textContent در span و آرایه oldValues را به روز می کند.
اما خطا از کجا می آید؟
پس از هر چرخه change detection، در حالت develop، انگولار به طور همزمان یک بررسی دیگر را اجرا میکند تا اطمینان حاصل کند که عبارات همان مقادیر را در طول اجرای change detection قبلی ایجاد میکنند. این بررسی بخشی از چرخه change detection اصلی نیست. پس از اتمام بررسی برای کل درخت کامپوننت ها اجرا می شود و دقیقاً همان مراحل را انجام می دهد. با این حال، این بار، همانطور که Angular تفاوت را تشخیص می دهد، DOM را به روز نمی کند. در عوض،خطای ExpressionChangedAfterItHasBeenCheckedError را برمیگرداند.
بنابراین اکنون می دانیم که خطا چه زمانی رخ داده است. اما چرا Angular به این بررسی نیاز دارد؟ خوب، تصور کنید که برخی از ویژگی های کامپوننت ها در طول اجرای change detection به روز شده اند. در نتیجه، عبارات مقادیر جدیدی تولید میکنند که با آنچه در رابط کاربری ارائه میشود ناسازگار است. بنابراین، Angular چه کاری انجام می دهد؟ مطمئناً می تواند چرخه change detection دیگری را برای همگام سازی وضعیت برنامه با رابط کاربری اجرا کند. اما اگر در طول این فرآیند برخی از ویژگی ها دوباره به روز شوند چه؟ الگو را ببینید؟ Angular در واقع می تواند در یک حلقه نامحدود از اجراهای change detection ختم شود. و در واقع، این اتفاق اغلب در AngularJS رخ می دهد .
برای جلوگیری از این وضعیت، Angular به اصطلاح جریان داده یک طرفه را اعمال کرد . و این بررسی که بعد از change detection اجرا می شود و ExpressionChangedAfterItHasBeenCheckedError
خطای حاصل از آن مکانیسم اجرایی است. هنگامی که Angular اتصالات را برای کامپوننت فعلی پردازش کرد، دیگر نمیتوانید property های کامپوننت را که در bindings استفاده میشود، بهروزرسانی کنید.
برای جلوگیری از خطا، باید اطمینان حاصل کنیم که مقادیر بازگردانده شده توسط عبارات در طول اجرای change detection و بررسی بعدی یکسان هستند. در این نمونه ی ما، میتوانیم این کار را با انتقال بخشtime
به خارج از getter انجام دهیم :
export class AppComponent { _time; get time() { return this._time; } constructor() { this._time = Date.now(); } }
با این حال، با این پیاده سازی مقدار دریافت کننده time
همیشه یکسان خواهد بود. ما هنوز باید مقدار را به روز کنیم. قبلاً یاد گرفتیم که آن بررسی که خطا را ایجاد میکند دقیقاً بعد از چرخه change detection بهطور synchronously اجرا میشود. بنابراین اگر آن را بهصورت asynchronously بهروزرسانی کنیم، از خطا جلوگیری میکنیم. بنابراین برای بهروزرسانی مقدار در هر میلیثانیه، میتوانیم از تابع setInterval
با تأخیر 1
میلیثانیه استفاده کنیم:
export class AppComponent { _time; get time() { return this._time; } constructor() { this._time = Date.now(); setInterval(() => { this._time = Date.now(); }, 1); } }
این پیاده سازی مشکل اصلی ما را حل می کند. اما متأسفانه یک مورد جدید را معرفی می کند. همه رویدادهای زمانبندی، مانند setInterval
،روند change detection را در Angular راهاندازی میکنند. این بدان معناست که با این پیادهسازی، در یک حلقه نامحدود از چرخههای change detection قرار میگیریم. برای جلوگیری از آن، ما به راهی برای اجرا setInterval
و عدم ایجاد change detection نیاز داریم. خوشبختانه برای ما، راهی برای انجام این کار وجود دارد. برای یادگیری نحوه انجام این کار، باید در وهله اول دلیل انجام change detection توسطsetInterval
در Angular را درک کنیم.
برخلاف React،فرآیند change detection در Angular میتواند به طور کاملاً خودکار در نتیجه هر رویداد غیرهمزمان (async event) در مرورگر فعال شود. این امر با استفاده از کتابخانه ای به نام zone.js
که مفهوم zoneها را معرفی می کند ممکن می شود. برخلاف تصور رایج، zoneها بخشی از مکانیسم change detection در Angular نیستند. در واقع، Angular می تواند بدون آنها کار کند . این کتابخانه به سادگی راهی برای رهگیری رویدادهای غیرهمزمان، مانند setInterval
، و اطلاع دادن به Angular در مورد آنها فراهم می کند. بر اساس آن اعلان، Angular روند change detection را اجرا می کند.
جالب اینجاست که شما می توانید zoneهای مختلفی را در یک صفحه وب داشته باشید. یکی از آنهاNgZone
است . هنگامی که Angular بوت استرپ می شود ایجاد می شود. این همان zone است که برنامه Angular در آن اجرا میشود. و Angular فقط اعلانهایی درباره رویدادهایی که در داخل این منطقه رخ میدهند دریافت میکند.
اما، zone.js
همچنین یک API برای اجرای برخی از کدها در zone
ای غیر از angular zone
ارائه می دهد. Angular از رویدادهای ناهمگام در مناطق دیگر مطلع نمی شود. و عدم اطلاع رسانی به معنای عدم انجام change detection است. نام متدی که برای انجام این کار فراخوانی می شود runOutsideAngular
و توسط سرویس NgZone
پیاده سازی می شود .
در اینجا تزریقNgZone
و اجرای setInterval
خارج از angularzone
را داریم :
export class AppComponent { _time; get time() { return this._time; } constructor(zone: NgZone) { this._time = Date.now(); zone.runOutsideAngular(() => { setInterval(() => { this._time = Date.now() }, 1); }); } }
اکنون ما دائماً زمان را به روز می کنیم، اما این کار را به صورت ناهمزمان و خارج از منطقه Angular انجام می دهیم. این تضمین می کند که در هنگام change detection و بررسی بعد از آن، time getter
همان مقدار را برمی گرداند. و هنگامی که Angular مقدارtime
را در چرخه change detection بعدی می خواند، مقدار به روز می شود و تغییرات بر روی صفحه نمایش نمایش داده می شود.
استفاده از NgZone برای اجرای برخی از کدها در خارج از Angular برای جلوگیری از شروع change detection یک تکنیک بهینه سازی رایج است.
شاید از خود بپرسید که آیا راهی برای دیدن این view و اتصالات داخل Angular وجود دارد یا خیر. در واقع وجود دارد. یک تابع به نامcheckAndUpdateView
داخل ماژول@angular/core
وجود دارد . روی هر view (component) در درخت کامپوننت ها اجرا می شود و بررسی هر view را انجام می دهد. این تابعی است که من همیشه وقتی با change detection مشکل دارم، اشکال زدایی را شروع می کنم.
سعی کنید این را برای خودتان دیباگ کنید. به این برنامه stackblitz بروید و کنسول را باز کنید. تابع را پیدا کنید و یک breakpoint در آنجا قرار دهید. روی دکمه کلیک کنید تا change detection فعال شود. متغیر view
را بررسی کنید. در اینجا یک تصویر از من در حال انجام این کار است:
اولین view
، view
میزبان است. این نوعی کامپوننت ریشه است که توسط Angular برای میزبانی app component ما ایجاد شده است. ما باید اجرا را از سر بگیریم تا به view فرزند آن برسیم که view ایجاد شده برایAppComponent
ما خواهد بود . آن را بررسی کنید . این ویژگیpropertycomponent
دارای ارجاع به نمونه ساخته شده از App
componentاست. ویژگی (property)هایnodes
ارجاع به گره های DOM ایجاد شده برای عناصر داخل template کامپوننتApp
دارند. آرایهoldValues
نتایج عبارات bindشده را ذخیره می کند.
ما به تازگی آموخته ایم که به دلیل محدودیت جریان داده یک طرفه، نمی توانید برخی از ویژگی های یک کامپوننت را در حین change detection پس از بررسی این مؤلفه تغییر دهید. این بهروزرسانی از طریق یک سرویس مشترک یا پخش رویداد همزمان ( asynchronous event broadcasting ) هنگام اجرای change detection برای کامپوننت های فرزند انجام میشود. اما همچنین میتوان مستقیماً یک کامپوننت والد را به کامپوننت فرزند تزریق کرد و وضعیت والد را در یک lifecycle hook بهروزرسانی کرد. در اینجا کدی وجود دارد که این را نشان می دهد:
@Component({ selector: 'my-app', template: ` <div [textContent]="text"></div> <child-comp></child-comp> ` }) export class AppComponent { text = 'Original text in parent component'; } @Component({ selector: 'child-comp', template: `<span>I am child component</span>` }) export class ChildComponent { constructor(private parent: AppComponent) {} ngAfterViewChecked() { this.parent.text = 'Updated text in parent component'; } }
اینجا می توانید آن را امتحان کنید . اساسا، ما یک سلسله مراتب ساده از دو کامپوننت را تعریف می کنیم. کامپوننت والد خاصیت text
مورد استفاده در binding را اعلام می کند. کامپوننت فرزند کامپوننت والد را به سازنده تزریق می کند و property آن را در ngAfterViewChecked
به روز می کند. آیا می توانید حدس بزنید که قرار است در کنسول چه چیزی ببینیم؟ ?
درست است، خطای آشنای ExpressionChangedAfterItWasChecked
. و این به این دلیل است که وقتی Angular چرخه حیاتngAfterViewChecked
را برای کامپوننت فرزند فراخوانی میکند، قبلاً اتصال کامپوننت App
والد را بررسی کرده است. اما ما در حال به روز رسانی ویژگی text در والد که در binding استفاده
شده است هستیم.
اما قسمت جالب اینجاست. اگر الان hook را عوض کنم چه؟ مثلا ، به ngOnInit
. آیا فکر می کنید ما هنوز هم خطا را مشاهده خواهیم کرد؟
export class ChildComponent { constructor(private parent: AppComponent) {} ngOnInit() { this.parent.text = 'Updated text in parent component'; } }
خب، این بار خطا آنجا نیست. این دمو را اجرا و بررسی کنید . در واقع ما می توانیم کد را در هر hook دیگری (به جزAfterViewInit
وAfterViewChecked
) قرار دهیم و خطا را در کنسول مشاهده نخواهیم کرد. پس اینجا چه خبر است؟ چراngAfterViewChecked یک hook
خاص است؟
برای درک این رفتار، باید بدانیم که Angular چه عملیاتی را در حین change detection انجام می دهد و ترتیب آنها را انجام می دهد. و، ما قبلاً می دانیم که کجا می توانیم آنها را پیدا کنیم: تابع checkAndUpdateView
که قبلاً به شما نشان دادم. در اینجا بخشی از کدی است که می توانید در بدنه تابع پیدا کنید:
function checkAndUpdateView(view, ...) { ... // update input bindings on child views (components) & directives, // call NgOnInit, NgDoCheck and ngOnChanges hooks if needed Services.updateDirectives(view, CheckType.CheckAndUpdate); // DOM updates, perform rendering for the current view (component) Services.updateRenderer(view, CheckType.CheckAndUpdate); // run change detection on child views (components) execComponentViewsAction(view, ViewAction.CheckAndUpdate); // call AfterViewChecked and AfterViewInit hooks callLifecycleHooksChildrenFirst(…, NodeFlags.AfterViewChecked…); ... }
همانطور که می بینید، Angular همچنین hook های چرخه حیات را به عنوان بخشی از change detection فعال می کند. نکته جالب این است که برخی از هوک ها قبل از قسمت رندر زمانی که Angular پردازش های binding را انجام می دهد و برخی بعد از آن فراخوانی می شوند. در اینجا نموداری وجود دارد که نشان میدهد هنگام اجرای change detection در Angular برای کامپوننت والد چه اتفاقی میافتد:
بیایید قدم به قدم آن را مرور کنیم. ابتدا، اتصالات ورودی برای کامپوننت فرزند را به روز می کند . سپس دوباره , هوک های DoCheck, OnInit
و OnChanges
را بر روی کامپوننت فرزند فراخوانی می کند . منطقی است زیرا فقط اتصالات ورودی را به روز کرده است و Angular باید به کامپوننت های فرزند اطلاع دهد که اتصالات ورودی initial شده اند. سپس Angular رندر کامپوننت فعلی را انجام می دهد. و پس از آن، change detection را برای کامپوننت فرزند اجرا می کند. این بدان معنی است که اساساً این عملیات را در view فرزند تکرار می کند. و در نهایت، کامپوننت فرزندAfterViewInit و AfterViewChecked را
فراخوانی می کند تا به آن اطلاع دهد که بررسی شده است.
چیزی که در اینجا می توانیم متوجه شویم این است که Angular پس از پردازش اتصالات کامپوننت والد، هوک AfterViewChecked
را برای کامپوننت فرزند فراخوانی می کند . از طرف دیگر، چرخه OnInit
قبل از پردازش binding ها فراخوانی می شود . بنابراین حتی اگر تغییری در مقدارtext در OnInit
وجود داشته باشد ، باز هم در بررسی زیر یکسان خواهد بود. و این رفتار به ظاهر عجیب و غریب نداشتن خطا با هوک ngOnInit
را توضیح می دهد .
خوب، بیایید اکنون آنچه را که یاد گرفتیم خلاصه کنیم. تمام کامپوننت ها در Angular به صورت داخلی در یک ساختار داده به نام view نمایش داده میشوند. کامپایلر Angular یک template را تجزیه می کند و bindingها را ایجاد می کند. هر binding یک property از یک عنصر DOM را برای به روز رسانی و عبارتی مورد استفاده برای به دست آوردن مقدار تعریف می کند. مقادیر قبلی که برای مقایسه در حین change detection استفاده میشوند، روی یک view در آرایهoldValues
ذخیره میشوند. در حین change detection در برنامه، Angular روی bindingها اجرا می شود، عبارات را ارزیابی می کند، آنها را با مقادیر قبلی مقایسه می کند و در صورت لزوم DOM را به روز می کند. پس از هر چرخه change detection در برنامه، Angular بررسی میکند تا مطمئن شود که وضعیت کامپوننت با رابط کاربری همگام است یا خیر. این بررسی به صورت همزمان انجام می شود و ممکن است خطای ExpressionChangedAfterItWasChecked را
ایجاد کند.
این مقاله برگردان شده یک مقاله معتبر درباره این موضوع است.برای دسترسی به مقاله منبع اینجا کلیک کنید.
برای مشاهده پست های بیشتر و ارتباط با من از طریق لینکدین اینجا کلیک کنید.
امیدوارم براتون مفید واقع شده باشه.