מאחורי הקלעים של דפדפני אינטרנט מודרניים
הקדמה
חומר הלימוד המקיף הזה על הפעולות הפנימיות של WebKit ו-Gecko הוא תוצאה של מחקר רב שביצעה המפתחת הישראלית טלי גרסיאל. במשך כמה שנים היא בדקה את כל הנתונים שפורסמו על הרכיבים הפנימיים של הדפדפן, והקדישה הרבה זמן לקריאת קוד המקור של דפדפני אינטרנט. היא כתבה:
כמפתחי אינטרנט, לימוד הרכיבים הפנימיים של פעולות הדפדפן עוזר לכם לקבל החלטות טובות יותר ולהבין את ההצדקות לשיטות המומלצות לפיתוח. המסמך הזה ארוך למדי, אבל מומלץ להקדיש לו זמן. ככה עדיף.
Paul Irish, מנהל קשרי מפתחים ב-Chrome
מבוא
דפדפני אינטרנט הם התוכנות הנפוצות ביותר. במאמר הזה אסביר איך הם פועלים מאחורי הקלעים. נבדוק מה קורה כשמקלידים google.com
בסרגל הכתובות עד שדף Google יופיע במסך הדפדפן.
הדפדפנים שעליהם נדבר
יש היום חמישה דפדפנים עיקריים שמשמשים במחשבים: Chrome, Internet Explorer, Firefox, Safari ו-Opera. בניידים, הדפדפנים העיקריים הם דפדפן Android, iPhone, Opera Mini ו-Opera Mobile, UC Browser, הדפדפנים של Nokia S40/S60 ו-Chrome. כל הדפדפנים האלה, מלבד דפדפני Opera, מבוססים על WebKit. אציג דוגמאות מהדפדפנים בקוד פתוח Firefox ו-Chrome, ומהדפדפן Safari (שחלקו בקוד פתוח). לפי נתוני הסטטיסטיקה של StatCounter (נכון ליוני 2013), Chrome, Firefox ו-Safari מהווים כ-71% מכלל השימוש בדפדפנים למחשבים ברחבי העולם. בנייד, דפדפן Android, iPhone ו-Chrome מהווים כ-54% מהשימוש.
הפונקציונליות הראשית של הדפדפן
הפונקציה העיקרית של דפדפן היא להציג את משאב האינטרנט שבחרתם, על ידי שליחת בקשה אליו מהשרת והצגתו בחלון הדפדפן. המשאב הוא בדרך כלל מסמך HTML, אבל הוא יכול להיות גם קובץ PDF, תמונה או סוג אחר של תוכן. המשתמש מציין את המיקום של המשאב באמצעות URI (מזהה משאב אחיד).
האופן שבו הדפדפן מפרש ומציג קובצי HTML מצוין במפרטי HTML ו-CSS. המפרטים האלה מתוחזקים על ידי ארגון W3C (World Wide Web Consortium), שהוא ארגון התקנים של האינטרנט. במשך שנים, הדפדפנים תומכים רק בחלק מהמפרטים ופתחו תוספים משלהם. זה גרם לבעיות תאימות חמורות לכותבי אתרים. כיום, רוב הדפדפנים תואמים יותר או פחות למפרטים.
לממשקי המשתמש של הדפדפנים יש הרבה דברים משותפים. בין הרכיבים הנפוצים בממשק המשתמש:
- סרגל הכתובות להוספת URI
- לחצני 'הקודם' ו'הבא'
- אפשרויות לסימון כסימנייה
- לחצני רענון ועצירה לרענון או להפסקת הטעינה של המסמכים הנוכחיים
- לחצן דף הבית שמוביל לדף הבית
לצערנו, ממשק המשתמש של הדפדפן לא מצוין במפרט רשמי, אלא רק משיטות עבודה טובות שמעוצבות לאורך שנים של ניסיון ודפדפנים שמחקים זה את זה. במפרט HTML5 לא מוגדרים רכיבי ממשק משתמש שחייבים להיות בדפדפן, אבל יש בו רשימה של כמה רכיבים נפוצים. בין היתר ניתן למצוא את סרגל הכתובות, שורת הסטטוס וסרגל הכלים. כמובן שיש תכונות ייחודיות לדפדפן מסוים, כמו מנהל ההורדות של Firefox.
תשתית ברמה גבוהה
הרכיבים העיקריים של הדפדפן הם:
- ממשק המשתמש: כולל סרגל הכתובות, הלחצן 'הקודם'/'הבא', תפריט הסימניות וכו'. כל חלק בתצוגת הדפדפן מלבד החלון שבו מופיע הדף המבוקש.
- מנוע הדפדפן: מנהל את הפעולות בין ממשק המשתמש לבין מנוע הרינדור.
- מנוע הרינדור: אחראי להצגת התוכן המבוקש. לדוגמה, אם התוכן המבוקש הוא HTML, מנוע העיבוד מנתח HTML ו-CSS ומציג במסך את התוכן המנתח.
- רישות: לקריאות רשת כמו בקשות HTTP, באמצעות הטמעות שונות לפלטפורמות שונות מאחורי ממשק שלא תלוי בפלטפורמה.
- קצה עורפי של ממשק משתמש: משמש לציור ווידג'טים בסיסיים כמו תיבות שילוב וחלונות. הקצה העורפי הזה חושף ממשק כללי שאינו ספציפי לפלטפורמה. מתחתיו נעשה שימוש בשיטות של ממשק המשתמש של מערכת ההפעלה.
- מַפְרִיט JavaScript. משמש לניתוח ולהפעלה של קוד JavaScript.
- אחסון נתונים. זהו שכבת עקביות. יכול להיות שהדפדפן יצטרך לשמור באופן מקומי כל מיני נתונים, כמו קובצי cookie. דפדפנים תומכים גם במנגנוני אחסון כגון LocalStorage, IndexedDB, WebSQL ו-FileSystem.
חשוב לציין שבדפדפנים כמו Chrome פועלים כמה מופעים של מנוע הרינדור: אחד לכל כרטיסייה. כל כרטיסייה פועלת בתהליך נפרד.
מנועי עיבוד
האחריות של מנוע העיבוד היא תקינה... רינדור, שמציג את התוכן המבוקש במסך הדפדפן.
כברירת מחדל, מנוע העיבוד יכול להציג מסמכים ותמונות בפורמט HTML ו-XML. אפשר להציג סוגים אחרים של נתונים באמצעות יישומי פלאגין או תוספים. לדוגמה, הצגת מסמכי PDF באמצעות פלאגין של נגן PDF. עם זאת, בחלק הזה נתמקד בתרחיש לדוגמה העיקרי: הצגת HTML ותמונות בפורמט CSS.
דפדפנים שונים משתמשים במנועי עיבוד שונים: Internet Explorer משתמש ב-Trident, Firefox משתמש ב-Gecko ו-Safari משתמש ב-WebKit. ב-Chrome וב-Opera (מגרסה 15 ואילך) נעשה שימוש ב-Blink, גרסת פורק של WebKit.
WebKit הוא מנוע עיבוד בקוד פתוח שהתחיל כמנוע לפלטפורמת Linux, ו-Apple שינתה אותו כדי לתמוך ב-Mac וב-Windows.
התהליך הראשי
מנוע הרינדור יתחיל לקבל את תוכן המסמך המבוקש משכבת הרשת. זה נעשה בדרך כלל במקטעים של 8kB.
לאחר מכן, זהו התהליך הבסיסי של מנוע הרינדור:
מנוע העיבוד יתחיל לנתח את מסמך ה-HTML ולהמיר רכיבים לצמתים של DOM בעץ שנקרא 'עץ התוכן'. המנוע ינתח את נתוני הסגנון, גם בקובצי CSS חיצוניים וגם ברכיבי סגנון. פרטי העיצוב יחד עם ההוראות החזוניות ב-HTML ישמשו ליצירת עץ נוסף: עץ הרינדור.
עץ הרינדור מכיל מלבנים עם מאפיינים חזותיים כמו צבע ומימדים. המלבנים נמצאים בסדר הנכון כדי להופיע במסך.
לאחר בניית עץ העיבוד, הוא עובר תהליך של פריסה. כלומר, יש לתת לכל צומת את הקואורדינטות המדויקות במקום שבו הן יופיעו על המסך. השלב הבא הוא צביעה – יתבצע סריקה של עץ הרינדור וכל צומת יצויר באמצעות שכבת הקצה העורפי של ממשק המשתמש.
חשוב להבין שמדובר בתהליך הדרגתי. כדי לשפר את חוויית המשתמש, מנוע הרינדור ינסה להציג את התוכן במסך בהקדם האפשרי. הוא לא ימתין עד שכל ה-HTML ינותח לפני שהוא יתחיל ליצור את עץ הרינדור ולתכנן את הפריסה שלו. חלקים מהתוכן ינותחו ויוצגו, והתהליך יימשך יחד עם שאר התוכן שיגיע מהרשת.
דוגמאות לתהליך ראשי
באיורים 3 ו-4 אפשר לראות שלמרות שב-WebKit וב-Gecko נעשה שימוש במונחים שונים במקצת, התהליך הוא בעיקר זהה.
ב-Gecko, העץ של הרכיבים בפורמט חזותי נקרא 'עץ מסגרת'. כל רכיב הוא מסגרת. ב-WebKit נעשה שימוש במונח 'עץ רינדור', והוא מורכב מ'אובייקטים לרינדור'. WebKit משתמשת במונח "layout" כדי למקם אלמנטים, ו-Gecko מכנה אותו מחדש "Reflow". 'קובץ מצורף' הוא המונח של WebKit לחיבור צומתי DOM ומידע ויזואלי ליצירת עץ העיבוד. הבדל לא סמנטי קטן הוא שב-Gecko יש שכבה נוספת בין ה-HTML לעץ ה-DOM. הוא נקרא 'content sink' והוא מפעל ליצירת רכיבי DOM. נדבר על כל חלק בתהליך:
ניתוח – כללי
ניתוח הוא תהליך חשוב מאוד במנוע הרינדור, ולכן נרחיב עליו קצת יותר. נתחיל עם מבוא קצר על ניתוח.
ניתוח מסמך הוא תרגום שלו למבנה שאפשר להשתמש בו בקוד. תוצאת הניתוח היא בדרך כלל עץ של צמתים שמייצגים את המבנה של המסמך. הוא נקרא עץ ניתוח או עץ תחביר.
לדוגמה, ניתוח הביטוי 2 + 3 - 1
יכול להחזיר את העץ הזה:
דקדוק
הניתוח מבוסס על כללי התחביר של המסמך: השפה או הפורמט שבהם הוא נכתב. לכל פורמט שאפשר לנתח צריך להיות דקדוק דטרמיניסטי שמורכב ממילון וכללי תחביר. היא נקראת תחביר ללא הקשר. שפות אנושיות הן לא שפות כאלה, ולכן אי אפשר לנתח אותן באמצעות שיטות ניתוח רגילות.
שילוב של מנתח (parser) עם מנתח קוד (lexer)
אפשר להפריד את הניתוח לשני תהליכי משנה: ניתוח מילוני וניתוח תחביר.
ניתוח מילוני הוא התהליך של פיצול הקלט לאסימונים. אסימונים הם אוצר המילים בשפה: האוסף של אבני בניין תקפות. בשפה אנושית, הוא יכלול את כל המילים שמופיעות במילון של השפה הזו.
ניתוח תחביר הוא החלת כללי התחביר של השפה.
המנתחים בדרך כלל מחלקים את העבודה בין שני רכיבים: הלקסר (שלפעמים נקרא גם כלי ההמרה) שאחראי לפיצול הקלט לאסימונים חוקיים, והמנתח שאחראי לבניית עץ הניתוח על ידי ניתוח מבנה המסמך בהתאם לכללי התחביר של השפה.
הניתוח יודע להסיר תווים לא רלוונטיים כמו רווחים ופסיקים.
תהליך הניתוח הוא איטרטיבי. בדרך כלל, המנתח יבקש מהמנתח סמלים טקסטואליים (lexer) טוקן חדש וינסה להתאים את הטוקן לאחד מכללי התחביר. אם מתקבלת התאמה לכלל, צומת התואם לאסימון יתווסף לעץ הניתוח והמנתח יבקש אסימון נוסף.
אם לא נמצא כלל תואם, המנתח ישמור את האסימון באופן פנימי וימשיך לבקש אסימונים עד שימצא כלל שתואם לכל האסימונים שנשמרו באופן פנימי. אם לא נמצא כלל, מנתח ה-JSON יוצר חריגה. המשמעות היא שהמסמך לא היה חוקי והכיל שגיאות תחביר.
תרגום
במקרים רבים עץ הניתוח הוא לא המוצר הסופי. ניתוח נתונים משמש לעיתים קרובות בתרגום: הוא מאפשר להפוך את מסמך הקלט לפורמט אחר. דוגמה לכך היא הידור. המהדרר שמדרג קוד מקור לקוד מכונה קודם מפרק אותו לעץ ניתוח ואז מתרגם את העץ למסמך של קוד מכונה.
דוגמה לניתוח
באיור 5 יצרנו עץ ניתוח מביטוי מתמטי. ננסה להגדיר שפה מתמטית פשוטה ולראות את תהליך הניתוח.
תחביר:
- אבני הבניין של תחביר השפה הן ביטויים, מונחים ופעולות.
- השפה שלנו יכולה לכלול מספר בלתי מוגבל של ביטויים.
- ביטוי מוגדר כ'מונח' ואחריו 'פעולה' ואחריה מונח נוסף
- פעולה היא אסימון פלוס או אסימון מינוס
- מונח הוא אסימון של מספר שלם או ביטוי
עכשיו ננתח את הקלט: 2 + 3 - 1
.
מחרוזת המשנה הראשונה שתואמת לכלל היא 2
: לפי כלל מס' 5, זוהי מונח.
ההתאמה השנייה היא 2 + 3
: ההתאמה הזו תואמת לכלל השלישי: מונח ואחריו פעולה ואחריה מונח אחר.
ההתאמה הבאה תתבצע רק בסוף הקלט.
2 + 3 - 1
הוא ביטוי כי כבר ידוע לנו ש-2 + 3
הוא מונח, כך שיש לנו מונח ואחריו פעולה ואחריה מונח נוסף.
הערך 2 + +
לא יתאים לאף כלל, ולכן הוא קלט לא חוקי.
הגדרות רשמיות לאוצר מילים ולתחביר
אוצר המילים מתבטא בדרך כלל באמצעות ביטויים רגולריים.
לדוגמה, השפה שלנו תוגדר כך:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
כפי שרואים, מספרים שלמים מוגדרים באמצעות ביטוי רגולרי.
בדרך כלל, תחביר מוגדר בפורמט שנקרא BNF. השפה שלנו תוגדר כך:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
אמרנו שמנתחים רגילים יכולים לנתח שפה אם הדקדוק שלה הוא דקדוק ללא הקשר. הגדרה אינטואיטיבית לדקדוק ללא הקשר היא דקדוק שאפשר לבטא לגמרי ב-BNF. להגדרה רשמית, ראו המאמר בוויקיפדיה על דקדוק ללא הקשר
סוגי מנתח הנתונים
יש שני סוגים של מנתחים: מנתחים מלמעלה למטה ומנתחים מלמטה למעלה. הסבר אינטואיטיבי הוא שמנתחי טקסט מלמעלה למטה בודקים את המבנה ברמה הגבוהה של התחביר ומנסים למצוא התאמה לכלל. מנתח מלמטה למעלה מתחיל מהקלט וממיר אותו בהדרגה לכללי התחביר, החל מהכללים ברמה הנמוכה עד לעמידה בכללים ברמה הגבוהה.
נראה איך שני סוגי המנתחים ינתחו את הדוגמה שלנו.
המנתח מלמעלה למטה יתחיל מהכלל ברמה הגבוהה יותר: הוא יזהה את 2 + 3
כביטוי. לאחר מכן, המערכת תזהה את 2 + 3 - 1
כביטוי (תהליך הזיהוי של הביטוי מתפתח בהתאם להתאמה לכללים האחרים, אבל נקודת ההתחלה היא הכלל ברמה הגבוהה ביותר).
המנתח מלמטה למעלה יסרוק את הקלט עד שימצא כלל שמתאים. לאחר מכן הוא יחליף את הקלט התואם בכלל. הפעולה הזו תימשך עד סוף הקלט. הביטוי שהתאים חלקית מועבר לסטאק של המנתח.
סוג זה של מנתח מלמטה למעלה נקרא מנתח Shift-reduce, כי הקלט מופנה ימינה (למשל, מצביע שמצביע קודם בתחילת הקלט ועובר ימינה) והוא מצטמצם בהדרגה לכללי תחביר.
יצירת מנתח באופן אוטומטי
יש כלים שיכולים ליצור מנתח. אתם נותנים להם את הדקדוק של השפה שלכם – אוצר המילים וכללי התחביר – והם יוצרים מנתח נתונים תקין. כדי ליצור מנתח צריך הבנה מעמיקה של ניתוח, ולא קל ליצור מנתח מותאם ביד, ולכן גנרטורים של מנתח יכולים להיות שימושיים מאוד.
WebKit משתמשת בשני מחוללי מנתחים ידועים: Flex כדי ליצור lexer ו-Bison ליצירת מנתח (יכול להיות שתיתקלו בהם עם השמות Lex ו-Yacc). קלט Flex הוא קובץ שמכיל הגדרות של ביטויים רגולריים של האסימונים. הקלט של Bison הוא כללי התחביר של השפה בפורמט BNF.
מנתח HTML
תפקיד המערכת לניתוח HTML הוא לנתח את תגי העיצוב של HTML לעץ ניתוח.
דקדוק HTML
אוצר המילים והדקדוק של HTML מוגדרים במפרטים שנוצרו על ידי ארגון W3C.
כפי שראינו במבוא לניתוח, אפשר להגדיר באופן רשמי את התחביר הדקדוקי באמצעות פורמטים כמו BNF.
לצערי כל הנושאים המקובלים בכלי הניתוח לא רלוונטיים ל-HTML (לא הצגתי אותם סתם בשביל הכיף - נשתמש בהם בניתוח CSS ו-JavaScript). אי אפשר להגדיר בקלות את HTML באמצעות תחביר ללא הקשר שנחוץ למנתחים.
יש פורמט רשמי להגדרת HTML – DTD (Document Type Definition) – אבל הוא לא דקדוק ללא הקשר.
זה נראה מוזר במבט ראשון, כי HTML דומה מאוד ל-XML. יש הרבה מנתחני XML זמינים. יש וריאנט XML של HTML – XHTML – אז מה ההבדל הגדול?
ההבדל הוא שגישת ה-HTML היא "סלחנית" יותר: היא מאפשרת להשמיט תגים מסוימים (שאחר כך נוספים במרומז), או לפעמים להשמיט תגי התחלה או סיום, וכן הלאה. באופן כללי, זהו תחביר 'רך', בניגוד לתחביר הנוקשה והתובעני של XML.
הפרט הזה, שנראה קטן, עושה את כל ההבדל. מצד אחד, זו הסיבה העיקרית לפופולריות של HTML: הוא סולח על הטעויות שלכם ומקל על החיים של מחברי האתר. מצד שני, קשה לכתוב תחביר פורמלי. לסיכום, לא ניתן לנתח בקלות קובצי HTML באמצעות מנתחים רגילים, כי התחביר שלהם לא נטול הקשר. לא ניתן לנתח קובצי HTML באמצעות מנתחי XML.
HTML DTD
הגדרת ה-HTML היא בפורמט DTD. פורמט זה משמש להגדרת השפות במשפחת SGML. הפורמט מכיל הגדרות לכל הרכיבים המותרים, למאפיינים ולהיררכיה שלהם. כפי שראינו קודם, ה-DTD של HTML לא יוצר תחביר ללא הקשר.
יש כמה וריאציות של DTD. המצב הקפדני תואם רק למפרטים, אבל מצבים אחרים מכילים תמיכה בסימון שדפדפנים השתמשו בו בעבר. המטרה היא תאימות לאחור לתוכן ישן יותר. ה-DTD הנוכחי הוא: www.w3.org/TR/html4/strict.dtd
DOM
עץ הפלט ('עץ הניתוח') הוא עץ של צמתים של רכיבי DOM ומאפיינים. DOM הוא קיצור של Document Object Model. זוהי הצגת האובייקט של מסמך ה-HTML והממשק של רכיבי HTML לעולם החיצוני, כמו JavaScript.
הרמה הבסיסית (root) של העץ היא האובייקט Document.
ל-DOM יש קשר כמעט אחד לאחד לסימון. לדוגמה:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
תגי העיצוב האלה יתורגםו לעץ ה-DOM הבא:
כמו HTML, גם DOM מוגדר על ידי ארגון W3C. www.w3.org/DOM/DOMTR זהו מפרט כללי לטיפול במסמכים. מודול ספציפי מתאר רכיבים ספציפיים של HTML. ההגדרות של HTML מפורטות כאן: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.
אני אומר שהעץ מכיל צומתי DOM, כלומר העץ מורכב מרכיבים שמטמיעים את אחד מממשקי ה-DOM. בדפדפנים נעשה שימוש בהטמעות קונקרטיות שיש להן מאפיינים אחרים שהדפדפן משתמש בהם באופן פנימי.
אלגוריתם הניתוח
כפי שראינו בקטעים הקודמים, לא ניתן לנתח HTML באמצעות המנתחים הרגילים מסוג 'למעלה למטה' או 'מלמטה למעלה'.
הסיבות לכך הן:
- אופיה הסוחף של השפה.
- העובדה שלדפדפנים יש יכולת מסורתית לסבול שגיאות כדי לתמוך במקרים ידועים של HTML לא תקין.
- תהליך הניתוח הוא ניתוח חוזר. בשפות אחרות, המקור לא משתנה במהלך הניתוח, אבל ב-HTML, קוד דינמי (כמו רכיבי סקריפט שמכילים קריאות
document.write()
) יכול להוסיף עוד אסימונים, כך שתהליך הניתוח בעצם משנה את הקלט.
מכיוון שלא ניתן להשתמש בשיטות הניתוח הרגילות, הדפדפנים יוצרים מנתחים מותאמים אישית לניתוח HTML.
האלגוריתם לניתוח מתואר בפירוט במפרט HTML5. האלגוריתם מורכב משני שלבים: יצירת אסימונים ויצירת עץ.
יצירת אסימונים היא הניתוח הלוקאלי, ניתוח הקלט לאסימונים. בין האסימונים של HTML יש תגי התחלה, תגי סיום, שמות מאפיינים וערכים של מאפיינים.
רכיב ההמרה לאסימונים מזהה את האסימון, מעניק אותו לבונה העץ וצורך את התו הבא לצורך זיהוי האסימון הבא, וכן הלאה עד לסוף הקלט.
האלגוריתם של ההמרה לאסימונים
הפלט של האלגוריתם הוא אסימון HTML. האלגוריתם מתואר כמכונה מצבי. כל מצב צורך תו אחד או יותר מזרם הקלט ומעדכן את המצב הבא לפי התווים האלה. ההחלטה מושפעת ממצב היצירה הנוכחי של האסימונים וממצב היצירה של העץ. כלומר, אותו תו שנצרך יניב תוצאות שונות למצב הבא הנכון, בהתאם למצב הנוכחי. האלגוריתם מורכב מדי כדי לתאר אותו במלואו, לכן נציג דוגמה פשוטה שתעזור לנו להבין את העיקרון.
דוגמה בסיסית – יצירת אסימונים מקוד ה-HTML הבא:
<html>
<body>
Hello world
</body>
</html>
המצב הראשוני הוא 'מצב הנתונים'.
כשמגיעים לתו <
, המצב משתנה ל-'Tag open state'.
שימוש בתווית a-z
יוצר 'אסימון של תג התחלה', והמצב משתנה ל-'מצב שם התג'.
אנחנו נשארים במצב הזה עד שתו ה->
מנוצל. כל תו מצורף לשם האסימון החדש. במקרה שלנו, הטוקן שנוצר הוא html
.
כשמגיעים לתג >
, מופק האסימון הנוכחי והמצב משתנה חזרה ל"מצב הנתונים".
התג <body>
יטופל באותו אופן.
עד עכשיו, התגים html
ו-body
הונפקו. חזרנו אל 'מצב הנתונים'.
שימוש בתווית H
של Hello world
יגרום ליצירה ולהפצה של אסימון תו, והפעולה הזו תמשיך עד שמגיעים ל-<
של </body>
. נשמיע אסימון תו לכל תו של Hello world
.
אנחנו חוזרים ל'מצב פתוח של תג'.
שימוש בקלט הבא /
יגרום ליצירת end tag token
ולמעבר אל 'מצב שם התג'. שוב, אנחנו נשארים במצב הזה עד שמגיעים ל->
.לאחר מכן, אסימון התג החדש יופיע ונחזור ל'מצב נתונים'.
המערכת תתייחס לקלט </html>
כמו למקרה הקודם.
אלגוריתם לבניית עצים
כשהמנתח נוצר, נוצר אובייקט Document. במהלך בניית העץ, עץ ה-DOM עם המסמך בשורש שלו ישתנה והרכיבים יתווספו אליו. כל צומת שיופץ על ידי מחוללי המחרוזות יטופל על ידי ה-constructor של העץ. לכל אסימון, המפרט מגדיר איזה רכיב DOM רלוונטי לו וייווצר עבור האסימון הזה. הרכיב מתווסף לעץ ה-DOM וגם לסטאק של הרכיבים הפתוחים. הערימה הזו משמשת לתיקון אי-התאמות של עיטורים ותגים לא סגורים. האלגוריתם מתואר גם כמכונה מצבי. המצבים נקראים 'מצבי הכנסה'.
נראה את תהליך יצירת העץ לדוגמה:
<html>
<body>
Hello world
</body>
</html>
הקלט לשלב יצירת העץ הוא רצף של אסימונים משלב היצירה של האסימונים. המצב הראשון הוא 'המצב הראשוני'. קבלת האסימון 'html' תגרום למעבר למצב "before html" ולעיבוד מחדש של האסימון במצב הזה. הפעולה הזו תגרום ליצירה של הרכיב HTMLHtmlElement, שיתווסף לאובייקט Document ברמה הבסיסית.
המצב ישתנה ל'לפני הראש'. לאחר מכן מתקבל האסימון 'body'. המערכת תיצור באופן משתמע רכיב HTMLHeadElement, למרות שאין לנו אסימון 'head', והוא יתווסף לעץ.
עכשיו עוברים למצב 'בתוך הראש' ואז למצב 'אחרי הראש'. מתבצע עיבוד מחדש של אסימון ה-body, היצירה וההוספה של HTMLBodyElement והמצב מועבר אל "in body".
עכשיו מתקבלים אסימוני התווים של המחרוזת 'Hello world'. התו הראשון יגרום ליצירה ולהוספה של צומת 'טקסט', והתווים האחרים יתווספו לצומת הזה.
קבלת האסימון לסיום הגוף תגרום להעברה למצב 'אחרי הגוף'. עכשיו נקבל את תג ה-html end שמעביר אותנו למצב "after after body". קבלת האסימון של סוף הקובץ תסיים את הניתוח.
פעולות לביצוע בסיום הניתוח
בשלב הזה הדפדפן יסמן את המסמך כאינטראקטיבי ויתחיל לנתח סקריפטים שנמצאים במצב 'מושהה': אלה שצריך להריץ אחרי ניתוח המסמך. מצב המסמך יוגדר כ'הושלם' ואירוע "load" יופעל.
אפשר לראות את האלגוריתמים המלאים של יצירת אסימונים ובניית עצים במפרט ה-HTML5.
סבלנות של דפדפנים לשגיאות
אף פעם לא מקבלים שגיאת "תחביר לא חוקי" בדף HTML. הדפדפנים מתקנים את התוכן הלא תקין וממשיכים.
לדוגמה, הקוד הבא ב-HTML:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
כנראה שהפרתי כמיליון כללים ('mytag' אינו תג סטנדרטי, סידור שגוי של הרכיבים p ו-div ועוד), אבל הדפדפן עדיין מציג זאת כראוי ולא מתלונן. לכן, חלק גדול מקוד המנתח מתקן את הטעויות במחבר ה-HTML.
טיפול בשגיאות הוא עקבי למדי בדפדפנים, אבל באופן מפתיע הוא לא היה חלק מהמפרטים של HTML. כמו סימניות ולחצני חזרה/קדימה, זה פשוט משהו שהתפתח בדפדפנים במשך השנים. יש מבנים לא חוקיים ידועים של HTML שחוזרים על עצמם באתרים רבים, והדפדפנים מנסים לתקן אותם באופן שתואם לדפדפנים אחרים.
חלק מהדרישות האלה מוגדרות במפרט HTML5. (WebKit מסכם את זה יפה בתגובה בתחילת הכיתה של מנתח ה-HTML).
המנתח מנתח קלט שהומר לאסימון לתוך המסמך, וכך יוצר את עץ המסמכים. אם המסמך מסודר בצורה תקינה, קל לנתח אותו.
לצערנו, אנחנו צריכים לטפל במסמכי HTML רבים שלא בפורמט תקין, ולכן מנתח ה-HTML צריך להיות סובלני לשגיאות.
אנחנו צריכים לטפל לפחות בתנאי השגיאה הבאים:
- אסור להוסיף את הרכיב הזה באופן מפורש בתוך תג חיצוני כלשהו. במקרה כזה, צריך לסגור את כל התגים עד לתג שאוסר את הרכיב, ולהוסיף אותו לאחר מכן.
- אסור לנו להוסיף את הרכיב ישירות. יכול להיות שהאדם שכתב את המסמך שכח תג כלשהו באמצע (או שהתג באמצע הוא אופציונלי). זה יכול לקרות עם התגים הבאים: HTML HEAD BODY TBODY TR TD LI (שכחתי תג כלשהו?).
- אנחנו רוצים להוסיף רכיב בלוק בתוך רכיב בשורה. סוגרים את כל הרכיבים בשורה עד לרכיב הבלוק הבא ברמה גבוהה יותר.
- אם הפעולה הזו לא עוזרת, צריך לסגור רכיבים עד שנקבל הרשאה להוסיף את הרכיב – או להתעלם מהתג.
בואו נסתכל על כמה דוגמאות של סבילות לשגיאות WebKit:
</br>
במקום <br>
חלק מהאתרים משתמשים ב-</br>
במקום ב-<br>
. כדי להיות תואם ל-IE ול-Firefox, WebKit מתייחס לזה כמו ל-<br>
.
הקוד:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
שימו לב שהטיפול בשגיאות הוא פנימי: הוא לא יוצג למשתמש.
טבלה חסרה
טבלה חסרת בית היא טבלה בתוך טבלה אחרת, אבל לא בתוך תא בטבלה.
לדוגמה:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
התכונה WebKit תשנה את ההיררכיה לשתי טבלאות אחות:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
הקוד:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
מערכת WebKit משתמשת בסטאק לתוכן הרכיב הנוכחי: היא תגרום להוצאה של הטבלה הפנימית מהסטאק של הטבלה החיצונית. הטבלאות יהיו עכשיו אחיות.
רכיבי טפסים בתצוגת עץ
אם המשתמש מכניס טופס לתוך טופס אחר, המערכת תתעלם מהטופס השני.
הקוד:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
היררכיית תגים עמוקה מדי
התגובה מדברת בעד עצמה.
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
תגי HTML או תגי סיום של גוף המודעה ממוקמים במיקום שגוי
שוב – התגובה מדברת בעד עצמה.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
לכן, מחברי אתרים צריך להיזהר - אלא אם רוצים להופיע כדוגמה בקטע קוד של סבילות לשגיאות WebKit - כותבים HTML בפורמט תקין.
ניתוח CSS
זוכרים את מושגי הניתוח מהמבוא? בניגוד ל-HTML, CSS הוא דקדוק ללא הקשר וניתן לנתח אותו באמצעות סוגי המנתחים שמתוארים במבוא. למעשה, מפרט ה-CSS מגדיר את תחביר הלקסיקון והתחביר של CSS.
הנה כמה דוגמאות:
הדקדוק המילוני (אוצר המילים) מוגדר באמצעות ביטויים רגולריים לכל אסימון:
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
"ident" הוא קיצור של המילה identifier (מזהה), כמו שם של מחלקה. "name" הוא מזהה רכיב (נקרא על ידי "#" )
דקדוק התחביר מתואר ב-BNF.
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
הסבר:
קבוצת כללים היא בעלת המבנה הבא:
div.error, a.error {
color:red;
font-weight:bold;
}
div.error
ו-a.error
הם בוררים. החלק שבתוך הסוגריים המסולסלים מכיל את הכללים שמחילים על קבוצת הכללים הזו.
המבנה הזה מוגדר בצורה פורמלית בהגדרה הזו:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
כלומר, כלל הוא סלקטורים או מספר סלקטורים, מופרדים באמצעות פסיק ורווחים (S מייצג רווח). קבוצת כללים מכילה סוגריים מסולסלים ובתוכם הצהרה, או (אופציונלי) מספר הצהרות שמופרדות באמצעות נקודה-פסיק. 'הצהרות' ו'בורר' יוגדרו בהגדרות BNF הבאות.
מנתח CSS של WebKit
WebKit משתמשת במחוללי מנתחי Flex ו-Bison כדי ליצור מנתחים באופן אוטומטי מקובצי הדקדוק של CSS. כפי שציינו בהקדמה למעבד הטקסט, Bison יוצרת מעבד טקסט מסוג shift-reduce מלמטה למעלה. ב-Firefox נעשה שימוש בניתוח מלמעלה למטה שנכתב באופן ידני. בשני המקרים, כל קובץ CSS מנותח לאובייקט StyleSheet. כל אובייקט מכיל כללי CSS. אובייקטי הכללים של CSS מכילים אובייקטים של סלקטורים והצהרות, ואובייקטים אחרים שתואמים לדקדוק של CSS.
סדר העיבוד של סקריפטים וגיליונות סגנונות
סקריפטים
המודל של האינטרנט הוא סינכרוני. מחברי סקריפטים מצפים שהם יעברו ניתוח ויופעלו באופן מיידי כשמנתח הנתונים מגיע לתג <script>
.
ניתוח המסמך מושהה עד להרצת הסקריפט.
אם הסקריפט הוא חיצוני, קודם צריך לאחזר את המשאב מהרשת – הפעולה הזו מתבצעת גם באופן סינכרוני, והניתוח מושהה עד לאחזור המשאב.
זה היה המודל במשך שנים רבות, והוא מצוין גם במפרטי HTML4 ו-HTML5.
מחברים יכולים להוסיף את המאפיין 'השהיה' לסקריפט. במקרה כזה, הוא לא יפסיק את ניתוח המסמך ויתבצע אחרי ניתוח המסמך. ב-HTML5 נוספה אפשרות לסמן את הסקריפט כאסינכרוני, כך שהוא ינותח ויתבצע על ידי שרשור אחר.
ניתוח ספקולטיבי
גם WebKit וגם Firefox מבצעים את האופטימיזציה הזו. בזמן הפעלת סקריפטים, שרשור אחר מנתח את שאר המסמך ומגלה אילו משאבים אחרים צריך לטעון מהרשת ולטעון אותם. כך ניתן לטעון משאבים בחיבורים מקבילים והמהירות הכוללת משתפרת. הערה: המנתח השערוני מנתח רק הפניות למשאבים חיצוניים כמו סקריפטים חיצוניים, גיליונות סגנונות ותמונות: הוא לא משנה את עץ ה-DOM – זה נשאר למנתח הראשי.
גיליונות סגנונות
לעומת זאת, לגיליון סגנונות יש מודל שונה. באופן רעיוני, נראה שגיליונות סגנונות לא משנים את עץ ה-DOM, ולכן אין סיבה להמתין ולהפסיק את ניתוח המסמך. עם זאת, יש בעיה עם סקריפטים שמבקשים מידע על סגנון במהלך שלב הניתוח של המסמך. אם הסגנון עדיין לא נטען ונותח, הסקריפט יקבל תשובות שגויות וככל הנראה גרם לבעיות רבות. נראה שמדובר בתרחיש קיצוני, אבל הוא די נפוץ. דפדפן Firefox חוסם את כל הסקריפטים כשיש גיליון סגנונות שעדיין נטען ומנותח. WebKit חוסם סקריפטים רק כשהם מנסים לגשת למאפייני סגנון מסוימים שעשויים להיות מושפעים מגיליונות סגנון שלא הועלו.
עיבוד של בניית עצים
בזמן בניית עץ ה-DOM, הדפדפן בונה עץ נוסף, שהוא עץ העיבוד. העץ הזה מכיל אלמנטים חזותיים בסדר שבו הם יוצגו. זהו הייצוג החזותי של המסמך. מטרת העץ הזה היא לאפשר ציור של התוכן בסדר הנכון.
ב-Firefox, הרכיבים בעץ העיבוד קוראים 'frames'. ב-WebKit נעשה שימוש במונח 'כלי רינדור' או 'אובייקט רינדור'.
למעבד גרפיקה יש אפשרות למקם ולצייר את עצמו ואת הצאצאים שלו.
לסיווג RenderObject של WebKit, סוג הבסיס של כלי הרינדור, מוגדרת ההגדרה הבאה:
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
כל רכיב עיבוד מייצג אזור מלבני שתואם בדרך כלל לתיבת ה-CSS של צומת, כפי שמתואר במפרט CSS2. הוא כולל מידע גיאומורפי כמו רוחב, גובה ומיקום.
סוג התיבה מושפע מהערך 'display' של מאפיין הסגנון שרלוונטי לצומת (ראו הקטע חישוב הסגנון). זהו קוד WebKit להחלטה איזה סוג של עיבוד (render) צריך ליצור לצומת DOM, בהתאם למאפיין התצוגה:
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
גם סוג הרכיב נלקח בחשבון: לדוגמה, לפקדות בטופס ולטבלאות יש מסגרות מיוחדות.
ב-WebKit, אם רכיב רוצה ליצור עיבוד מיוחד, הוא יגביל את השיטה createRenderer()
.
המודלים להצגה מפנים לאובייקטים של סגנונות שמכילים מידע לא גיאומורפי.
הקשר של עץ הרינדור לעץ ה-DOM
הרסטוררים תואמים לרכיבי DOM, אבל היחס הוא לא אחד לאחד. רכיבי DOM שאינם חזותיים לא יתווספו לעץ העיבוד. דוגמה לכך היא הרכיב head. בנוסף, אלמנטים שהערך שלהם בתצוגה הוקצה ל-'none' לא יופיעו בעץ (אבל אלמנטים עם רמת חשיפה 'מוסתרת' יופיעו בעץ).
יש רכיבי DOM שתואמים לכמה אובייקטים חזותיים. בדרך כלל מדובר ברכיבים עם מבנה מורכב שלא ניתן לתאר באמצעות מלבן אחד. לדוגמה, לאלמנט "select" יש שלושה כלים לרינדור: אחד לאזור התצוגה, אחד לתיבת הרשימה הנפתחת ואחד ללחצן. כמו כן, כאשר טקסט מחולק למספר שורות מפני שהרוחב אינו מספיק לשורה אחת, השורות החדשות יתווספו ככלי רינדור נוספים.
דוגמה נוספת למעבדי רינדור מרובים היא HTML לא תקין. לפי מפרט ה-CSS, רכיב מוטבע חייב להכיל רק רכיבי בלוק או רק רכיבים מוטבעים. במקרה של תוכן מעורב, המערכת תיצור רכיבי עיבוד אנונימיים של בלוקים כדי לעטוף את הרכיבים שמוצגים בשורה.
חלק מאובייקטי הרינדור תואמים לצומת DOM, אבל לא באותו מקום בעץ. רכיבים צפים ורכיבים שממוקמים באופן מוחלט לא נמצאים בזרימה, הם ממוקמים בחלק אחר של העץ וממופים למסגרת האמיתית. מסגרת placeholder תופיע במקום שבו אמורים להופיע התמונות.
תהליך היצירה של העץ
ב-Firefox, המצגת רשומה כמאזין לעדכוני DOM.
המצגת מעניקה הרשאה ליצירת פריימים ל-FrameConstructor
, וה-constructor משנה את הסגנון (ראו חישוב סגנון) ויוצר פריים.
ב-WebKit התהליך של פתרון הסגנון ויצירת רינדור נקרא "קובץ מצורף". לכל צומת DOM יש שיטה 'attach'. הצירוף הוא סינכרוני, והוספת הצומת לעץ ה-DOM גורמת לקריאה ל-method 'attach' של הצומת החדש.
עיבוד ה-HTML ותגי הגוף גורם ליצירת השורש של עץ העיבוד.
האובייקט של עיבוד השורש תואם למה שמפרט ה-CSS מכנה את הבלוק המכיל: הבלוק העליון שמכיל את כל הבלוקים האחרים. המימדים שלו הם אזור התצוגה: המימדים של אזור התצוגה בחלון הדפדפן.
ב-Firefox הוא נקרא ViewPortFrame
וב-WebKit הוא נקרא RenderView
.
זהו אובייקט הרינדור שאליו המסמך מפנה.
שאר העץ נוצר כהוספה של צומתי DOM.
חישוב סגנון
כדי לבנות את עץ העיבוד, צריך לחשב את התכונות החזותיות של כל אובייקט רינדור. כדי לעשות זאת, מחשבים את מאפייני הסגנון של כל רכיב.
הסגנון כולל גיליונות סגנונות ממקורות שונים, רכיבי סגנון מוטמעים ומאפיינים חזותיים ב-HTML (כמו המאפיין 'bgcolor').המאפיינים החזותיים מתורגמים למאפייני סגנון CSS תואמים.
מקורות הגיליונות הם גיליונות הסגנון שמוגדרים כברירת מחדל בדפדפן, גיליונות הסגנון שסופקו על ידי מחבר הדף וגיליונות הסגנון של המשתמש – אלה גיליונות סגנון שסופקו על ידי משתמש הדפדפן (דפדפנים מאפשרים לכם להגדיר את הסגנונות המועדפים עליכם. לדוגמה, ב-Firefox, עושים זאת על ידי הוספת גיליון סגנונות לתיקייה 'פרופיל Firefox').
חישוב הסגנון כרוך בכמה קשיים:
- נתוני הסגנון הם מבנה גדול מאוד שמכיל את מאפייני הסגנון הרבים, ויכול לגרום לבעיות בזיכרון.
חיפוש הכללים התואמים לכל רכיב עלול לגרום לבעיות בביצועים אם לא מבצעים אופטימיזציה. קשה מאוד לעבור על כל רשימת הכללים של כל רכיב כדי למצוא התאמות. לבוררים יכול להיות מבנה מורכב שעלול לגרום לתהליך ההתאמה להתחיל בנתיב שנראה מבטיח אבל מתברר שהוא לא מוביל לתוצאה, ולכן צריך לנסות נתיב אחר.
לדוגמה, הבורר המורכב הזה:
div div div div{ ... }
כלומר, הכללים חלים על
<div>
שהוא צאצא של 3 תווי div. נניח שרוצים לבדוק אם הכלל חל על רכיב<div>
נתון. בוחרים נתיב מסוים למעלה בעץ לצורך בדיקה. ייתכן שתצטרכו לעבור על עץ הצומת למעלה כדי לראות שיש רק שני תווי div והכלל לא חל. לאחר מכן תצטרכו לנסות נתיבים אחרים בעץ.כדי להחיל את הכללים, צריך להשתמש בכללי שרשור מורכבים למדי שמגדירים את היררכיית הכללים.
נראה איך הדפדפנים מתמודדים עם הבעיות האלה:
שיתוף נתוני סגנון
צמתים של WebKit מפנים לאובייקטי סגנון (RenderStyle). אפשר לשתף את האובייקטים האלה בין צמתים בתנאים מסוימים. הצמתים הם אחים או בני דודים, ו:
- האלמנטים חייבים להיות באותו מצב של העכבר (למשל, לא ניתן שהאלמנט אחד יהיה ב-:hover והאלמנט השני לא יהיה ב-:hover)
- לא יכול להיות מזהה לאף אחד מהרכיבים
- שמות התגים צריכים להיות זהים
- מאפייני הכיתה צריכים להתאים
- קבוצת המאפיינים הממופים חייבת להיות זהה
- מצבי הקישור צריכים להיות זהים
- מצבי המיקוד חייבים להיות זהים
- אף אחד מהרכיבים לא אמור להיות מושפע מבוררי מאפיינים, כאשר 'מושפע' מוגדר כקיומו של התאמה לבורר שמשתמש בבורר מאפיינים בכל מיקום בבורר
- אסור לכלול מאפייני סגנון מוטבעים ברכיבים
- אסור להשתמש בבוררים אחים בכלל. WebCore פשוט מקפיץ מתג גלובלי בכל פעם שיש בורר אח, ומשבית את שיתוף הסגנונות של המסמך כולו כשהם נמצאים. זה כולל את הסלקטורים + ו-:first-child ו-:last-child.
עץ הכללים של Firefox
ב-Firefox יש שני עצים נוספים לחישוב קל יותר של סגנון: עץ הכללים ועץ ההקשר של הסגנון. ל-WebKit יש גם אובייקטים של סגנון, אבל הם לא מאוחסנים בעץ כמו עץ ההקשר של הסגנון, רק הצומת של ה-DOM מפנה לסגנון הרלוונטי.
הקשרי הסגנון מכילים ערכים סופיים. הערכים מחושבים על ידי החלת כל כללי ההתאמה בסדר הנכון וביצוע פעולות מניפולציה שמעבירות אותם מערכים לוגיים לערכים קונקרטיים. לדוגמה, אם הערך הלוגי הוא אחוז מהמסך, הוא יחושב ויעבור טרנספורמציה ליחידות מוחלטות. הרעיון של עץ הכללים הוא ממש חכם. הוא מאפשר לשתף את הערכים האלה בין צמתים כדי למנוע חישוב שלהם שוב. כך גם חוסכים מקום.
כל הכללים התואמים מאוחסנים בעץ. לצמתים התחתונים בנתיב יש עדיפות גבוהה יותר. העץ מכיל את כל הנתיבים להתאמות של כללים שנמצאו. אחסון הכללים מתבצע באופן מדורג. העץ לא מחושב בהתחלה לכל צומת, אבל בכל פעם שצריך לחשב סגנון של צומת, הנתיבים המחושבים מתווספים לעץ.
הרעיון הוא לראות את נתיבי העץ כמילים במילון. נניח שכבר חישבנו את עץ הכללים הזה:
נניח שאנחנו צריכים להתאים כללים לרכיב אחר בעץ התוכן, ומגלים שהכללים שתואמים (בסדר הנכון) הם B-E-I. הנתיב הזה כבר קיים בעץ, כי כבר חישבנו את הנתיב A-B-E-I-L. עכשיו יהיה לנו פחות עבודה.
נראה איך העץ חוסך לנו עבודה.
חלוקה למבנים
הקשרי הסגנון מחולקים ל-structs. המבנים האלה מכילים פרטי סגנון לקטגוריה מסוימת, כמו גבול או צבע. כל המאפיינים במבנה עוברים בירושה או לא עוברים בירושה. מאפיינים שעוברים בירושה הם מאפיינים שעוברים בירושה מהאלמנט ההורה, אלא אם הם מוגדרים על ידי האלמנט. נכסים שלא עברו בירושה (שנקראים מאפייני 'reset') משתמשים בערכי ברירת מחדל אם הם לא מוגדרים.
בעזרת העץ אנחנו שומרים במטמון מבנים שלמים (שמכילים את ערכי הסיום המחושבים) בעץ. הרעיון הוא שאם הצומת התחתון לא סיפק הגדרה למבנה, אפשר להשתמש במבנה שנשמר במטמון בצומת גבוה יותר.
חישוב ההקשרים של הסגנונות באמצעות עץ הכללים
כשמחשבים את הקשר הסגנון של רכיב מסוים, קודם אנחנו מחשבים נתיב בעץ הכללים או משתמשים בנתיב קיים. לאחר מכן, אנחנו מתחילים להחיל את הכללים בנתיב כדי למלא את המבנים בקונטקסט של הסגנון החדש. אנחנו מתחילים בצומת התחתון של הנתיב – זה עם העדיפות הגבוהה ביותר (בדרך כלל הבורר הספציפי ביותר) ועוברים בעץ עד שהמבנה שלנו מלא. אם אין מפרט של המבנה באותו צומת של הכלל, אנחנו יכולים לבצע אופטימיזציה משמעותית – אנחנו עולים במעלה העץ עד שאנחנו מוצאים צומת שמפרט אותו באופן מלא ומצביע עליו – זוהי האופטימיזציה הטובה ביותר – המבנה כולו משותף. כך חוסכים בזיכרון ובחישוב של ערכי הסיום.
אם נמצא הגדרות חלקיות, נמשיך לעלות בעץ עד שהמבנה יתמלא.
אם לא מצאנו הגדרות למבנה הנתונים, במקרה שהוא מסוג 'עובר בירושה', אנחנו מפנים למבנה הנתונים של ההורה בעץ ההקשר. במקרה הזה הצלחנו גם לשתף מבני. אם מדובר במבנה reset, המערכת תשתמש בערכי ברירת המחדל.
אם הצומת הספציפי ביותר מוסיף ערכים, נצטרך לבצע חישובים נוספים כדי להפוך אותו לערכים בפועל. לאחר מכן אנחנו שומרים את התוצאה במטמון בצומת העץ כדי שילדים יוכלו להשתמש בה.
אם לרכיב יש אח או אחות שמפנה לאותו צומת עץ, אפשר לשתף ביניהם את כל הקשר הסגנון.
בואו נראה דוגמה: נניח שיש לנו את הקוד הבא ב-HTML
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error</span> error
</p>
</div>
<div class="err" id="div2">another error</div>
</body>
</html>
וגם את הכללים הבאים:
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
כדי לפשט את הדברים, נניח שאנחנו צריכים למלא רק שני מבני struct: מבנה ה-struct של הצבע ומבנה ה-struct של השוליים. המבנה של הצבע מכיל רק חבר אחד: הצבע. המבנה של השוליים מכיל את ארבעת הצדדים.
עץ הכללים שייווצר ייראה כך (הצומתים מסומנים בשם הצומת: מספר הכלל שאליו הם מפנים):
עץ ההקשר ייראה כך (שם צומת: צומת כלל שאליו הם מפנים):
נניח שאנחנו מנתחים את ה-HTML ומגיעים לתג <div>
השני. אנחנו צריכים ליצור הקשר סגנון לצומת הזה ולמלא את מבני הנתונים של הסגנון שלו.
נתאמת את הכללים ונגלה שכללי ההתאמה של <div>
הם 1, 2 ו-6.
כלומר, כבר יש נתיב קיים בעץ שבו הרכיב שלנו יכול להשתמש, ואנחנו רק צריכים להוסיף אליו צומת נוסף עבור כלל 6 (צומת F בעץ הכללים).
נוצר הקשר סגנון ונוסיף אותו לעץ ההקשר. ההקשר של הסגנון החדש יצביע על הצומת F בעץ הכללים.
עכשיו אנחנו צריכים למלא את המבנים של הסגנונות. נתחיל למלא את מבנה השוליים. מאחר שציר הכלל האחרון (F) לא מוסיף למבנה של השוליים, אפשר לעלות במורד העץ עד שמוצאים מבנה ששמור במטמון וחושב בהוספת ציר קודם, ולהשתמש בו. נמצא אותו בצומת B, שהוא הצומת העליון שבו צוינו כללי שוליים.
יש לנו הגדרה למבנה הצבעים, ולכן לא ניתן להשתמש במבנה ששמור במטמון. מאחר שלצבע יש מאפיין אחד, אין צורך לעלות לעץ כדי למלא מאפיינים אחרים. נחשב את ערך הסיום (נמיר מחרוזת ל-RGB וכו') ונשמור במטמון את המבנה המחושב בצומת הזה.
העבודה על הרכיב השני <span>
היא עוד יותר קלה. נבצע התאמה של הכללים ונגיע למסקנה שהוא מפנה לכלל G, כמו ה-span הקודם.
מאחר שיש לנו אחים ואחיות שמפנים לאותו צומת, אנחנו יכולים לשתף את כל ההקשר של הסגנון ופשוט להצביע על ההקשר של הקטע הקודם.
למבנים שמכילים כללים שעוברים בירושה מההורה, האחסון במטמון מתבצע בעץ ההקשר (מאפיין הצבע עובר בירושה, אבל Firefox מתייחס אליו כאל איפוס ומאחסן אותו במטמון בעץ הכללים).
לדוגמה, אם הוספנו כללים לגופנים בפסקה:
p {font-family: Verdana; font size: 10px; font-weight: bold}
במקרה כזה, אלמנט הפסקה, שהוא צאצא של ה-div בעץ ההקשר, יכול היה לשתף את אותה מבנה גופן כמו ההורה שלו. מצב זה קורה אם לא הוגדרו כללי גופנים בפסקה.
ב-WebKit, שאין בו עץ כללים, ההצהרות שתואמות עוברות סריקה ארבע פעמים. קודם מופעלים כללים לא חשובים בעדיפות גבוהה (נכסים שצריך להחיל קודם כי אחרים תלויים בהם, כמו תצוגה), אחר כך כללים חשובים בעדיפות גבוהה, אחר כך כללים לא חשובים בעדיפות רגילה ואז כללים חשובים בעדיפות רגילה. המשמעות היא שנכסים שמופיעים כמה פעמים יטופלו לפי סדר המדורג הנכון. המשתמש האחרון מנצח.
לסיכום: שיתוף אובייקטי הסגנון (במלואם או חלק מהמבנים הפנימיים שלהם) פותר את הבעיות 1 ו-3. עץ הכללים של Firefox עוזר גם להחיל את המאפיינים בסדר הנכון.
מניפולציה של הכללים להתאמה קלה
יש כמה מקורות לכללי סגנון:
- כללי CSS, בגיליונות סגנונות חיצוניים או ברכיבי סגנון.
css p {color: blue}
- מאפייני סגנון בתוך השורה, כמו
html <p style="color: blue" />
- מאפיינים חזותיים של HTML (שמותאמים לכללי סגנון רלוונטיים)
html <p bgcolor="blue" />
שני המאפיינים האחרונים תואמים בקלות לרכיב, כי הוא הבעלים של מאפייני הסגנון, וניתן למפות מאפייני HTML באמצעות הרכיב בתור המפתח.
כפי שציינתי קודם בבעיה מס' 2, ההתאמה של כללי ה-CSS יכולה להיות מורכבת יותר. הכללים עוברים שינויים כדי לפתור את רמת הקושי ולקבל גישה קלה יותר.
אחרי הניתוח של גיליון הסגנונות, הכללים מתווספים לאחת מכמה מפות גיבוב, בהתאם לבורר. יש מפות לפי מזהה, לפי שם הכיתה, לפי שם התג ומפה כללית לכל מה שלא נכלל בקטגוריות האלה. אם הסלקטור הוא מזהה, הכלל יתווסף למפה של המזהים. אם מדובר בכיתה, הוא יתווסף למפת הכיתה וכו'.
כך קל יותר להתאים את הכללים. אין צורך לבדוק בכל הצהרה: אנחנו יכולים לחלץ מהמפות את הכללים הרלוונטיים לאלמנט. האופטימיזציה הזו מבטלת יותר מ-95% מהכללים, כך שאין צורך להתייחס אליהם אפילו בתהליך ההתאמה(4.1).
לדוגמה, אלה כללי הסגנון הבאים:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
הכלל הראשון יתווסף למיפוי הכיתות. הסרטון השני במפת המזהה והשלישי במפת התג.
בקטע ה-HTML הבא:
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
קודם ננסה למצוא כללים לרכיב p. מפת הכיתה תכיל מפתח 'error', שבמסגרתו נמצא הכלל של 'p.error'. לרכיב div יהיו כללים רלוונטיים במפת המזהה (המפתח הוא המזהה) ובמפת התג. כל מה שנשאר לעשות הוא לבדוק אילו מהכללים שחולצו על ידי המפתחות אכן מתאימים.
לדוגמה, אם הכלל של div היה:
table div {margin: 5px}
הוא עדיין יחולץ ממפת התגים, כי המפתח הוא הבורר השמאלי ביותר, אבל הוא לא יתאים לאלמנט div שלנו, שאין לו ישות אב של טבלה.
גם WebKit וגם Firefox מבצעים את אותה מניפולציה.
סדר היררכי של גיליונות סגנונות
לאובייקט הסגנון יש מאפיינים שתואמים לכל מאפיין חזותי (כל מאפייני ה-CSS אבל הם כלליים יותר). אם המאפיין לא מוגדר על ידי אף אחד מהכללים המותאמים, חלק מהמאפיינים יכולים לעבור בירושה לאובייקט הסגנון של רכיב ההורה. למאפיינים אחרים יש ערכי ברירת מחדל.
הבעיה מתחילה כשיש יותר מהגדרה אחת – כאן מגיע סדר ההשתלשלות כדי לפתור את הבעיה.
הצהרה עבור מאפיין סגנון יכולה להופיע במספר גיליונות סגנונות, ומספר פעמים בתוך גיליון סגנונות. כלומר, הסדר שבו מחילים את הכללים חשוב מאוד. הסדר הזה נקרא 'סדר מדורג'. לפי מפרט CSS2, סדר המפל הוא (מהנמוך לגבוה):
- הצהרות בדפדפן
- הצהרות רגילות של משתמשים
- הצהרות רגילות של המחבר
- כתיבת הצהרות חשובות
- הצהרות חשובות למשתמש
הצהרות הדפדפן הן הפחות חשובות, והמשתמש מבטל את ההצהרה של המחבר רק אם ההצהרה סומנה כחשובה. הצהרות באותו סדר ימוינו לפי רמת הספציפיות ולאחר מכן לפי הסדר שבו הן צוינו. המאפיינים החזותיים של HTML מתורגמים להצהרות CSS תואמות . הם נחשבים לכללים של המחבר עם עדיפות נמוכה.
ספציפיות
הספציפיות של הסלקטורים מוגדרת במפרט CSS2 באופן הבא:
- ספירה של 1 אם ההצהרה שממנה היא מגיעה היא מאפיין 'style' ולא כלל עם סלקטור, 0 אחרת (= a)
- ספירה של מאפייני המזהים בבורר (= b)
- ספירת מספר המאפיינים והפסאודו-כיתות האחרים בבורר (= c)
- ספירת מספר שמות הרכיבים והפסאודו-רכיבים בבורר (= d)
שרשור של ארבעת המספרים a-b-c-d (במערכת מספרים עם בסיס גדול) נותן את הספציפיות.
בסיס המספרים שבו אתם צריכים להשתמש מוגדר לפי המספר הגבוה ביותר שיש לכם באחת מהקטגוריות.
לדוגמה, אם a=14, אפשר להשתמש בבסיס הקסדצימלי. במקרה הלא סביר שבו a=17, תצטרכו בסיס מספרי של 17 ספרות. המצב המאוחר יותר יכול להתרחש עם בורר כזה: html body div p ... (17 תגים בבורר... לא סביר מאוד).
מספר דוגמאות:
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
מיון הכללים
אחרי שהכללים מותאמים, הם ממוינים לפי כללי השרשרת.
ב-WebKit נעשה שימוש במיון בועות לרשימות קטנות ובמיון מיזוג לרשימות גדולות.
WebKit מיישמת את המיון על ידי שינוי האופרטור >
של הכללים:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
תהליך הדרגתי
מערכת WebKit משתמשת בדגל שמציין אם כל גיליונות הסגנונות ברמה העליונה (כולל @imports) נטענו. אם הסגנון לא נטען במלואו בזמן ההצמדה, המערכת משתמשת בסמלי placeholder והוא מסומן במסמך. הם יחושבו מחדש אחרי טעינת גיליונות הסגנונות.
פריסה
כשיוצרים את המכשיר להצגה ומוסיפים אותו לעץ, אין לו מיקום וגודל. חישוב הערכים האלה נקרא פריסה או הזרמה חוזרת.
ב-HTML נעשה שימוש במודל פריסה מבוסס-זרימה, כלומר ברוב המקרים אפשר לחשב את הגיאומטריה במעבר יחיד. רכיבים בשלב מאוחר יותר 'בתהליך' בדרך כלל לא משפיעים על הגיאומטריה של הרכיבים בשלבים מוקדמים יותר של התהליך, כך שהפריסה יכולה להמשיך משמאל לימין, מלמעלה למטה במסמך. יש יוצאי דופן: לדוגמה, יכול להיות שתצטרכו יותר מעבר אחד כדי לעבד טבלאות HTML.
מערכת הקואורדינטות היא ביחס למסגרת הבסיס. נעשה שימוש בקואורדינטות העליונות והשמאליות.
יצירת הפריסה היא תהליך רקורסיבי. הוא מתחיל במעבד השורש, שתואם לרכיב <html>
במסמך ה-HTML. הפריסה ממשיכה באופן רפלקסיבי בחלק מההיררכיה של המסגרות או בכלה, ומחשבת מידע גיאומורפי לכל רכיב עיבוד גרפיקה שדורש זאת.
המיקום של כלי הרינדור ברמה הבסיסית הוא 0,0 והמידות שלו הן אזור התצוגה – החלק הגלוי של חלון הדפדפן.
לכל המכשירים להצגת תוכן יש שיטת 'פריסה' או 'זרימה מחדש', וכל מכשיר להצגת תוכן מפעיל את שיטת הפריסה של הצאצאים שלו שצריכים פריסה.
מערכת של סיביות מלוכלכות
כדי לא לבצע פריסה מלאה בכל שינוי קטן, הדפדפנים משתמשים במערכת 'bit מלוכלך'. רכיב עיבוד שעבר שינוי או הוספה מסומן בעצמו ובצאצאים שלו כ'לא נקי': רכיב שצריך פריסה.
קיימים שני דגלים: "dirty" ו-"Childrentyly" (ילדים מלוכלכים). משמעות הדבר היא שלמרות שהכלי לרינדור עצמו יכול להיות בסדר, יש לו לפחות צאצא אחד שדורש פריסה.
פריסה גלובלית ומצטברת
אפשר להפעיל את הפריסה בכל עץ הרינדור – זוהי פריסה 'גלובלית'. המצב הזה יכול לקרות כתוצאה מ:
- שינוי סגנון גלובלי שמשפיע על כל המכשירים שמריצים את ה-renderer, כמו שינוי גודל הגופן.
- כתוצאה משינוי גודל המסך
אפשר להגדיר פריסה מצטברת, שבה רק העצמים שעבר עליהם עיבוד גרפי יתפרסו (הדבר עלול לגרום לנזק מסוים שיחייב פריסות נוספות).
פריסה מצטברת מופעלת (באופן אסינכרוני) כשהכלים לרינדור מלוכלכים. לדוגמה, כשמתבצע צירוף של מעבדי גרפיקה חדשים לעץ הרינדור אחרי שהגיע תוכן נוסף מהרשת והתווסף לעץ ה-DOM.
פריסה אסינכרונית וסינכרונית
הפריסה המצטברת מתבצעת באופן אסינכרוני. ב-Firefox נוצרות תורים של 'פקודות זרימה מחדש' לפריסות מצטברות, ומתזמן מפעיל את ביצוע הקבוצה של הפקודות האלה. ב-WebKit יש גם טיימר שמפעיל פריסה מצטברת – העץ עובר טרנספורמציה ומתבצעת פריסה של המרתונים 'מלוכלכים'.
סקריפטים שמבקשים פרטי סגנון, כמו "offsetHeight", יכולים להפעיל פריסה מצטברת באופן סינכרוני.
בדרך כלל, הפריסה הגלובלית מופעלת באופן סינכרוני.
לפעמים האירוע layout מופעל כקריאה חוזרת (callback) אחרי פריסה ראשונית, כי מאפיינים מסוימים, כמו מיקום הגלילה, השתנו.
אופטימיזציות
כשפריסה מופעלת על ידי 'שינוי גודל' או שינוי במיקום של ה-renderer (ולא בגודל), גדלי הרינדור נלקחים מהמטמון ולא מחושבים מחדש…
במקרים מסוימים רק עץ משנה משתנה והפריסה לא מתחילה מהשורש. המצב הזה יכול לקרות במקרים שבהם השינוי הוא מקומי ולא משפיע על הסביבה שלו – כמו טקסט שמוחדר לשדות טקסט (אחרת כל הקשה על מקש תפעיל פריסה שמתחילה מהשורש).
תהליך הפריסה
בדרך כלל, הפריסה בנויה לפי התבנית הבאה:
- כלי הרינדור הראשי קובע את הרוחב שלו.
- הורה מעל ילדים וגם:
- ממקמים את ה-renderer הצאצא (מגדירים את הערכים x ו-y שלו).
- אם צריך, קוראים לפריסה של הצאצא – אם היא לא נקייה, אם אנחנו בפריסה גלובלית או מסיבה אחרת – ומחשבים את הגובה של הצאצא.
- האב משתמש בגבהים המצטברים של הצאצאים ובגובה של השוליים והרווחים כדי להגדיר את הגובה שלו – ההורה של האב של ה-renderer ישתמש בגובה הזה.
- מגדיר את הביט המשויך לזיהום כ-false.
Firefox משתמש באובייקט "state" (nsHTMLReflowState) כפרמטר לפריסה (הנקרא גם "reflow"). בין היתר, המדינה כוללת את רוחב ההורים.
הפלט של הפריסה ב-Firefox הוא אובייקט 'metrics'(nsHTMLReflowMetrics). הוא יכיל את הגובה המחושב של ה-renderer.
חישוב רוחב
רוחב המכשיר להצגת הגרפיקה מחושב על סמך רוחב בלוק המאגר, מאפיין 'רוחב' של סגנון המכשיר להצגת הגרפיקה, השוליים והגבולות.
לדוגמה, רוחב ה-div הבא:
<div style="width: 30%"/>
המערכת של WebKit תחשב את הערך הזה באופן הבא(השיטה calcWidth של הכיתה RenderBox):
- רוחב הקונטיינר הוא הערך המקסימלי של availableWidth של הקונטיינר ו-0. במקרה הזה, הערך של availableWidth הוא contentWidth, שמחושב לפי הנוסחה הבאה:
clientWidth() - paddingLeft() - paddingRight()
הערכים של clientWidth ו-clientHeight מייצגים את החלק הפנימי של אובייקט, לא כולל שוליים וסרגל גלילה.
רוחב הרכיבים הוא מאפיין הסגנון 'width'. הוא יחושב כערך מוחלט על ידי חישוב האחוז מתוך רוחב הקונטיינר.
עכשיו הגבולות והרווחים האופקיים מתווספים.
עד עכשיו זה היה החישוב של 'הרוחב המועדף'. עכשיו המערכת תחשב את הרוחב המינימלי והמקסימלי.
אם הרוחב המועדף גדול מהרוחב המקסימלי, המערכת משתמשת ברוחב המקסימלי. אם הוא קטן מהרוחב המינימלי (היחידת הקטנה ביותר שלא ניתן לשבור אותה), המערכת משתמשת ברוחב המינימלי.
הערכים מאוחסנים במטמון למקרה שיהיה צורך בפריסה, אבל הרוחב לא משתנה.
מעבר שורה
כאשר כלי לרינדור באמצע פריסה מחליט שהוא צריך להשתבש, כלי הרינדור מפסיק ועובר להורה של הפריסה שצריך לנתק. הרכיב ההורה יוצר את המעבדים הנוספים וקורא לפריסה בהם.
ציור
בשלב ציור, חוצה את עץ העיבוד ושיטת " Paint() " של כלי הרינדור קוראת להצגת תוכן על המסך. ב-Painting נעשה שימוש ברכיב התשתית של ממשק המשתמש.
גלובלי ומצטבר
בדומה לפריסה, גם הצביעה יכולה להיות גלובלית – כל העץ צבוע – או מצטברת. בציור מצטבר, חלק מהמנפיקים משתנים באופן שלא משפיע על כל העץ. ה-renderer ששונה מבטל את המלבן שלו במסך. כתוצאה מכך, מערכת ההפעלה מתייחסת אליו כאל 'אזור מלוכלך' ויוצרת אירוע 'צביעה'. מערכת ההפעלה עושה זאת בצורה חכמה וממזגת כמה אזורים לאחד. ב-Chrome זה מורכב יותר כי המנגנון להצגת הגרפיקה נמצא בתהליך שונה מהתהליך הראשי. Chrome מדמה את התנהגות מערכת ההפעלה במידה מסוימת. המצגת מקשיבה לאירועים האלה ומעבירה את ההודעה לשורש העיבוד. המערכת עוברת על העץ עד שמגיעה למעבד התצוגה הרלוונטי. הוא יתעדכן מחדש (ובדרך כלל גם הצאצאים שלו).
סדר ציור
ב-CSS2 מוגדר הסדר של תהליך הציור. זהו למעשה הסדר שבו הרכיבים מוערםים בהקשרים של סידור המקבצים. הסדר הזה משפיע על ציור כי המקבצים נצבעים מההתחלה ועד הסוף. סדר הערימה של כלי לרינדור בלוקים הוא:
- צבע רקע
- תמונת רקע
- border
- ילדים
- outline
רשימת התצוגה של Firefox
Firefox עובר על עץ הרינדור ויוצר רשימת תצוגה של המלבן המצויר. הוא מכיל את המכשירים הרלוונטיים לתמונה המלבנית, בסדר הציור הנכון (רקעים של המכשירים, ואז גבולות וכו').
כך צריך לעבור על העץ רק פעם אחת לצורך צביעה מחדש, במקום כמה פעמים – צביעה של כל הרקעים, אחר כך של כל התמונות, אחר כך של כל השוליים וכו'.
כדי לבצע אופטימיזציה של התהליך, Firefox לא מוסיף אלמנטים שיסתתרו, כמו אלמנטים שנמצאים לגמרי מתחת לאלמנטים אטומים אחרים.
אחסון של מלבן ב-WebKit
לפני הצביעה מחדש, מערכת WebKit שומרת את המלבן הישן כקובץ בייטמאפ. לאחר מכן, הוא מצייר רק את ההבדל בין המלבנים החדש והישן.
שינויים דינמיים
הדפדפנים מנסים לבצע את הפעולות המינימליות האפשריות בתגובה לשינוי. לכן, שינויים בצבע של רכיב יגרמו רק לצביעה מחדש של הרכיב. שינויים במיקום של האלמנט יגרמו לפריסה ולצביעה מחדש של האלמנט, של הצאצאים שלו ואולי גם של האחים שלו. הוספת צומת DOM תגרום לפריסה ולצביעה מחדש של הצומת. שינויים משמעותיים, כמו הגדלת גודל הגופן של רכיב ה-html, יגרמו לביטול התוקף של מטמון, לפריסה מחדש ולצביעה מחדש של כל העץ.
השרשורים של מנוע הרינדור
מנוע הרינדור הוא עם שרשור יחיד. כמעט כל הפעולות, מלבד פעולות רשת, מתבצעות בשרשור אחד. ב-Firefox וב-Safari, זהו ה-thread הראשי של הדפדפן. ב-Chrome, זהו ה-thread הראשי של תהליך הכרטיסייה.
פעולות רשת יכולות לבצע כמה שרשורים מקבילים. מספר החיבורים המקבילים מוגבל (בדרך כלל 2 עד 6 חיבורים).
לולאת אירוע
ה-thread הראשי בדפדפן הוא לולאת אירועים. זוהי לולאה אינסופית שמאפשרת לתהליך להמשיך לפעול. המערכת ממתינה לאירועים (כמו אירועים של פריסה וציור) ומעבדת אותם. זהו הקוד של Firefox לולאת האירועים הראשית:
while (!mExiting)
NS_ProcessNextEvent(thread);
מודל חזותי של CSS2
הקנבס
לפי מפרט CSS2, המונח 'לוח הציור' מתאר 'המרחב שבו מבנה העיצוב עובר רינדור': המקום שבו הדפדפן צובע את התוכן.
שטח הציור הוא אינסופי בכל אחד מהמימדים של המרחב, אבל הדפדפנים בוחרים רוחב ראשוני על סמך המימדים של אזור התצוגה.
לפי www.w3.org/TR/CSS2/zindex.html, הלוח שקוף אם הוא נכלל בלוח אחר, ויש לו צבע שהוגדר בדפדפן אם הוא לא נכלל בלוח אחר.
מודל Box ב-CSS
מודל התיבות של CSS מתאר את התיבות המלבניות שנוצרות עבור רכיבים בעץ המסמך וממוקמות בהתאם למודל הפורמט החזותי.
לכל תיבה יש אזור תוכן (למשל טקסט, תמונה וכו') ואזורים אופציונליים של שוליים, גבולות וריפוי מסביב.
כל צומת יוצר 0…n תיבות כאלה.
לכל הרכיבים יש מאפיין "display" שקובע את סוג התיבה שתיווצר.
דוגמאות:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
ברירת המחדל היא שורה אחר שורה, אבל גיליון הסגנונות של הדפדפן עשוי להגדיר הגדרות ברירת מחדל אחרות. לדוגמה: תצוגת ברירת המחדל של הרכיב 'div' היא של בלוק.
דוגמה לגיליון סגנונות ברירת מחדל זמינה כאן: www.w3.org/TR/CSS2/sample.html.
סכמת מיקום
יש שלוש סכימות:
- רגיל: האובייקט ממוקם בהתאם למיקום שלו במסמך. כלומר, המקום שלו בעץ העיבוד דומה למקומו בעץ ה-DOM והוא מעוצב בהתאם לסוג התיבה ולמידות שלו
- מספר ממשי (float): האובייקט מוצב בהתחלה לפי זרימה רגילה, ולאחר מכן מועבר כמה שיותר שמאלה או ימינה
- מוחלט: האובייקט מוצב בעץ העיבוד במקום אחר מאשר בעץ ה-DOM
סכימה למיקום מוגדרת על ידי המאפיין position והמאפיין float.
- סטטי ויחסי גורמים לזרימה רגילה
- הערכים absolute ו-fixed גורמים למיקום מוחלט
במיקום סטטי לא מוגדר מיקום, והמערכת משתמשת במיקום ברירת המחדל. בסכמות האחרות, המחבר מציין את המיקום: למעלה, למטה, ימין, שמאל.
אופן הפריסה של התיבה נקבע על סמך:
- סוג התיבה
- מידות הקופסה
- סכמת מיקום
- מידע חיצוני כמו גודל התמונה וגודל המסך
סוגי תיבות
תיבת בלוק: יוצרת בלוק – יש לה מלבן משלה בחלון הדפדפן.
תיבה בתוך שורה: אין לה בלוק משלה, אבל היא נמצאת בתוך בלוק מכיל.
הבלוקים מעוצבים בפורמט אנכי אחד אחרי השני. הפורמט של טקסט בשורה אחת הוא אופקי.
תיבות בתוך שורה ממוקמות בתוך שורות או 'תיבות שורה'. הקווים גבוהים לפחות כמו התיבה הגבוהה ביותר, אבל יכולים להיות גבוהים יותר כשהתיבות מותאמות 'לקו הבסיס' – כלומר החלק התחתון של הרכיב מיושר לנקודה בתיבה אחרת שאינה התחתונה. אם רוחב המארז לא מספיק, הטקסטים בתוך השורה יופיעו בכמה שורות. בדרך כלל זה מה שקורה בפסקה.
מיקום
קרוב-משפחה
מיקום יחסי – המיקום נקבע כרגיל ולאחר מכן הוא מועבר לפי הערך הנדרש.
צף
תיבת צף מוסטת שמאלה או ימינה לקו. התכונה המעניינת היא שהתיבות האחרות זורמות מסביב לה. ה-HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
ייראה כך:
מוחלט וקבוע
הפריסה מוגדרת במדויק, ללא קשר לתהליך הרגיל. הרכיב לא משתתף בתהליך הרגיל. המאפיינים האלה הם יחסיים לקונטיינר. באופן קבוע, המאגר הוא אזור התצוגה.
ייצוג בשכבות
המאפיין הזה מצוין על ידי מאפיין ה-CSS z-index. היא מייצגת את המימד השלישי של התיבה: את המיקום שלה לאורך ציר "z".
התיבות מחולקות לערמות (שנקראות הקשרי עריכה). בכל סטאק, האלמנטים מאחור ייצבעו קודם והרכיבים הקדמיים למעלה, קרוב יותר למשתמש. במקרה של חפיפה, הרכיב העליון יסתיר את הרכיב הקודם.
הערימות מסודרות לפי המאפיין z-index. תיבות עם המאפיין 'z-index' יוצרות מקבץ מקומי. אזור התצוגה מכיל את הסטאק החיצוני.
דוגמה:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div
style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
</div>
<div
style="z-index: 1;background-color:green;width: 2in; height: 2in;">
</div>
</p>
התוצאה תהיה:
למרות ש-div האדום מופיע לפני ה-div הירוק בסימון, והוא היה צבוע לפניו בזרימה הרגילה, ערך המאפיין z-index שלו גבוה יותר, ולכן הוא מופיע מוקדם יותר בסטאק שנמצא בקופסה ברמה הבסיסית.
משאבים
ארכיטקטורת הדפדפן
- Grosskurth, Alan. ארכיטקטורת עזר לדפדפני אינטרנט (pdf)
- גופטה, ויניט. איך פועלים דפדפנים – חלק 1 – ארכיטקטורה
ניתוח
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (נקרא גם 'ספר הדרקון'), Addison-Wesley, 1986
- ריק ג'ליף (Rick Jelliffe). היפה והחיה: שני טיוטות חדשות ל-HTML 5.
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers
- L. דייוויד ברון (David Baron), HTML ו-CSS מהירים יותר: תוכן פנימי של Layout Engine למפתחי אתרים (סרטון דיון טכנולוגי של Google)
- L. David Baron, מנוע הפריסה של Mozilla
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- כריס ווטרסון (Chris Watson), סקירה כללית על Gecko
- Alexander Larsson, The life of an HTML HTTP request
WebKit
- David Hyatt, Implementing CSS(part 1)
- David Hyatt, סקירה כללית של WebCore
- דייוויד הייט (David Hyatt), עיבוד של WebCore
- David Hyatt, The FOUC Problem
מפרטים של W3C
הוראות ל-build של דפדפנים
תרגומים
הדף הזה תורגם ליפנית, פעמיים:
- איך פועלים הדפדפנים – מאחורי הקלעים של דפדפני האינטרנט המודרניים (ja) מאת @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 מאת @ikeike443 ו@kiyoto01.
אפשר לראות את התרגומים המארחים באופן חיצוני של קוריאנית ושל טורקית.
תודה לכולם.