Scope.$apply Life Cycle
בפוסט זה אני רוצה להסביר את סדר האירועים כאשר מבצעים $apply. בנוסף אני אעמוד על ההבדלים בין $evalAsync, $$postDigest ו- $timeout. חשוב לדעת את ההבדלים בין הפונקציות בעיקר כאשר בונים directives שמשתמשים הרבה ב- jQuery. בפוסט זה אני לא מסביר מה $apply עושה, כי על זה יש פוסט מעולה.
1. מתי קוראים ל- $apply?
כאשר נורה האירוע DOMContentLoaded אנגולר מתחיל את העבודה שלו ( פירוט מלא יותר אתם יכולים לקורא בפוסטים קודמים ) בלבצע compile על הדף ואז לקרוא ל- $apply. ראו קוד מתוך מתודה doBootstrap.
injector.invoke(function(scope, element, compile, injector, animate) {
scope.$apply(function() {
element.data('$injector', injector);
compile(element)(scope);
});
});
המשמעות של הקוד הזה שהפעלת כל ה- directives על הדף מתבצעת תחת $apply.
2. אז מתי צריך לקרוא ל- $apply באופן יזום?
כאשר צריך להגיב לאירועים, למשל של ממשק משתמש או תקשורת צריך בסוף האירוע להפעיל את $apply. דוגמת קוד מ- ng-event.
forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove
mouseenter mouseleave keydown keyup keypress submit focus blur copy
cut paste'.split(' '),
function(name) {
var directiveName = directiveNormalize('ng-' + name);
ngEventDirectives[directiveName] = ['$parse', function($parse) {
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
returnfunction(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
}];
}
);
כאן אנחנו רואים קוד מתוחכם שמייצר directive לכל אירוע, למשל ng-click. בכל ה- directive יש רישום לאירוע (element.on()) והפונקציה שנקראת בעת האירוע קוראת ל- $apply. זאת הסיבה שכאשר אתם משתמשים ב- ng-click אתם לא צריכים לקורא ל- $apply.
אנגולר בשירותים $http ו- $timeout גם מפעיל את $apply אחרי ביצוע הפונקציות שנרשמו לאירועים אסיכרונים.
אז מתי כן צריך לקרוא ל- $apply? כאשר אנחנו נרשמים לאירועים, לא דרך directive או שרות של אנגולר, צריך לבצע הפעלת $apply, לדוגמא עבודה עם $.ajax.
3. מה סדר הפעולות כאשר מבצעים $apply?
הקוד של $apply מאוד פשוט. אתם יכולים לראות שהוא מבצע את הקוד שלכם ואחרי זה מבצע את $digest שבודק מה השתנה בעץ של ה- scope. שוב ממליץ בחום לקורא את הפוסט הזה.
אפשר לחלק את הפונקציה $digest לשלושה חלקים עיקריים:
1. $evalAsync Queue
2. Traverse Scope Loop (TSL)
3. $$postDigest
הסברים:
החלק העיקרי של $digest זה TSL, החלק הזה אחראי בקוד למצוא מה השתנה בעץ של Scopes ולהפעיל את הפנקציות שנרשמו לשינוי במידה ויש ($watch.). אם אנגולר מזהה שינוי בעץ, השדה dirty יהיה במצב true ואז צריך לחזור לבצע שוב את הסריקה על העץ ( בציור זה שני החצים הסגולים ). הסיבה שחוזרים לסרוק שוב את העץ כי יכול להיות שהפונקציה שנרשמה לשינוי ביצעה בעצמה שינוי באחד ה- Scopes. במידה ואנגולר יגיעה לסריקה ה-10 של העץ ברצף, הוא יזרוק טעות ויצא מהפונקציה של ה- $digest.
5. מתי משתמשים ב- $evalAsync?
השימוש ב-$evalAsync לא נפוץ אך במצבים מסוימים זה מאוד יכול לעזור. תחשבו שאתם רוצים למשל לבנות directive שרוצה לבצע את הפעולות שלו על ה-DOM אחרי שכל ה-directives סיימו לבצע את עבודתם. למשל כאשר בניתי Group Validation in ng-repeat הייתי חייב לבדוק אם יש כפילות רק אחרי ש- ng-repeat סיים לייצר את כל הילדים שלו ובצע את כל ה- directives על כל ילד, ולכן כתבתי את הקוד בתוך $evelAsync.
6. מתי משתמשים ב- $$postDigest ?
מאוד לא נפוץ, הדולר הכפול מסמן פונקציה פרטית. אחרי שה- $digest סיים לעבור על כל העץ של ה- scopes, השורות האחרונות של הפונקציה זה לבצע את כל המתודות שרשומות ב- $$postDigest.
דוגמא לשימוש ב- $$postDigest:
angular.module("app", [])
.directive('postDigest', function ($document) {
return {
template : '<div ng-if="test">
<div id="eyal"></div>
</div>',
link : function(scope, element) {
// Not Work
var eyal = $('#eyal', element);
eyal.append($('<div>Link</div>'));
// Work
scope.$$postDigest(function() {
var eyal2 = $('#eyal', element);
eyal2.append($('<div>$$postDigest</div>'));
});
}
}
});
הסבר:
ב- template יש ng-if והוא מיצר את הילדים רק כאשר ה- $digest עובד ואז מופעל ה- $watch של ה- ng-if. כמו שהסברתי בסעיף 1, אחרי שאנגולר מקמפל את הדף הוא מבצע את $apply. ה- div עם ה-id='eyal' לא יהיה בעץ עד שה-$digest יעבוד וזה קורה אחרי ביצוע של הפונקציה link של postDigest directive, מכאן שאנחנו חייבים לעטוף את הקוד ב- $$postDigest כדי לבצע את הקוד אחרי שכל ה- wachers עבדו. היה אפשר להשתמש גם ב- $timeout אך זה פחות יעיל.
סיכום
בפוסט זה סקרתי את סדר הפעולות של $digest ו- $apply. מי שאהב את העומק של הדברים ורוצה להמשיך להעמיק בנושא מוזמן לבוא לקורס שלי AngularJS Deep Dive Course.
Lee Elenbaas (@LeeElenbaas)
יוני 1, 2014 at 5:51 am
לשימוש ב-$timeout במקום ב-$$postDigest יש ייתרון של שימוש ב-API מוצהר
בעוד ש-$$postDigest איננו נועד לשימוש וייתכן שהשימוש הפנימי בו ישתנה בגרסאות חדשות