מאחורי הקלעים של דפדפני אינטרנט מודרניים
הקדמה
המדריך המקיף הזה על הפעולות הפנימיות של WebKit ו-Gecko הוא תוצאה של מחקר רב שבוצע על ידי המפתחת הישראלית טלי גרסיאל. במשך כמה שנים היא בדקה את כל הנתונים שפורסמו על המבנה הפנימי של הדפדפנים, והקדישה הרבה זמן לקריאת קוד המקור של דפדפני אינטרנט. היא כתבה:
כמפתחי אתרים, הבנת הפעולות הפנימיות של הדפדפן עוזרת לכם לקבל החלטות טובות יותר ולהבין את ההצדקות מאחורי השיטות המומלצות לפיתוח. המסמך הזה די ארוך, אבל מומלץ להקדיש קצת זמן כדי לקרוא אותו לעומק. ככה עדיף.
פול אייריש, קשרי מפתחים ב-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, באמצעות הטמעות שונות לפלטפורמות שונות מאחורי ממשק שאינו תלוי בפלטפורמה.
- קצה עורפי של ממשק המשתמש: משמש לציור ווידג'טים בסיסיים כמו תיבות משולבות וחלונות. ה-backend הזה חושף ממשק כללי שלא ספציפי לפלטפורמה. הוא משתמש בשיטות של ממשק המשתמש של מערכת ההפעלה.
- מפרש 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, ואפל שינתה אותו כדי לתמוך ב-Mac וב-Windows.
התהליך הראשי
מנוע העיבוד יתחיל לקבל את התוכן של המסמך המבוקש משכבת הרשת. בדרך כלל, הפעולה הזו תתבצע בחלקים של 8kB.
לאחר מכן, זהו התהליך הבסיסי של מנוע העיבוד:
מנוע העיבוד יתחיל לנתח את מסמך ה-HTML ולהמיר רכיבים לצומתי DOM בעץ שנקרא "עץ התוכן". המנוע ינתח את נתוני הסגנון, גם בקובצי CSS חיצוניים וגם ברכיבי סגנון. המידע על הסגנון, יחד עם ההוראות החזותיות ב-HTML, ישמשו ליצירת עץ נוסף: עץ העיבוד.
עץ העיבוד מכיל מלבנים עם מאפיינים חזותיים כמו צבע ומידות. המלבנים מסודרים בסדר הנכון להצגה במסך.
אחרי בניית עץ העיבוד, הוא עובר תהליך של "פריסה". כלומר, צריך לתת לכל צומת את הקואורדינטות המדויקות שבהן הוא אמור להופיע במסך. השלב הבא הוא ציור – המערכת תעבור על עץ העיבוד וכל צומת יצויר באמצעות שכבת הקצה העורפי של ממשק המשתמש.
חשוב להבין שזה תהליך הדרגתי. כדי לשפר את חוויית המשתמש, מנוע העיבוד ינסה להציג את התוכן במסך כמה שיותר מהר. הוא לא ימתין עד שכל ה-HTML ינותח לפני שיתחיל לבנות ולפרוס את עץ העיבוד. חלקים מהתוכן ינותחו ויוצגו, והתהליך יימשך עם שאר התוכן שממשיך להגיע מהרשת.
דוגמאות לתהליך הראשי
באיורים 3 ו-4 אפשר לראות שלמרות ש-WebKit ו-Gecko משתמשים במונחים שונים, התהליך זהה.
ב-Gecko, עץ הרכיבים בפורמט חזותי נקרא "עץ מסגרות". כל רכיב הוא פריים. ב-WebKit משתמשים במונח "עץ עיבוד" והוא מורכב מ "אובייקטים לעיבוד". ב-WebKit משתמשים במונח "פריסה" למיקום של רכיבים, וב-Gecko קוראים לזה "עימוד מחדש". "צירוף" הוא המונח של WebKit לחיבור צומתי DOM ומידע חזותי כדי ליצור את עץ העיבוד. הבדל קטן ולא סמנטי הוא של-Gecko יש שכבה נוספת בין ה-HTML לבין עץ ה-DOM. הוא נקרא "content sink" והוא משמש ליצירת רכיבי DOM. נדבר על כל חלק בתהליך:
ניתוח – כללי
מכיוון שהניתוח הוא תהליך משמעותי מאוד במנוע העיבוד, נתעמק בו קצת יותר. נתחיל בהקדמה קצרה על ניתוח.
ניתוח מסמך פירושו תרגום שלו למבנה שהקוד יכול להשתמש בו. התוצאה של הניתוח היא בדרך כלל עץ של צמתים שמייצגים את מבנה המסמך. זה נקרא עץ ניתוח או עץ תחביר.
לדוגמה, ניתוח הביטוי 2 + 3 - 1 יכול להחזיר את העץ הזה:
דקדוק
הניתוח מבוסס על כללי התחביר של המסמך: השפה או הפורמט שבהם הוא נכתב. לכל פורמט שאתם יכולים לנתח חייב להיות דקדוק דטרמיניסטי שמורכב מכללי אוצר מילים ותחביר. היא נקראת דקדוק חופשי הקשר. שפות אנושיות הן לא שפות כאלה, ולכן אי אפשר לנתח אותן באמצעות טכניקות ניתוח רגילות.
שילוב של מנתח ולקסר
אפשר לחלק את תהליך הניתוח לשני תהליכי משנה: ניתוח מילוני וניתוח תחביר.
ניתוח לקסיקלי הוא התהליך של פירוק הקלט לאסימונים. אסימונים הם אוצר המילים של השפה: אוסף של אבני בניין תקינות. בשפה אנושית, הוא יכלול את כל המילים שמופיעות במילון של אותה שפה.
ניתוח תחביר הוא החלת כללי התחביר של השפה.
מנתחים בדרך כלל מחלקים את העבודה בין שני רכיבים: הלקסר (לפעמים נקרא מנתח לקסיקלי) שאחראי על פירוק הקלט לאסימונים חוקיים, והמנתח שאחראי על בניית עץ הניתוח על ידי ניתוח מבנה המסמך לפי כללי התחביר של השפה.
הנתח הלקסיקלי יודע איך להסיר תווים לא רלוונטיים כמו רווחים לבנים ומעברי שורה.
תהליך הניתוח הוא תהליך שחוזר על עצמו. המנתח בדרך כלל יבקש מהמנתח הלקסיקלי אסימון חדש וינסה להתאים את האסימון לאחד מכללי התחביר. אם נמצאת התאמה לכלל, צומת שתואם לאסימון יתווסף לעץ הניתוח והמנתח יבקש אסימון נוסף.
אם אין כלל תואם, המנתח ישמור את האסימון באופן פנימי וימשיך לבקש אסימונים עד שיימצא כלל שתואם לכל האסימונים ששמורים באופן פנימי. אם לא נמצא כלל, המנתח יציג חריגה. המשמעות היא שהמסמך לא היה תקין והכיל שגיאות תחביר.
תרגום
בהרבה מקרים, עץ הניתוח הוא לא המוצר הסופי. ניתוח משמש לעיתים קרובות בתרגום: המרת מסמך הקלט לפורמט אחר. לדוגמה, קומפילציה. הקומפיילר שמקמפל קוד מקור לשפת מכונה מנתח אותו קודם לעץ ניתוח ואז מתרגם את העץ למסמך בשפת מכונה.
דוגמה לניתוח
באיור 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 ליצירת מנתח לקסיקלי ו-Bison ליצירת מנתח (יכול להיות שתיתקלו בהם בשמות Lex ו-Yacc). הקלט של Flex הוא קובץ שמכיל הגדרות של ביטויים רגולריים של הטוקנים. הקלט של Bison הוא כללי התחביר של השפה בפורמט BNF.
מנתח HTML
המטרה של מנתח ה-HTML היא לנתח את תגי העיצוב של ה-HTML ולהפוך אותם לעץ ניתוח.
דקדוק HTML
אוצר המילים והתחביר של HTML מוגדרים במפרטים שנוצרו על ידי ארגון W3C.
כפי שראינו במבוא לניתוח, אפשר להגדיר תחביר דקדוקי באופן רשמי באמצעות פורמטים כמו BNF.
לצערנו, כל הנושאים הקונבנציונליים של מנתח לא חלים על HTML (לא העליתי אותם סתם בשביל הכיף – הם ישמשו לניתוח CSS ו-JavaScript). קשה להגדיר HTML באמצעות דקדוק חופשי-הקשר שנדרש למנתחים.
יש פורמט רשמי להגדרת HTML – DTD (הגדרת סוג מסמך) – אבל הוא לא דקדוק חופשי-הקשר.
זה נראה מוזר במבט ראשון, כי HTML די דומה ל-XML. יש הרבה מנתחי XML זמינים. יש גרסת XML של HTML, שנקראת XHTML. אז מה ההבדל הגדול?
ההבדל הוא שגישת ה-HTML היא יותר "סלחנית": היא מאפשרת להשמיט תגים מסוימים (שמתווספים באופן משתמע), או לפעמים להשמיט תגי התחלה או סיום, וכן הלאה. באופן כללי, התחביר של JSON הוא "רך", בניגוד לתחביר הנוקשה והתובעני של XML.
הפרט הקטן הזה יכול לעשות הבדל גדול. מצד אחד, זו הסיבה העיקרית לכך ש-HTML כל כך פופולרית: היא סלחנית כלפי טעויות ומקלה על החיים של יוצרי האתרים. מצד שני, קשה לכתוב דקדוק פורמלי. לסיכום, מנתחי HTML רגילים לא יכולים לנתח HTML בקלות, כי הדקדוק שלו לא נטול הקשר. מנתחי XML לא יכולים לנתח HTML.
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.
השורש של העץ הוא האובייקט 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 באמצעות מנתחי ה-top down או ה-bottom up הרגילים.
הסיבות לכך:
- האופי הסלחני של השפה.
- העובדה שדפדפנים כוללים סבילות מסורתית לשגיאות כדי לתמוך במקרים ידועים של HTML לא תקין.
- תהליך הניתוח הוא תהליך שניתן להפעיל מחדש. בשפות אחרות, המקור לא משתנה במהלך הניתוח, אבל ב-HTML, קוד דינמי (כמו רכיבי סקריפט שמכילים קריאות ל-
document.write()) יכול להוסיף אסימונים, כך שתהליך הניתוח משנה בפועל את הקלט.
דפדפנים לא יכולים להשתמש בטכניקות הניתוח הרגילות, ולכן הם יוצרים מנתחים מותאמים אישית לניתוח HTML.
אלגוריתם הניתוח מתואר בפירוט במפרט של HTML5. האלגוריתם מורכב משני שלבים: טוקניזציה ובניית עץ.
טוקניזציה היא ניתוח לקסיקלי, שבו הקלט מנותח לטוקנים. בין הטוקנים של HTML יש תגי פתיחה, תגי סגירה, שמות מאפיינים וערכי מאפיינים.
ה-tokenizer מזהה את הטוקן, מעביר אותו ל-tree constructor, ומשתמש בתו הבא כדי לזהות את הטוקן הבא, וכך הלאה עד לסוף הקלט.
האלגוריתם ליצירת טוקנים
הפלט של האלגוריתם הוא אסימון 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> יטופל כמו במקרה הקודם.
אלגוריתם לבניית עץ
כשהמנתח נוצר, נוצר גם אובייקט המסמך. בשלב בניית העץ, עץ ה-DOM עם המסמך בבסיס שלו ישתנה ויתווספו לו רכיבים. כל צומת שנוצר על ידי ה-tokenizer יעובד על ידי ה-tree constructor. לכל אסימון, המפרט מגדיר איזה רכיב DOM רלוונטי לו וייווצר עבורו. הרכיב נוסף לעץ ה-DOM וגם למחסנית של הרכיבים הפתוחים. המחסנית הזו משמשת לתיקון חוסר התאמה בהטמעה ותגים שלא נסגרו. האלגוריתם מתואר גם כמכונת מצבים. המצבים האלה נקראים "מצבי הוספה".
בואו נראה את תהליך בניית העץ של הקלט לדוגמה:
<html>
<body>
Hello world
</body>
</html>
הקלט לשלב בניית העץ הוא רצף של טוקנים משלב הטוקניזציה. המצב הראשון הוא המצב הראשוני. קבלת האסימון "html" תגרום למעבר למצב "לפני html" ולעיבוד מחדש של האסימון במצב הזה. הפעולה הזו תגרום ליצירת הרכיב HTMLHtmlElement, שיתווסף לאובייקט הבסיס Document.
המצב ישתנה ל-"לפני הכותרת". לאחר מכן מתקבל האסימון "body". HTMLHeadElement ייווצר באופן משתמע למרות שאין לנו אסימון "head", והוא יתווסף לעץ.
עכשיו אנחנו עוברים למצב "in head" ואז למצב "after head". האסימון של גוף המסמך מעובד מחדש, נוצר HTMLBodyElement ומוכנס, והמצב מועבר אל "in body".
האסימונים של התווים במחרוזת "Hello world" התקבלו. התו הראשון יגרום ליצירה ולהוספה של צומת "טקסט", והתווים האחרים יתווספו לצומת הזה.
קבלת האסימון של סוף גוף הדף תגרום למעבר למצב "אחרי גוף הדף". עכשיו נקבל את תג הסיום של ה-HTML, שיעביר אותנו למצב "after after body". קבלת אסימון סוף הקובץ תסיים את הניתוח.
פעולות בסיום הניתוח
בשלב הזה הדפדפן יסמן את המסמך כאינטראקטיבי ויתחיל לנתח סקריפטים שנמצאים במצב "מושהה": סקריפטים שצריך להפעיל אחרי שהמסמך מנותח. מצב המסמך יוגדר כ "הושלם" ויופעל אירוע "טעינה".
במפרט של 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 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 או body שלא נמצאים במקום הנכון
שוב – התגובה מדברת בעד עצמה.
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" הוא קיצור של מזהה, כמו שם של מחלקה. "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 מייצג רווח). קבוצת כללים מכילה סוגריים מסולסלים ובתוכם הצהרה או מספר הצהרות שמופרדות באמצעות נקודה-פסיק. המונחים "declaration" ו-"selector" יוגדרו בהגדרות ה-BNF הבאות.
מנתח CSS של WebKit
WebKit משתמש במחוללי מנתחים של Flex ו-Bison כדי ליצור מנתחים באופן אוטומטי מקובצי הדקדוק של CSS. כפי שאתם זוכרים מהמבוא למנתח, Bison יוצר מנתח shift-reduce מלמטה למעלה. ב-Firefox נעשה שימוש במנתח מלמעלה למטה שנכתב באופן ידני. בשני המקרים, כל קובץ CSS מנותח לאובייקט StyleSheet. כל אובייקט מכיל כללי CSS. אובייקטים של כללי CSS מכילים אובייקטים של סלקטורים והצהרות ואובייקטים אחרים שתואמים לדקדוק של CSS.
סדר העיבוד של סקריפטים וגיליונות סגנונות
סקריפטים
המודל של האינטרנט הוא סינכרוני. היוצרים מצפים שהסקריפטים ינותחו ויופעלו מיד כשהמנתח מגיע לתג <script>.
ניתוח המסמך נעצר עד שהסקריפט מופעל.
אם הסקריפט חיצוני, צריך קודם לאחזר את המשאב מהרשת. גם הפעולה הזו מתבצעת באופן סינכרוני, והניתוח נעצר עד לאחזור המשאב.
זה היה המודל במשך שנים רבות, והוא מצוין גם במפרטים של HTML4 ו-HTML5.
יוצרים יכולים להוסיף את המאפיין "defer" לסקריפט, ובמקרה כזה הוא לא יעצור את ניתוח המסמך ויופעל אחרי שהמסמך ינותח. ב-HTML5 יש אפשרות לסמן את הסקריפט כאסינכרוני, כדי ששרשור אחר ינתח ויפעיל אותו.
ניתוח ספקולטיבי
גם WebKit וגם Firefox מבצעים את האופטימיזציה הזו. בזמן שהסקריפטים פועלים, שרשור אחר מנתח את שאר המסמך, מזהה אילו משאבים נוספים צריך לטעון מהרשת וטוען אותם. כך אפשר לטעון משאבים בחיבורים מקבילים ולשפר את המהירות הכוללת. הערה: המנתח הספקולטיבי מנתח רק הפניות למשאבים חיצוניים כמו סקריפטים חיצוניים, גיליונות סגנונות ותמונות. הוא לא משנה את עץ ה-DOM – זה התפקיד של המנתח הראשי.
גיליונות סגנונות
לעומת זאת, לגיליונות סגנונות יש מודל אחר. מבחינה רעיונית, נראה שמכיוון שגיליונות סגנונות לא משנים את עץ ה-DOM, אין סיבה להמתין להם ולעצור את ניתוח המסמך. עם זאת, יש בעיה עם סקריפטים שמבקשים מידע על סגנון במהלך שלב ניתוח המסמך. אם הסגנון עדיין לא נטען ונותח, הסקריפט יקבל תשובות שגויות, וככל הנראה זה גרם להרבה בעיות. זה נראה כמו מקרה קצה, אבל הוא די נפוץ. Firefox חוסם את כל הסקריפטים כשיש גיליון סגנונות שעדיין נטען ומנותח. WebKit חוסם סקריפטים רק כשהם מנסים לגשת למאפייני סגנון מסוימים שעשויים להיות מושפעים מגיליונות סגנונות שלא נטענו.
בניית עץ עיבוד (render tree)
בזמן שעץ ה-DOM נבנה, הדפדפן בונה עץ נוסף, עץ העיבוד. העץ הזה מורכב מאלמנטים חזותיים בסדר שבו הם יוצגו. זהו הייצוג החזותי של המסמך. המטרה של העץ הזה היא לאפשר את הצגת התוכן בסדר הנכון.
ב-Firefox, הרכיבים בעץ העיבוד נקראים "מסגרות". ב-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 שקובע איזה סוג של רכיב רינדור צריך ליצור לצומת 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" לא יופיעו בעץ (אבל רכיבים עם הרשאות גישה "hidden" יופיעו בעץ).
יש רכיבי DOM שתואמים לכמה אובייקטים חזותיים. בדרך כלל מדובר ברכיבים עם מבנה מורכב שאי אפשר לתאר באמצעות מלבן אחד. לדוגמה, לרכיב "select" יש שלושה רכיבי רינדור: אחד לאזור התצוגה, אחד לתיבת הרשימה הנפתחת ואחד לכפתור. בנוסף, אם הטקסט מחולק לכמה שורות כי הרוחב לא מספיק לשורה אחת, השורות החדשות יתווספו כרכיבי עיבוד נוספים.
דוגמה נוספת למספר מנועי עיבוד היא HTML פגום. לפי מפרט ה-CSS, רכיב מוטבע חייב להכיל רק רכיבי בלוק או רק רכיבים מוטבעים. במקרה של תוכן מעורב, ייווצרו רכיבי עיבוד אנונימיים של בלוקים כדי לעטוף את הרכיבים שמוטמעים בשורה.
חלק מהאובייקטים לעיבוד תואמים לצומת DOM, אבל לא באותו מקום בעץ. רכיבים צפים ורכיבים עם מיקום מוחלט נמצאים מחוץ לזרימה, ממוקמים בחלק אחר של העץ וממופים למסגרת האמיתית. במקום שבו הם אמורים להיות מופיע פריים של פלייסהולדר.
תהליך בניית העץ
ב-Firefox, המצגת רשומה כמאזין לעדכוני DOM.
המצגת מעבירה את יצירת המסגרת אל FrameConstructor, וה-constructor פותר את הסגנון (ראו חישוב סגנון) ויוצר מסגרת.
ב-WebKit, התהליך של פתרון הסגנון ויצירת רכיב רינדור נקרא "צירוף". לכל צומת DOM יש שיטת attach. הצירוף הוא סינכרוני, והוספת הצומת לעץ ה-DOM קוראת לקטע הקוד method "attach" של הצומת החדש.
עיבוד התגים html ו-body יוצר את השורש של עץ העיבוד.
אובייקט העיבוד ברמה הבסיסית (root) תואם למה שמכונה במפרט ה-CSS "הבלוק המכיל": הבלוק העליון ביותר שמכיל את כל הבלוקים האחרים. המימדים שלו הם אזור התצוגה: המימדים של אזור התצוגה בחלון הדפדפן.
ב-Firefox היא נקראת ViewPortFrame וב-WebKit היא נקראת RenderView.
זהו אובייקט העיבוד שהמסמך מפנה אליו.
שאר העץ נבנה כהוספה של צומתי DOM.
חישוב הסגנון
כדי לבנות את עץ העיבוד, צריך לחשב את המאפיינים החזותיים של כל אובייקט עיבוד. כדי לעשות את זה, המערכת מחשבת את מאפייני הסגנון של כל רכיב.
הסגנון כולל גיליונות סגנונות ממקורות שונים, רכיבי סגנון מוטבעים ומאפיינים חזותיים ב-HTML (כמו המאפיין "bgcolor").המאפיין האחרון מתורגם למאפייני סגנון תואמים ב-CSS.
המקורות של גיליונות הסגנונות הם גיליונות הסגנונות שמוגדרים כברירת מחדל בדפדפן, גיליונות הסגנונות שסופקו על ידי יוצר הדף וגיליונות הסגנונות של המשתמש – אלה גיליונות סגנונות שסופקו על ידי המשתמש בדפדפן (הדפדפנים מאפשרים לכם להגדיר את הסגנונות המועדפים עליכם. לדוגמה, ב-Firefox, עושים את זה על ידי הוספת גיליון סגנונות לתיקייה Firefox Profile.
חישוב הסגנון מעורר כמה קשיים:
- נתוני סגנון הם מבנה גדול מאוד, שמכיל את מאפייני הסגנון הרבים, וזה יכול לגרום לבעיות בזיכרון.
אם לא מבצעים אופטימיזציה, חיפוש הכללים התואמים לכל רכיב יכול לגרום לבעיות בביצועים. מעבר על כל רשימת הכללים של כל רכיב כדי למצוא התאמות היא משימה כבדה. לסלקטורים יכול להיות מבנה מורכב שיכול לגרום לתהליך ההתאמה להתחיל בנתיב שנראה מבטיח, אבל מתברר כחסר תועלת, ואז צריך לנסות נתיב אחר.
לדוגמה, הסלקטור המורכב הזה:
div div div div{ ... }המשמעות היא שהכללים חלים על
<div>שהוא צאצא של 3 רכיבי div. נניח שאתם רוצים לבדוק אם הכלל חל על רכיב<div>מסוים. אתם בוחרים נתיב מסוים בעץ כדי לבדוק אותו. יכול להיות שתצטרכו לעבור על עץ הצמתים רק כדי לגלות שיש רק שני רכיבי div והכלל לא חל. אחר כך צריך לנסות נתיבים אחרים בעץ.החלת הכללים כוללת כללי מדורגים מורכבים למדי שמגדירים את ההיררכיה של הכללים.
בואו נראה איך הדפדפנים מתמודדים עם הבעיות האלה:
שיתוף נתוני סגנון
צומתי WebKit מפנים לאובייקטים של סגנון (RenderStyle). בתנאים מסוימים, צמתים יכולים לשתף את האובייקטים האלה. הצמתים הם אחים או בני דודים וגם:
- הרכיבים חייבים להיות באותו מצב עכבר (לדוגמה, לא יכול להיות מצב שבו רכיב אחד נמצא במצב :hover והשני לא)
- לאף אחד מהרכיבים לא צריך להיות מזהה
- שמות התגים צריכים להיות זהים
- מאפייני הכיתה צריכים להתאים
- קבוצת המאפיינים הממופים צריכה להיות זהה
- מצבי הקישור חייבים להיות תואמים
- מצבי ההתמקדות במודעה צריכים להיות תואמים
- אף אחד מהרכיבים לא אמור להיות מושפע מסלקטורים של מאפיינים, כאשר "מושפע" מוגדר כהתאמה של סלקטור כלשהו שמשתמש בסלקטור של מאפיין במיקום כלשהו בתוך הסלקטור.
- אסור שיהיה מאפיין סגנון מוטבע ברכיבים
- אסור להשתמש בסלקטורים של אחים בכלל. WebCore פשוט מפעיל מתג גלובלי כשמתגלה סלקטור אחים, ומשבית את שיתוף הסגנון לכל המסמך כשהם קיימים. זה כולל את הסלקטור + וסלקטורים כמו :first-child ו- :last-child.
עץ הכללים של Firefox
ל-Firefox יש שני עצים נוספים שמקלים על חישוב הסגנון: עץ הכללים ועץ ההקשר של הסגנון. גם ל-WebKit יש אובייקטים של סגנון, אבל הם לא מאוחסנים בעץ כמו עץ ההקשר של הסגנון, אלא רק צומת ה-DOM מצביע על הסגנון הרלוונטי שלו.
הקשרי הסגנון מכילים ערכי סיום. הערכים מחושבים על ידי החלת כל הכללים התואמים בסדר הנכון וביצוע פעולות שמשנות אותם מערכים לוגיים לערכים קונקרטיים. לדוגמה, אם הערך הלוגי הוא אחוז מהמסך, הוא יחושב ויומר ליחידות מוחלטות. הרעיון של עץ הכללים ממש חכם. הוא מאפשר לשתף את הערכים האלה בין צמתים כדי להימנע מחישוב חוזר שלהם. זה גם חוסך מקום.
כל הכללים התואמים מאוחסנים בעץ. הצמתים התחתונים בענף הם בעדיפות גבוהה יותר. העץ מכיל את כל הנתיבים של ההתאמות לכללים שנמצאו. אחסון הכללים מתבצע באופן עצל. העץ לא מחושב בהתחלה לכל צומת, אבל בכל פעם שצריך לחשב סגנון של צומת, הנתיבים המחושבים מתווספים לעץ.
הרעיון הוא לראות את הנתיבים בעץ כמילים בלקסיקון. נניח שכבר חישבנו את עץ הכללים הזה:
נניח שאנחנו צריכים להתאים כללים לרכיב אחר בעץ התוכן, ומגלים שהכללים התואמים (בסדר הנכון) הם B-E-I. הנתיב הזה כבר קיים בעץ כי כבר חישבנו את הנתיב A-B-E-I-L. עכשיו תהיה לנו פחות עבודה.
בואו נראה איך העץ חוסך לנו עבודה.
חלוקה ל-struct
ההקשרים של הסגנון מחולקים למבנים. המבנים האלה מכילים מידע על סגנון של קטגוריה מסוימת, כמו גבול או צבע. כל המאפיינים במבנה הם מאפיינים שעוברים בירושה או מאפיינים שלא עוברים בירושה. מאפיינים שעוברים בירושה הם מאפיינים שאם הם לא מוגדרים על ידי הרכיב, הם עוברים בירושה מהרכיב הראשי שלו. מאפיינים שלא עוברים בירושה (נקראים מאפייני "איפוס") משתמשים בערכי ברירת המחדל אם הם לא מוגדרים.
העץ עוזר לנו על ידי שמירת מבנים שלמים (שמכילים את ערכי הקצה המחושבים) במטמון בעץ. הרעיון הוא שאם הצומת התחתון לא סיפק הגדרה למבנה, אפשר להשתמש במבנה שנשמר במטמון בצומת עליון.
חישוב הקשרי הסגנון באמצעות עץ הכללים
כשאנחנו מחשבים את הקשר הסגנון של רכיב מסוים, אנחנו קודם מחשבים נתיב בעץ הכללים או משתמשים בנתיב קיים. לאחר מכן אנחנו מתחילים להחיל את הכללים בנתיב כדי למלא את המבנים בהקשר הסגנון החדש שלנו. אנחנו מתחילים בצומת התחתון של הנתיב – זה עם הקדימות הגבוהה ביותר (בדרך כלל הסלקטור הספציפי ביותר) ועוברים על העץ עד שהמבנה שלנו מלא. אם אין מפרט ל-struct בצומת הכלל הזה, אנחנו יכולים לבצע אופטימיזציה משמעותית – אנחנו עולים בעץ עד שאנחנו מוצאים צומת שמציין אותו באופן מלא ומצביעים עליו – זו האופטימיזציה הטובה ביותר – ה-struct כולו משותף. כך נחסך חישוב של ערכי קצה וזיכרון.
אם אנחנו מוצאים הגדרות חלקיות, אנחנו עולים במעלה העץ עד שהמבנה מתמלא.
אם לא מצאנו הגדרות ל-struct שלנו, אז במקרה שה-struct הוא מסוג "inherited", אנחנו מפנים ל-struct של ההורה שלנו בעץ ההקשר. במקרה הזה הצלחנו גם לשתף מבנים. אם זה מבנה לאיפוס, המערכת תשתמש בערכי ברירת המחדל.
אם הצומת הספציפי ביותר מוסיף ערכים, צריך לבצע כמה חישובים נוספים כדי להמיר אותו לערכים בפועל. לאחר מכן אנחנו שומרים את התוצאה במטמון בצומת העץ כדי שצאצאים יוכלו להשתמש בה.
אם לאלמנט יש אלמנט אחים שמצביע על אותו צומת עץ, אפשר לשתף ביניהם את כל הקשר הסגנון.
בואו נראה דוגמה: נניח שיש לנו את ה-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}
כדי לפשט את הדברים, נניח שאנחנו צריכים למלא רק שני מבנים: המבנה של הצבע והמבנה של השוליים. המבנה של הצבע מכיל רק איבר אחד: הצבע. המבנה של השוליים מכיל את ארבעת הצדדים.
עץ הכללים שיתקבל ייראה כך (הצמתים מסומנים בשם הצומת: מספר הכלל שהם מצביעים עליו):
עץ ההקשר ייראה כך (שם הצומת: צומת הכלל שאליו הוא מצביע):
נניח שאנחנו מנתחים את ה-HTML ומגיעים לתג <div> השני. אנחנו צריכים ליצור הקשר סגנון לצומת הזה ולמלא את מבני הסגנון שלו.
אנחנו נתאים את הכללים ונגלה שהכללים התואמים ל-<div> הם 1, 2 ו-6.
זה אומר שכבר יש נתיב קיים בעץ שהרכיב שלנו יכול להשתמש בו, ואנחנו רק צריכים להוסיף לו עוד צומת בשביל כלל 6 (צומת F בעץ הכללים).
ניצור הקשר סגנון ונכניס אותו לעץ ההקשר. הקשר הסגנון החדש יצביע על צומת F בעץ הכללים.
עכשיו צריך למלא את מבני הסגנון. נתחיל במילוי המבנה של השוליים. מכיוון שצומת הכלל האחרון (F) לא מוסיף למבנה ה-struct של השוליים, אנחנו יכולים לעלות בעץ עד שנמצא מבנה struct שנשמר במטמון וחושב בהוספת צומת קודמת, ולהשתמש בו. נמצא אותו בצומת B, שהוא הצומת העליון ביותר שצוינו בו כללי שוליים.
יש לנו הגדרה למבנה הצבע, אז אנחנו לא יכולים להשתמש במבנה שנשמר במטמון. לצבע יש מאפיין אחד, אז אנחנו לא צריכים לעלות בעץ כדי למלא מאפיינים אחרים. אנחנו נחשב את ערך הסיום (נמיר את המחרוזת ל-RGB וכו') ונשמור במטמון את המבנה המחושב בצומת הזה.
העבודה על הרכיב השני של <span> קלה עוד יותר. אנחנו נתאים את הכללים ונגיע למסקנה שהיא מצביעה על כלל G, כמו הטווח הקודם.
מכיוון שיש לנו אחים שמצביעים על אותו צומת, אנחנו יכולים לשתף את כל הקשר הסגנון ופשוט להצביע על ההקשר של ה-span הקודם.
במבנים שמכילים כללים שעוברים בירושה מההורה, השמירה במטמון מתבצעת בעץ ההקשר (המאפיין color עובר בירושה, אבל 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 יכולה להיות מסובכת יותר. כדי לפתור את הקושי, הכללים משתנים כדי שיהיה קל יותר לגשת אליהם.
אחרי ניתוח גיליון הסגנונות, הכללים מתווספים לאחת מכמה מפות גיבוב (hash maps), בהתאם לבורר. יש מפות לפי מזהה, לפי שם כיתה, לפי שם תג ומפה כללית לכל מה שלא מתאים לקטגוריות האלה. אם הסלקטור הוא מזהה, הכלל יתווסף למפת המזהים. אם הוא מחלקה, הוא יתווסף למפת המחלקות וכו'.
השינוי הזה מקל מאוד על ההתאמה לכללים. אין צורך לחפש בכל הצהרה: אנחנו יכולים לחלץ את הכללים הרלוונטיים לאלמנט מהמפות. האופטימיזציה הזו מבטלת יותר מ-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 אם ההצהרה שממנה הוא מגיע היא מאפיין "סגנון" ולא כלל עם בורר, אחרת 0 (= א)
- סופרים את מספר מאפייני המזהה בסלקטור (= b)
- סופרים את מספר המאפיינים האחרים והמחלקות המדומות בסלקטור (= c)
- סופרים את מספר שמות הרכיבים והרכיבים המדומים בסלקטור (= d)
שרשור ארבעת המספרים a-b-c-d (במערכת מספרים עם בסיס גדול) נותן את הספציפיות.
בסיס המספרים שבו צריך להשתמש מוגדר לפי הספירה הגבוהה ביותר שיש לכם באחת הקטגוריות.
לדוגמה, אם a=14, אפשר להשתמש בבסיס הקסדצימלי. במקרה הלא סביר שבו a=17, תצטרכו בסיס מספרים של 17 ספרות. המצב השני יכול לקרות עם סלקטור כזה: html body div 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) נטענו. אם הסגנון לא נטען במלואו בזמן ההוספה, המערכת תשתמש ב-placeholders ותסמן אותם במסמך, והם יחושבו מחדש אחרי שגיליונות הסגנונות ייטענו.
פריסה
כשהרכיב לעיבוד נוצר ומתווסף לעץ, אין לו מיקום וגודל. חישוב הערכים האלה נקרא פריסה או עימוד מחדש.
HTML משתמש במודל פריסה מבוסס-זרימה, כלומר ברוב המקרים אפשר לחשב את הגיאומטריה במעבר אחד. רכיבים שמופיעים מאוחר יותר "בזרימה" בדרך כלל לא משפיעים על הגיאומטריה של רכיבים שמופיעים מוקדם יותר "בזרימה", כך שהפריסה יכולה להתבצע משמאל לימין, מלמעלה למטה לאורך המסמך. יש חריגים: לדוגמה, טבלאות HTML עשויות לדרוש יותר ממעבר אחד.
מערכת הקואורדינטות היא יחסית למסגרת הבסיסית (root). הקואורדינטות העליונות והשמאליות משמשות.
הפריסה היא תהליך רקורסיבי. הוא מתחיל ברכיב הרינדור הבסיסי, שתואם לרכיב <html> של מסמך ה-HTML. הפריסה ממשיכה באופן רקורסיבי דרך חלק מההיררכיה של המסגרת או כולה, ומחשבת מידע גיאומטרי לכל רכיב רינדור שדורש זאת.
המיקום של רכיב הרינדור הבסיסי הוא 0,0 והמימדים שלו הם אזור התצוגה – החלק הגלוי של חלון הדפדפן.
לכל רכיבי הרינדור יש שיטת "פריסה" או "התאמה", וכל רכיב רינדור מפעיל את שיטת הפריסה של הצאצאים שלו שצריכים פריסה.
מערכת ביטים מלוכלכים
כדי לא לבצע פריסה מלאה בכל שינוי קטן, דפדפנים משתמשים במערכת "dirty bit". רכיב רינדור שמשתנה או מתווסף מסמן את עצמו ואת הצאצאים שלו כ "מלוכלכים": דורשים פריסה.
יש שני דגלים: "dirty" ו-"children are dirty". המשמעות של הדגל השני היא שלמרות שהרכיב עצמו תקין, יש לו לפחות רכיב צאצא אחד שצריך פריסה.
פריסה גלובלית ומצטברת
אפשר להפעיל פריסה על כל עץ העיבוד – זו פריסה "גלובלית". זה יכול לקרות כתוצאה מהפעולות הבאות:
- שינוי סגנון גלובלי שמשפיע על כל רכיבי הרינדור, כמו שינוי גודל הגופן.
- כתוצאה משינוי גודל המסך
הפריסה יכולה להיות מצטברת, רק המעבדים המלוכלכים יפרסו (זה יכול לגרום לנזק מסוים שידרוש פריסות נוספות).
פריסה מצטברת מופעלת (באופן אסינכרוני) כשהמעבדים (renderers) לא מעודכנים. לדוגמה, כשמתווספים רכיבי רינדור חדשים לעץ הרינדור אחרי שתוכן נוסף הגיע מהרשת ונוסף לעץ ה-DOM.
פריסה אסינכרונית וסינכרונית
הפריסה המצטברת מתבצעת באופן אסינכרוני. דפדפן Firefox מוסיף לתור "פקודות הזרמה מחדש" לפריסות מצטברות, ומתזמן מפעיל את הפקודות האלה באצווה. ב-WebKit יש גם טיימר שמפעיל פריסה מצטברת – המערכת עוברת על העץ ומבצעת פריסה של רכיבי רינדור "מלוכלכים".
סקריפטים שמבקשים מידע על סגנון, כמו offsetHeight, יכולים להפעיל פריסה מצטברת באופן סינכרוני.
פריסה גלובלית תופעל בדרך כלל באופן סינכרוני.
לפעמים הפריסה מופעלת כקריאה חוזרת (callback) אחרי פריסה ראשונית, כי מאפיינים מסוימים, כמו מיקום הגלילה, השתנו.
הגדרות אופטימיזציה
כשפריסה מופעלת על ידי "שינוי גודל" או שינוי במיקום של ה-renderer(ולא בגודל), הגדלים של ה-renderer נלקחים מהמטמון ולא מחושבים מחדש…
במקרים מסוימים, רק עץ משנה משתנה והפריסה לא מתחילה מהשורש. זה יכול לקרות במקרים שבהם השינוי הוא מקומי ולא משפיע על הסביבה שלו – כמו טקסט שמוזן בשדות טקסט (אחרת כל הקשה על מקש תפעיל פריסה שמתחילה מהשורש).
תהליך הפריסה
הפריסה בדרך כלל נראית כך:
- ה-renderer של ההורה קובע את הרוחב שלו.
- ההורה עובר על הילדים ו:
- מיקום של רכיב ה-renderer הצאצא (מגדיר את ה-x וה-y שלו).
- הפונקציה קוראת לפריסת הצאצא אם צריך – אם היא מלוכלכת או אם אנחנו בפריסה גלובלית, או מסיבה אחרת – ומחשבת את הגובה של הצאצא.
- ההורה משתמש בגבהים המצטברים של הילדים ובגבהים של השוליים והריפוד כדי להגדיר את הגובה שלו – ההורה של המעבד (renderer) של ההורה ישתמש בזה.
- מגדיר את הביט המלוכלך שלו כ-False.
דפדפן Firefox משתמש באובייקט "מצב" (nsHTMLReflowState) כפרמטר לפריסה (שנקראת reflow). בין היתר, המצב כולל את רוחב ההורה.
הפלט של פריסת Firefox הוא אובייקט "מדדים" (nsHTMLReflowMetrics). הוא יכיל את הגובה המחושב של ה-renderer.
חישוב הרוחב
רוחב ה-renderer מחושב לפי רוחב בלוק המאגר, מאפיין ה-"width" של סגנון ה-renderer, השוליים והגבולות.
לדוגמה, הרוחב של ה-div הבא:
<div style="width: 30%"/>
החישוב של WebKit יהיה כזה(שיטת calcWidth של RenderBox):
- רוחב הקונטיינר הוא הערך המקסימלי של availableWidth של הקונטיינר ו-0. הערך של availableWidth במקרה הזה הוא contentWidth, שמחושב כך:
clientWidth() - paddingLeft() - paddingRight()
המאפיינים clientWidth ו-clientHeight מייצגים את החלק הפנימי של אובייקט, לא כולל הגבול ופס הגלילה.
רוחב הרכיבים הוא מאפיין הסגנון "width". הוא יחושב כערך מוחלט על ידי חישוב האחוז מרוחב הקונטיינר.
הגבולות האופקיים והמרווחים הפנימיים נוספו.
עד עכשיו זה היה החישוב של "הרוחב המועדף". עכשיו יחושבו הרוחב המינימלי והרוחב המקסימלי.
אם הרוחב המועדף גדול מהרוחב המקסימלי, המערכת תשתמש ברוחב המקסימלי. אם הוא קטן מהרוחב המינימלי (היחידה הקטנה ביותר שלא ניתנת לפיצול), המערכת משתמשת ברוחב המינימלי.
הערכים נשמרים במטמון למקרה שצריך פריסה, אבל הרוחב לא משתנה.
מעבר שורה
כשמנוע עיבוד באמצע פריסה מחליט שהוא צריך להפסיק, הוא מפסיק ומעביר את ההחלטה לרכיב ההורה של הפריסה. ההורה יוצר את ה-renderers הנוספים וקורא לפריסה שלהם.
ציור
בשלב הציור, המערכת עוברת על עץ העיבוד וקוראת ל-method "paint()" של המעבד כדי להציג תוכן במסך. הציור משתמש ברכיב התשתית של ממשק המשתמש.
גלובלי ומצטבר
כמו פריסה, גם צביעה יכולה להיות גלובלית – כל העץ נצבע – או מצטברת. בציור מצטבר, חלק מהמעבדים משתנים באופן שלא משפיע על כל העץ. המנוע ששונה מבטל את המלבן שלו במסך. כתוצאה מכך, מערכת ההפעלה רואה את זה כ "אזור מלוכלך" ויוצרת אירוע "צביעה". מערכת ההפעלה עושה את זה בצורה חכמה ומאחדת כמה אזורים לאחד. ב-Chrome זה יותר מסובך כי ה-renderer נמצא בתהליך שונה מהתהליך הראשי. Chrome מדמה את ההתנהגות של מערכת ההפעלה במידה מסוימת. התצוגה מאזינה לאירועים האלה ומעבירה את ההודעה לשורש העיבוד. המערכת עוברת על העץ עד שהיא מגיעה למעבד הרלוונטי. הוא יצייר את עצמו מחדש (ובדרך כלל גם את הצאצאים שלו).
סדר הציור
ב-CSS2 מוגדר הסדר של תהליך הציור. זהו למעשה הסדר שבו הרכיבים נערמים בהקשרי הערימה. הסדר הזה משפיע על הציור כי המחסניות מצוירות מאחור לפנים. סדר הערימה של רכיב רינדור בלוקים הוא:
- צבע הרקע
- תמונת רקע
- border
- ילדים
- outline
רשימת התצוגה של Firefox
Firefox עובר על עץ העיבוד ובונה רשימת תצוגה למלבן המצויר. הוא מכיל את רכיבי ה-renderer שרלוונטיים למלבן, בסדר הציור הנכון (הרקעים של רכיבי ה-renderer, ואז הגבולות וכו').
כך צריך לעבור על העץ רק פעם אחת לצביעה מחדש במקום כמה פעמים – צביעת כל הרקעים, אחר כך כל התמונות, אחר כך כל הגבולות וכו'.
Firefox מייעל את התהליך בכך שהוא לא מוסיף אלמנטים שיוסתרו, כמו אלמנטים שנמצאים לגמרי מתחת לאלמנטים אטומים אחרים.
אחסון מלבני של WebKit
לפני הציור מחדש, WebKit שומר את המלבן הישן כמפת סיביות. לאחר מכן, הוא מצייר רק את ההבדל בין המלבנים החדשים והישנים.
שינויים דינמיים
הדפדפנים מנסים לבצע את הפעולות המינימליות האפשריות בתגובה לשינוי. לכן, שינויים בצבע של רכיב יגרמו רק לצביעה מחדש של הרכיב. שינויים במיקום של הרכיב יגרמו לפריסה ולצביעה מחדש של הרכיב, של הצאצאים שלו ואולי גם של האחים שלו. הוספת צומת DOM תגרום לפריסה ולצביעה מחדש של הצומת. שינויים משמעותיים, כמו הגדלת הגופן של רכיב ה-HTML, יגרמו לביטול המטמון, לפריסה מחדש ולצביעה מחדש של כל העץ.
השרשורים של מנוע העיבוד
מנוע הרינדור הוא בעל שרשור יחיד. כמעט הכול, חוץ מפעולות ברשת, קורה בשרשור אחד. ב-Firefox וב-Safari, זהו ה-thread הראשי של הדפדפן. ב-Chrome, זהו ה-thread הראשי של תהליך הכרטיסייה.
אפשר לבצע פעולות ברשת באמצעות כמה שרשורים מקבילים. מספר החיבורים המקבילים מוגבל (בדרך כלל 2 עד 6 חיבורים).
לולאת אירוע
ה-thread הראשי של הדפדפן הוא לולאת אירוע. זו לולאה אינסופית ששומרת על התהליך פעיל. הוא ממתין לאירועים (כמו אירועי פריסה וצביעה) ומעבד אותם. זהו קוד Firefox ללולאת האירוע הראשית:
while (!mExiting)
NS_ProcessNextEvent(thread);
מודל חזותי של CSS2
אזור העריכה
לפי מפרט CSS2, המונח "קנבס" מתאר את "המרחב שבו מבנה העיצוב מוצג": המקום שבו הדפדפן מציג את התוכן.
ה-canvas הוא אינסופי בכל מימד של המרחב, אבל הדפדפנים בוחרים רוחב ראשוני על סמך המימדים של חלון התצוגה.
לפי www.w3.org/TR/CSS2/zindex.html, אזור הציור שקוף אם הוא נמצא בתוך אזור אחר, ואם לא, הוא מקבל צבע שמוגדר על ידי הדפדפן.
מודל תיבה ב-CSS
מודל התיבה של CSS מתאר את התיבות המלבניות שנוצרות עבור רכיבים בעץ המסמך, ומסודרות לפי מודל העיצוב החזותי.
לכל תיבה יש אזור תוכן (למשל טקסט, תמונה וכו') ואזורים אופציונליים מסביב של ריפוד, גבול ושוליים.
כל צומת יוצר 0…n תיבות כאלה.
לכל הרכיבים יש מאפיין "display" שקובע את סוג התיבה שתיווצר.
דוגמאות:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
ברירת המחדל היא inline, אבל גיליון הסגנונות של הדפדפן יכול להגדיר ברירות מחדל אחרות. לדוגמה: ברירת המחדל של התצוגה של הרכיב "div" היא בלוק.
אפשר למצוא דוגמה לגיליון סגנונות שמוגדר כברירת מחדל כאן: www.w3.org/TR/CSS2/sample.html.
סכמת מיקום
יש שלוש סכימות:
- רגיל: האובייקט ממוקם לפי המיקום שלו במסמך. כלומר, המיקום שלו בעץ העיבוד זהה למיקום שלו בעץ ה-DOM, והוא מוצג בהתאם לסוג התיבה ולמידות שלה.
- צף: האובייקט ממוקם קודם כמו בזרימה רגילה, ואז מוזז כמה שיותר שמאלה או ימינה
- מוחלט: האובייקט ממוקם בעץ העיבוד במקום שונה מאשר בעץ ה-DOM
סכימת המיקום מוגדרת על ידי המאפיין position והמאפיין float.
- static ו-relative גורמים לזרימה רגילה
- 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 האדום מופיע לפני הירוק בתגי העיצוב, והיה נצבע לפניו בתהליך הרגיל, המאפיין z-index גבוה יותר, ולכן הוא נמצא קדימה יותר בערימה שמוחזקת על ידי תיבת הבסיס.
משאבים
ארכיטקטורת הדפדפן
- Grosskurth, Alan. ארכיטקטורת עזר לדפדפני אינטרנט (PDF)
- Gupta, Vineet. איך דפדפנים פועלים – חלק 1 – ארכיטקטורה
ניתוח
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
- ריק ג'ליף. The Bold and the Beautiful: two new drafts for HTML 5.
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
- L. דיוויד בארון, Faster HTML and CSS: Layout Engine Internals for מפתחי/ות אתרים (סרטון של Google tech talk)
- L. דיוויד בארון, מנוע הפריסה של Mozilla
- L. דיוויד בארון, Mozilla Style System Documentation
- כריס ווטרסון, הערות בנושא עימוד מחדש של HTML
- כריס ווטרסון, סקירה כללית של Gecko
- אלכסנדר לרסון, החיים של בקשת HTTP ב-HTML
WebKit
- דייוויד הייאט, Implementing CSS(part 1)
- David Hyatt, An Overview of WebCore
- דיוויד הייאט, רינדור WebCore
- דיוויד הייאט, The FOUC Problem
מפרטי W3C
הוראות ליצירת דפדפנים
תרגומים
הדף הזה תורגם ליפנית פעמיים:
- How Browsers Work - Behind the Scenes of Modern Web Browsers (ja) מאת @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 by @ikeike443 and @kiyoto01.
אתם יכולים לראות את התרגומים שמתארחים חיצונית בקוריאנית ובטורקית.
תודה לכולם!