איך דפדפנים פועלים

מאחורי הקלעים של דפדפני אינטרנט מודרניים

Tali Garsiel
Tali Garsiel

הקדמה

הסקירה המקיפה הזו על הפעילות הפנימית של WebKit וגקו היא תוצאה של מחקר נרחב שבוצעה על ידי השחקנית הישראלית טלי גרסיל. במשך כמה שנים, היא בדקה את כל הנתונים שפורסמו לגבי רכיבים פנימיים של הדפדפן והקדישה זמן רב לקריאת קוד המקור של דפדפן האינטרנט. היא כתבה:

כמפתחי אתרים, ההיכרות עם ההיבטים הפנימיים של תפעול דפדפן תעזור לכם לקבל החלטות טובות יותר ולדעת את ההצדקה שעומדת מאחורי שיטות מומלצות לפיתוח. למרות שמדובר במסמך ארוך למדי, מומלץ להקדיש זמן כדי להתעמק בנושא. ככה עדיף.

פול אירי, צוות קשרי מפתחים ב-Chrome

מבוא

דפדפני אינטרנט הם התוכנות הנפוצות ביותר. בפריימר הזה נסביר איך הם פועלים מאחורי הקלעים. נראה מה קורה כשמקלידים google.com בסרגל הכתובות עד שמופיע הדף של Google במסך הדפדפן.

דפדפנים שנדבר עליהם

במחשב השולחני משתמשים כיום בחמישה דפדפנים עיקריים: Chrome, Internet Explorer, Firefox, Safari ו-Opera. בנייד, הדפדפנים העיקריים הם דפדפן Android, iPhone, Opera Mini ו-Opera Mobile, דפדפן UC, דפדפני 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) ארגון, שהוא ארגון התקנים של האינטרנט. במשך שנים, דפדפנים פעלו בהתאם רק לחלק מהמפרטים ופיתחו תוספים משלהם. הדבר גרם לבעיות תאימות חמורות עבור מחברי אתרים. כיום, רוב הדפדפנים תואמים פחות או יותר למפרטים.

לממשקי המשתמש של הדפדפן יש הרבה במשותף. בין הרכיבים הנפוצים בממשק המשתמש:

  1. סרגל הכתובות להוספת URI
  2. לחצנים למעבר קדימה ואחורה
  3. אפשרויות להוספה לסימניות
  4. לחצני רענון ועצירה לרענון או להפסקת הטעינה של מסמכים נוכחיים
  5. לחצן הבית שמעביר אתכם לדף הבית

למרבה הפלא, ממשק המשתמש של הדפדפן אינו מצוין באף מפרט רשמי, אלא רק משיטות עבודה מומלצות שנוצרו לאורך שנים של ניסיון ועל ידי דפדפנים שמחקים זה את זה. מפרט ה-HTML5 אינו מגדיר רכיבי ממשק משתמש שהדפדפן חייב, אבל מפרט כמה רכיבים נפוצים. הכתובות האלה כוללות את סרגל הכתובות, שורת הסטטוס וסרגל הכלים. כמובן שיש תכונות ייחודיות לדפדפן מסוים, כמו מנהל ההורדות של Firefox.

תשתית ברמה גבוהה

הרכיבים העיקריים בדפדפן הם:

  1. ממשק המשתמש: כולל סרגל הכתובות, הלחצן 'הקודם/הבא', תפריט הסימניות וכו'. הצגת כל חלק בדפדפן מלבד החלון שבו מופיע הדף המבוקש.
  2. מנוע הדפדפן: ממזג את הפעולות בין ממשק המשתמש למנוע הרינדור.
  3. מנוע הרינדור: אחראי להצגת התוכן המבוקש. לדוגמה, אם התוכן המבוקש הוא HTML, מנוע העיבוד מנתח את ה-HTML ואת ה-CSS ומציג את התוכן שנותח במסך.
  4. Networking: לקריאות רשת כמו בקשות HTTP, שימוש בהטמעות שונות לפלטפורמות שונות מאחורי ממשק שאינו תלוי פלטפורמה.
  5. קצה עורפי של ממשק המשתמש: משמש לציור ווידג'טים בסיסיים כמו תיבות וחלונות משולבים. הקצה העורפי חושף ממשק כללי שלא ספציפי לפלטפורמה. מתחתיו נעשה שימוש בשיטות של ממשק משתמש של מערכת ההפעלה.
  6. JavaScript interpreter – משמש לניתוח ולהפעלה של קוד JavaScript.
  7. אחסון נתונים. זוהי שכבת עקביות. ייתכן שהדפדפן יצטרך לשמור את כל סוגי הנתונים באופן מקומי, כמו קובצי Cookie. הדפדפנים תומכים גם במנגנוני אחסון כמו localStorage, IndexedDB, WebSQL ו-FileSystem.
רכיבי דפדפן
איור 1: רכיבי הדפדפן

חשוב לציין שדפדפנים כמו 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.

לאחר מכן, זהו התהליך הבסיסי של מנוע העיבוד:

תהליך בסיסי של מנוע רינדור
איור 2: התהליך הבסיסי של מנוע העיבוד

מנגנון העיבוד יתחיל לנתח את מסמך ה-HTML ולהמיר רכיבים לצומתי DOM בעץ שנקרא 'עץ התוכן'. המנוע ינתח את נתוני הסגנון, גם בקובצי CSS חיצוניים וגם ברכיבי סגנון. עיצוב המידע ביחד עם הוראות חזותיות ב-HTML ישמש ליצירת עץ נוסף: עץ העיבוד.

עץ העיבוד מכיל מלבנים עם מאפיינים חזותיים כמו צבע ומידות. המלבנים מוצגים בסדר הנכון במסך.

לאחר בניית עץ העיבוד, הוא עובר תהליך "פריסה". במילים אחרות, עליך לספק לכל צומת את הקואורדינטות המדויקות שבהן הוא אמור להופיע במסך. השלב הבא הוא ציור - נעבור על עץ העיבוד וכל צומת ייצבע באמצעות שכבת הקצה העורפי של ממשק המשתמש.

חשוב להבין שמדובר בתהליך הדרגתי. כדי לשפר את חוויית המשתמש, מנגנון העיבוד ינסה להציג תוכן במסך בהקדם האפשרי. הוא לא ימתין עד שכל ה-HTML ינותח לפני שתתחיל בבנייה ובפריסה של עץ העיבוד. חלקים מהתוכן ינותחו ויוצגו, והתהליך יימשך עם שאר התוכן שיגיע מהרשת.

דוגמאות לתהליך ראשי

תהליך ראשי של WebKit.
איור 3: התהליך הראשי של WebKit
תהליך ראשי של מנוע עיבוד Gecko של Mozilla.
איור 4: התהליך הראשי של מנוע העיבוד Gecko של Mozilla

במספרים 3 ו-4 ניתן לראות שאמנם WebKit וגקו משתמשים במונחים מעט שונים, אבל הזרימה זהה.

שממית קוראת לעץ של רכיבים בעיצוב חזותי "עץ מסגרת". כל רכיב הוא מסגרת. WebKit משתמש במונח "Render Tree" (עץ עיבוד) ומכיל "Render Objects" (אובייקטים של עיבוד). WebKit משתמשת במונח "פריסה" לצורך הצבת רכיבים, בעוד Gecko קוראת לו "Reflow". קובץ מצורף הוא המונח של WebKit לחיבור צומתי DOM ומידע חזותי כדי ליצור את עץ העיבוד. הבדל קל שאינו סמנטי הוא שבשממית יש שכבה נוספת בין ה-HTML לעץ ה-DOM. הוא נקרא 'כיור תוכן' והוא מפעל לייצור רכיבי DOM. נדבר על כל חלק בזרימה:

ניתוח - כללי

מאחר שניתוח הוא תהליך משמעותי מאוד במנוע העיבוד, נבחן אותו קצת יותר לעומק. נתחיל במבוא קטן בנושא ניתוח.

ניתוח מסמך פירושו תרגום למבנה שהקוד יכול להשתמש בו. תוצאת הניתוח היא בדרך כלל עץ של צמתים שמייצגים את מבנה המסמך. הוא נקרא עץ ניתוח או עץ תחביר.

לדוגמה, ניתוח הביטוי 2 + 3 - 1 יכול להחזיר את העץ הבא:

צומת עץ של ביטוי מתמטי.
איור 5: צומת עץ של ביטויים מתמטיים

דקדוק

הניתוח מבוסס על כללי התחביר שהמסמך מציית להם: השפה או הפורמט שבהם הוא נכתב. כל פורמט שניתן לנתח חייב להיות בעל דקדוק דטרמיניסטי שכולל אוצר מילים וכללי תחביר. השיטה הזו נקראת דקדוק ללא הקשר. שפות אנושיות אינן שפות כאלה ולכן לא ניתן לנתח אותן באמצעות טכניקות ניתוח קונבנציונליות.

מנתח - שילוב של Lexer

ניתן להפריד את הניתוח לשני תהליכי משנה: ניתוח מילוני וניתוח תחביר.

ניתוח מילוני הוא התהליך של פיצול הקלט לאסימונים. האסימונים הם אוצר המילים של השפה: אוסף של אבני בניין תקינות. בשפה אנושית, המקור יכלול את כל המילים שמופיעות במילון של השפה הזו.

ניתוח תחביר הוא היישום של כללי התחביר של השפה.

מנתחים בדרך כלל מחלקים את העבודה בין שני רכיבים: ה-lexer (שבחלק מהמקרים נקרא Tokenizer) שאחראי לפיצול הקלט לאסימונים תקפים, והמנתח שאחראי על בניית עץ הניתוח על ידי ניתוח של מבנה המסמך בהתאם לכללי התחביר של השפה.

הוא יודע איך להסיר תווים לא רלוונטיים כמו רווחים ומעברי שורה.

ממסמך המקור לניתוח עצים
איור 6: ממסמך המקור לניתוח עצים

תהליך הניתוח הוא איטרטיבי. בדרך כלל המנתח יבקש מה-lexer אסימון חדש וינסה להתאים את האסימון לאחד מכללי התחביר. אם יש התאמה לכלל, צומת שמתאים לאסימון יתווסף לעץ הניתוח והמנתח יבקש אסימון נוסף.

אם אין אף כלל תואם, המנתח יאחסן את האסימון באופן פנימי וימשיך לבקש אסימונים עד שיימצא כלל שתואם לכל האסימונים שמאוחסנים באופן פנימי. אם לא נמצא כלל, המנתח יציג חריגה. המשמעות היא שהמסמך לא היה חוקי והכיל שגיאות תחביר.

תרגום

במקרים רבים עץ הניתוח הוא לא המוצר הסופי. לעיתים קרובות משתמשים בניתוח בתרגום: מתבצע טרנספורמציה של מסמך הקלט לפורמט אחר. דוגמה לכך היא הידור. המהדר שיוצר קוד מקור לקוד מכונה מנתח אותו תחילה לעץ ניתוח ולאחר מכן מתרגם את העץ למסמך קוד מכונה.

תהליך ההידור
איור 7: תהליך ההידור

דוגמה לניתוח

בדוגמה 5 בנינו עץ ניתוח מביטוי מתמטי. ננסה להגדיר שפה מתמטית פשוטה ונראה את תהליך הניתוח.

תחביר:

  1. אבני הבניין של תחביר השפה הן ביטויים, מונחים ופעולות.
  2. השפה שלנו יכולה לכלול כל מספר של ביטויים.
  3. ביטוי מוגדר כ "מונח" ואחריו "פעולה" ואחריה איבר אחר
  4. פעולה היא אסימון פלוס או אסימון מינוס
  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 כביטוי (תהליך הזיהוי של הביטוי ישתנה בהתאם לשאר הכללים, אבל נקודת ההתחלה היא הכלל ברמה הגבוהה ביותר).

המנתח מלמטה למעלה יסרוק את הקלט עד להתאמה לכלל. לאחר מכן הפונקציה תחליף את הקלט התואם בכלל. הקוד הזה ימשיך לפעול עד שיסתיים הקלט. הביטוי התואם חלקית ממוקם במקבץ של המנתח.

מוערם קלט
1 - 2 + 3
term + 3 - 1
פעולה לתקופה 1 - 3
ביטוי - 1
פעולת ביטוי 1
ביטוי -

מנתח מסוג זה של מלמטה למעלה נקרא מנתח shift-הפחתה, כי הקלט מועבר ימינה (נניח שהמצביע מצביע קודם בתחילת הקלט ועובר ימינה) והוא מוקטן בהדרגה לכללי תחביר.

המערכת יוצרת מנתחים באופן אוטומטי

יש כלים שיכולים ליצור מנתח. מזינים את הדקדוק של השפה – כללי התחביר ואוצר המילים שלה – והם יוצרים מנתח פעיל. כדי ליצור מנתח, צריך הבנה עמוקה בניתוח ולא קל ליצור באופן ידני מנתח שעבר אופטימיזציה, לכן מחוללי הניתוחים יכולים להיות שימושיים מאוד.

ב-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 - XHTML - אז מה ההבדל הגדול?

ההבדל הוא שגישת ה-HTML 'סלחנית' יותר: היא מאפשרת לך להשמיט תגים מסוימים (שלאחר מכן נוספים באופן מרומז), או לפעמים להשמיט תגי התחלה או סיום וכן הלאה. ככלל, מדובר בתחביר "רך", בניגוד לתחביר הכבד והתובעני של XML.

הפרט הזה נראה קטן בלבד, ומשנה את גבולות העולם. מצד אחד, זו הסיבה העיקרית לכך ש-HTML כל כך פופולרי: הוא סולח על הטעויות ומקל על מחבר האינטרנט. מצד שני, הוא מקשה על כתיבת דקדוק רשמי. לכן לסיכום, לא ניתן לנתח בקלות את ה-HTML באמצעות מנתחים רגילים, מכיוון שהדקדוק שלו אינו חופשי מהקשר. אין אפשרות לנתח HTML על ידי מנתחי XML.

DTD של HTML

הגדרת HTML היא בפורמט DTD. הפורמט הזה משמש להגדרת השפות של משפחת ה-SGML. הפורמט מכיל הגדרות לכל הרכיבים המותרים, המאפיינים וההיררכיה שלהם. כפי שראינו קודם, ה-HTML DTD אינו יוצר דקדוק ללא הקשר.

יש כמה וריאציות של ה-DTD. המצב המחמיר תואם אך ורק למפרטים, אך מצבים אחרים מכילים תמיכה בסימון שבו השתמשו דפדפנים בעבר. המטרה היא תאימות לאחור עם תוכן ישן. ה-DTD המחמיר הנוכחי נמצא כאן: www.w3.org/TR/html4/strict.dtd

DOM

עץ הפלט ("עץ הניתוח") הוא עץ של צומתי מאפיינים ואלמנטים של DOM. DOM הוא קיצור של Document Object Model. זוהי תצוגת האובייקט של מסמך ה-HTML והממשק של רכיבי HTML לעולם החיצוני, כמו JavaScript.

שורש העץ הוא האובייקט מסמך.

ל-DOM יש קשר כמעט אחד-לאחד לתגי העיצוב. למשל:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

תגי העיצוב האלה יתורגמו לעץ ה-DOM הבא:

עץ DOM של תגי העיצוב לדוגמה
איור 8: עץ 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 באמצעות המנתחים הרגילים מסוג 'למעלה-למטה' או 'למטה למעלה'.

הסיבות לכך הן:

  1. האופי הסלחני של השפה.
  2. העובדה שלדפדפנים יש סבילות מסורתית לשגיאות כדי לתמוך במקרים מוכרים של HTML לא חוקי.
  3. תהליך הניתוח הוא כניסה חוזרת. בשפות אחרות, המקור לא משתנה במהלך הניתוח, אבל ב-HTML, קוד דינמי (כמו רכיבי סקריפט שמכילים קריאות ל-document.write()) יכול להוסיף אסימונים נוספים, כך שתהליך הניתוח משנה בפועל את הקלט.

אין אפשרות להשתמש בטכניקות הניתוח הרגילות. דפדפנים יוצרים מנתחים מותאמים אישית לניתוח HTML.

אלגוריתם הניתוח מתואר בפירוט באמצעות מפרט HTML5. האלגוריתם מורכב משני שלבים: המרה לאסימונים ובניית עצים.

האסימונים הם הניתוח המילוני, תוך ניתוח הקלט לאסימונים. יש בין אסימוני ה-HTML תגי התחלה, תגי סיום, שמות מאפיינים וערכי מאפיינים.

הממיר מזהה את האסימון, נותן אותו לבונה העץ, משתמש בתו הבא לצורך זיהוי האסימון הבא, וכן הלאה עד סוף הקלט.

תהליך ניתוח HTML (נלקח ממפרט של HTML5)
איור 9: תהליך ניתוח HTML (נלקח ממפרט של HTML5)

האלגוריתם של ההמרה לאסימונים

הפלט של האלגוריתם הוא אסימון HTML. האלגוריתם מבוטא כמכונת מצבים. כל מצב צורכת תו אחד או יותר של זרם הקלט ומעדכנת את המצב הבא בהתאם לתווים האלה. ההחלטה מושפעת ממצב האסימונים הנוכחי וממצב בניית העצים. המשמעות היא שאותו תו שנצרך יניב תוצאות שונות עבור המצב הבא הנכון, בהתאם למצב הנוכחי. האלגוריתם מורכב מכדי לתאר אותו באופן מלא, ולכן נראה דוגמה פשוטה שתעזור לנו להבין את העיקרון.

דוגמה בסיסית – יצירת אסימונים ל-HTML הבא:

<html>
  <body>
    Hello world
  </body>
</html>

המצב הראשוני הוא 'מצב הנתונים'. כשמשתמשים בתו <, המצב משתנה ל'מצב פתוח של התג'. שימוש בתו a-z גורם ליצירה של 'אסימון תג התחלתי', המצב משתנה ל'מצב שם התג'. המצב הזה יישאר במצב הזה עד שהתו > ינוצל. כל תו מצורף לשם של האסימון החדש. במקרה שלנו האסימון שנוצר הוא אסימון html.

כשמגיעים לתג >, נוצר האסימון הנוכחי והמצב משתנה חזרה ל'מצב הנתונים'. המערכת תטפל בתג <body> לפי אותם שלבים. עד עכשיו הוסרו התגים html ו-body. חזרנו עכשיו ל"מצב הנתונים". שימוש בתו H של Hello world יגרום ליצירה ולפלט של אסימון תו, והוא יימשך עד < של </body>. אנחנו נפלט אסימון תו לכל תו של Hello world.

חזרנו עכשיו ל'מצב פתוח של תגים'. שימוש בקלט הבא, /, יגרום ליצירה של end tag token ולמעבר אל 'מצב שם התג'. שוב נישאר במצב הזה עד שנגיע אל >.לאחר מכן יופק אסימון התג החדש ונחזור אל "מצב הנתונים". הקלט של </html> יטופל כמו המקרה הקודם.

יצירת אסימון לקלט לדוגמה
איור 10: הוספת אסימון (טוקניזציה) לקלט לדוגמה

אלגוריתם לבניית עצים

בזמן יצירת המנתח, נוצר אובייקט המסמך. במהלך יצירת העץ, עץ ה-DOM שבו המסמך נמצא ברמה הבסיסית ישתנה ואלמנטים יתווספו אליו. כל צומת שמפיק האסימון יעובד על ידי בנאי העצים. לכל אסימון, המפרט מגדיר איזה רכיב DOM רלוונטי לו והוא ייווצר בשביל האסימון הזה. הרכיב מתווסף לעץ ה-DOM וגם לערימת הרכיבים הפתוחים. המקבץ הזה משמש לתיקון אי-התאמות בהצבת תגים ותגים לא סגורים. האלגוריתם מתואר גם כמכונת מצב. המצבים נקראים 'מצבי הכנסה'.

נבחן את תהליך בניית העצים בקלט לדוגמה:

<html>
  <body>
    Hello world
  </body>
</html>

הקלט לשלב בניית העצים הוא רצף של אסימונים משלב ההמרה. המצב הראשון הוא "מצב ראשוני". קבלת אסימון 'html' תגרום למעבר למצב 'לפני html' ולעיבוד מחדש של האסימון במצב הזה. פעולה זו תגרום ליצירת הרכיב HTMLHtmlElement, שיצורף לאובייקט השורש של המסמך.

המצב ישתנה ל-'לפני head'. לאחר מכן מתקבל אסימון ה-"body". רכיב HTMLHeadElement ייווצר באופן מרומז למרות שאין לנו אסימון "head" והוא יתווסף לעץ.

כעת אנו עוברים למצב "בראש" ואז למצב "אחרי". אסימון הגוף מעובד מחדש, המערכת יוצרת ומוכנסת רכיב HTMLBodyElement והמצב מועבר אל "in body".

אסימוני התווים במחרוזת "Hello world" מתקבלים עכשיו. התו הראשון יגרום ליצירה ולהוספה של צומת 'טקסט' והתווים האחרים יצורפו לצומת הזה.

קבלה של אסימון ה-body תוביל להעברה למצב "after body". עכשיו נקבל את תג הסיום ב-HTML שיעביר אותנו למצב "after after body". קבלת אסימון סוף הקובץ תגרום לסיום הניתוח.

בניית עצים של HTML לדוגמה.
איור 11: בניית עץ של HTML לדוגמה

פעולות בסיום הניתוח

בשלב זה הדפדפן יסמן את המסמך כאינטראקטיבי ויתחיל לנתח סקריפטים שנמצאים במצב "נדח": אלו שצריך לבצע לאחר ניתוח המסמך. לאחר מכן מצב המסמך יוגדר כ'הושלם' ואירוע 'טעינה' יופעל.

במפרט ה-HTML5 אפשר לראות את האלגוריתמים המלאים ליצירת אסימונים ובניית עצים.

סובלנות לשגיאות של דפדפנים

אף פעם לא מתקבלת שגיאת "תחביר לא חוקי" בדף HTML. דפדפנים מתקנים תוכן לא תקין וממשיכים.

ניקח לדוגמה את קוד ה-HTML הבא:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

כנראה שהפרתי כמיליון כללים ("mytag" אינו תג רגיל, קינון שגוי של רכיבי "p" ו-"div" ועוד), אבל הדפדפן עדיין מציג אותו נכון ואינו מתלונן. לכן, חלק גדול מקוד המנתח מתקן את השגיאות של מחבר ה-HTML.

הטיפול בשגיאות הוא עקבי למדי בדפדפנים, אבל למרבה הפלא, הוא לא נכלל במפרטי ה-HTML. כמו הוספת סימניות ולחצני 'הקודם' ו'הבא', זה משהו שהתפתח בדפדפנים במשך השנים. יש מבני HTML לא חוקיים ידועים שחוזרים על עצמם באתרים רבים, והדפדפנים מנסים לתקן אותם באופן שתואם לדפדפנים אחרים.

מפרט HTML5 אכן מגדיר חלק מהדרישות האלה. (WebKit מסכם זאת יפה בהערה בתחילת המחלקה של מנתח ה-HTML).

המנתח מנתח את הקלט שעבר המרה לאסימונים במסמך, וכך בונה את עץ המסמכים. אם המסמך מעוצב בצורה תקינה, ניתן לנתח אותו בקלות.

לצערנו, עלינו לטפל במסמכי HTML רבים שאינם בעלי מבנה תקין, ולכן המנתח חייב להיות סובלני משגיאות.

אנחנו צריכים לטפל לפחות בתנאי השגיאה הבאים:

  1. אסור באופן מפורש להוסיף אלמנט בתוך תג חיצוני כלשהו. במקרה כזה עלינו לסגור את כל התגים עד לתג שמונע על ידי המפתח, ולהוסיף אותו לאחר מכן.
  2. אין לנו הרשאה להוסיף את הרכיב ישירות. יכול להיות שמי שכותב את המסמך שכח תג כלשהו ביניהם (או שהתג באמצע הוא אופציונלי). ייתכן שזה המצב עם התגים הבאים: HTML HEAD BODY TBODY TR TD LI (האם שכחתי מישהו?).
  3. אנחנו רוצים להוסיף רכיב חסימה בתוך רכיב מוטבע. סגירת כל הרכיבים המוטבעים עד לאלמנט הבלוק הבא.
  4. אם זה לא עוזר, אפשר לסגור רכיבים עד שנאפשר להוסיף אותם, או להתעלם מהתג.

הנה כמה דוגמאות לסבילות לשגיאות 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" הוא קיצור של מזהה, כמו שם מחלקה. "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 הבאות.

מנתח WebKit CSS

WebKit משתמש במחוללי מנתחים של Flex and Bison כדי ליצור מנתחים באופן אוטומטי מקובצי הדקדוק של CSS. כפי שזוכרים מהמבוא של המנתח, Bison יוצר מנתח shift-Decrease-Decrease מלמטה. Firefox משתמש במנתח מלמעלה למטה שנכתב באופן ידני. בשני המקרים, כל קובץ CSS מנותח לאובייקט StyleSheet. כל אובייקט מכיל כללי CSS. האובייקטים של כלל ה-CSS מכילים אובייקטים מסוג סלקטור והצהרה ואובייקטים אחרים שתואמים לדקדוק של CSS.

מנתח CSS.
איור 12: ניתוח CSS

סדר העיבוד של סקריפטים וגיליונות סגנונות

סקריפטים

המודל של האינטרנט הוא סינכרוני. המחברים מצפים שסקריפטים ינותחו ויופעלו מייד כשהמנתח מגיע לתג <script>. ניתוח המסמך נעצר עד שהסקריפט מופעל. אם הסקריפט חיצוני, תחילה יש לאחזר את המשאב מהרשת. פעולה זו מבוצעת גם באופן סינכרוני, ולאחר מכן ניתוח השהיות עד שהמשאב מאוחזר. זה היה הדגם במשך שנים רבות, והוא מצוין גם במפרטים של HTML4 ו-5. מחברים יכולים להוסיף את המאפיין 'defer' לסקריפט. במקרה כזה, הוא לא יעצור את ניתוח המסמך ויבוצע לאחר ניתוח המסמך. HTML5 מוסיף אפשרות לסמן את הסקריפט כאסינכרוני כדי שהוא ינותח ויבוצע על ידי שרשור אחר.

ניתוח ספקולטיבי

האופטימיזציה הזו מתבצעת גם ב-WebKit וגם ב-Firefox. בזמן ביצוע סקריפטים, thread אחר מנתח את שאר המסמך ומגלה אילו משאבים אחרים צריך לטעון מהרשת וטוען אותם. כך ניתן לטעון משאבים בחיבורים מקבילים ולשפר את המהירות הכוללת. הערה: המנתח הספקולטיבי מנתח רק הפניות למשאבים חיצוניים כמו סקריפטים חיצוניים, גיליונות סגנונות ותמונות: הוא לא משנה את עץ ה-DOM – שנותר למנתח הראשי.

גיליונות סגנונות

לעומת זאת, לגיליונות סגנונות יש מודל שונה. באופן עקרוני, נראה שגיליונות סגנונות לא משנים את עץ ה-DOM, ולכן אין סיבה להמתין להם ולהפסיק את ניתוח המסמך. עם זאת, יש בעיה בסקריפטים שמבקשים פרטי סגנון במהלך שלב ניתוח המסמך. אם הסגנון עדיין לא נטען ונותח, הסקריפט יקבל תשובות שגויות ונראה שזה גרם להרבה בעיות. נראה שזה מקרה קצה, אבל הוא די נפוץ. Firefox חוסם את כל הסקריפטים כשיש גיליון סגנונות שעדיין נטען ומנתח. WebKit חוסם סקריפטים רק כשהם מנסים לגשת למאפייני סגנון מסוימים שעשויים להיות מושפעים מגיליונות סגנונות שלא נטענו.

עיבוד בניית עצים

במהלך הבנייה של עץ ה-DOM, הדפדפן בונה עץ נוסף, שהוא עץ העיבוד. עץ זה מורכב מאלמנטים חזותיים בסדר שבו הם יוצגו. זהו הייצוג החזותי של המסמך. מטרת העץ הזה היא לאפשר לצייר את התוכן בסדר הנכון.

Firefox קורא לרכיבים בעץ העיבוד "frames". WebKit משתמש במונח של Renderer או אובייקט עיבוד.

רינדור יודע איך לפרוס את עצמו ולצייר את עצמו ואת הילדים שלו.

מחלקה 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, בהתאם למאפיין display:

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". בנוסף, רכיבים שהערך שלהם לתצוגה הוקצה כ'ללא' לא יופיעו בעץ (לעומת זאת, רכיבים עם חשיפה 'מוסתרת' יופיעו בעץ).

יש רכיבי DOM שתואמים לכמה אובייקטים ויזואליים. בדרך כלל מדובר ברכיבים בעלי מבנה מורכב שלא ניתן לתאר באמצעות מלבן אחד. לדוגמה, לאלמנט "select" יש שלושה כלי רינדור: אחד לאזור התצוגה, אחד לתיבת הרשימה הנפתחת ואחת ללחצן. כמו כן, כשהטקסט מחולק למספר שורות כי הרוחב לא מספיק לשורה אחת, השורות החדשות יתווספו כרכיבי רינדור נוספים.

דוגמה נוספת לכמה כלי עיבוד הוא HTML לא תקין. בהתאם למפרט ה-CSS, רכיב מוטבע חייב להכיל רק רכיבי חסימה או רק רכיבים מוטבעים. במקרה של תוכן מעורב, ייווצרו מעבדי בלוקים אנונימיים כדי 'לעטוף את האלמנטים המוטבעים'.

חלק מאובייקטי העיבוד תואמים לצומת DOM אבל לא נמצאים באותו מקום בעץ. רכיבים צפים ורכיבים במיקום מוחלט יוצאים מהזרימה, ממוקמים בחלק אחר של העץ וממופים למסגרת האמיתית. מסגרת placeholder היא המקום שבו הם היו אמורים להופיע.

עץ העיבוד ועץ ה-DOM התואם.
איור 13: עץ העיבוד ועץ ה-DOM התואם. 'אזור התצוגה' הוא הבלוק הראשוני שמכיל את הקטע. ב-WebKit זה יהיה האובייקט RenderView

הזרימה של בניית העץ

ב-Firefox, המצגת רשומה כהאזנה לעדכוני DOM. המצגת מאציל את יצירת המסגרות ל-FrameConstructor והבונה מנתח סגנון (ראו חישוב סגנון) ויוצר מסגרת.

ב-WebKit, תהליך פתרון הסגנון ויצירת כלי לעיבוד נקרא 'צירוף קובץ'. לכל צומת DOM יש שיטת 'צירוף'. הקובץ המצורף הוא סינכרוני. הוספת צומת לעץ ה-DOM קוראת לשיטת 'צירוף' של הצומת החדשה.

עיבוד של תגי ה-HTML ותגי body גורם לבניית השורש של עץ העיבוד. אובייקט העיבוד הבסיסי (root) תואם למה שהמפרט של CSS קורא לבלוק המכיל: הבלוק העליון שמכיל את כל הבלוקים האחרים. המידות שלו הם אזור התצוגה: מידות אזור התצוגה של חלון הדפדפן. Firefox קורא לו ViewPortFrame ו-WebKit קורא לו RenderView. זהו אובייקט העיבוד שהמסמך מפנה אליו. שאר העץ בנוי כהוספה של צומתי DOM.

מפרט CSS2 במודל העיבוד.

חישוב סגנון

כדי לבנות את עץ העיבוד, צריך לחשב את המאפיינים החזותיים של כל אובייקט עיבוד. כדי לעשות זאת, מחשבים את תכונות הסגנון של כל רכיב.

הסגנון כולל גיליונות סגנונות ממקורות שונים, רכיבי סגנון מוטבעים ומאפיינים חזותיים ב-HTML (כמו המאפיין "bgcolor").בשלב מאוחר יותר, האפשרות תתורגם למאפייני סגנון CSS תואמים.

המקורות של גיליונות סגנונות הם גיליונות ברירת המחדל של הדפדפן, גיליונות הסגנונות שמסופקים על ידי מחבר הדף וגיליונות הסגנונות של המשתמש - אלו הם גיליונות סגנונות שסופקו על ידי המשתמש בדפדפן (דפדפנים מאפשרים לך להגדיר את הסגנונות המועדפים עליך. ב-Firefox, למשל, ניתן לעשות זאת על ידי הצבת גיליון סגנונות בתיקייה "Firefox Profile").

חישוב הסגנון מעלה מספר קשיים:

  1. נתוני סגנון הם מבנה גדול מאוד שמכיל את מאפייני הסגנון רבים, ולכן עשויים לגרום לבעיות זיכרון.
  2. איתור הכללים התואמים של כל רכיב עלול לגרום לבעיות בביצועים אם הוא לא עבר אופטימיזציה. מעבר על רשימת הכללים המלאה של כל רכיב כדי למצוא התאמות היא משימה כבדה. לסלקטורים יכול להיות מבנה מורכב שעלול לגרום לתהליך ההתאמה להתחיל במסלול מבטיח לכאורה, שמוכח כלא מוצלח וצריך לנסות דרך אחרת.

    לדוגמה, באמצעות הבורר המורכב הבא:

    div div div div{
    ...
    }
    

    כלומר, הכללים חלים על <div> שהוא הצאצא של 3 divs. נניח שאתם רוצים לבדוק אם הכלל חל על רכיב <div> נתון. לצורך בדיקה, בוחרים נתיב מסוים בעץ. ייתכן שיהיה עליך להעביר את עץ הצמתים למעלה רק כדי לגלות שיש רק שני divs ושהכלל לא חל. לאחר מכן תצטרכו לנסות נתיבים אחרים בעץ.

  3. החלת הכללים כוללת כללים מורכבים למדי של סולם שמגדירים את ההיררכיה של הכללים.

נראה כיצד הדפדפנים מתמודדים עם הבעיות הבאות:

נתוני הסגנון משותפים

צומתי WebKit מפנים לאובייקטים של סגנון (RenderStyle). תנאים מסוימים יכולים לשתף את האובייקטים האלה באמצעות צמתים. הצמתים הם אחים או בני דודים, וגם:

  1. הרכיבים חייבים להיות באותו מצב עכבר (לדוגמה, אחד מהם לא יכול להיות ב-:hover בזמן שהאחר לא)
  2. לאף אחד מהאלמנטים לא יכול להיות מזהה
  3. שמות התגים צריכים להיות זהים
  4. מאפייני הכיתה צריכים להיות זהים
  5. קבוצת המאפיינים הממופים חייבת להיות זהה
  6. מצבי הקישור חייבים להתאים
  7. מצבי המיקוד חייבים להתאים
  8. בוררי מאפיינים לא אמורים להשפיע על אף אחד מהרכיבים, כאשר בוררי המאפיינים מושפעים מכך שיש להם התאמה כלשהי של סלקטור שמשתמשת בבורר מאפיינים בכל מיקום שהוא בתוך הבורר.
  9. אסור לכלול מאפייני סגנון בתוך השורה ברכיבים
  10. אסור להשתמש בבוררים-אחים. WebCore פשוט מבצע מתג גלובלי בכל פעם שהמערכת נתקלת בבורר אח כלשהו, ומשבית את שיתוף הסגנונות עבור המסמך כולו אם הוא קיים. הבורר כולל את הבורר + ובוררים כמו :ילד ראשון ו-:ילד אחרון.

עץ הכללים ב-Firefox

ב-Firefox יש שני עצים נוספים לחישוב קל יותר של סגנון: עץ הכללים ועץ הקשר הסגנון. גם ל-WebKit יש אובייקטים של סגנון, אבל הם לא מאוחסנים בעץ כמו עץ הקשר סגנון, רק הצומת של ה-DOM מפנה לסגנון הרלוונטי.

עץ הקשר בסגנון Firefox.
איור 14: עץ הקשר בסגנון Firefox.

הקשרי הסגנונות מכילים ערכי סיום. הערכים מחושבים על ידי החלת כל כללי ההתאמה בסדר הנכון וביצוע מניפולציות שמשנים אותם מערכים לוגיים לממשי. לדוגמה, אם הערך הלוגי הוא אחוז מהמסך, הוא יחושב וישומר ליחידות מוחלטות. הרעיון של עץ הכלל ממש חכם. כך ניתן לשתף את הערכים האלה בין צמתים כדי לא לחשב אותם שוב. כך גם חוסכים מקום.

כל הכללים התואמים מאוחסנים בעץ. לצמתים התחתונים בנתיב יש עדיפות גבוהה יותר. העץ מכיל את כל הנתיבים להתאמות הכללים שנמצאו. האחסון של הכללים מתבצע באופן מדורג. העץ אינו מחושב בהתחלה עבור כל צומת, אך בכל פעם שיש לחשב סגנון צומת, הנתיבים המחושבים נוספים לעץ.

הרעיון הוא לראות את נתיבי העצים כמילים בלקסיקון. נניח שכבר חישבנו את עץ הכללים הזה:

עץ כללים מחושבים
איור 15: עץ הכללים המחושבים.

נניח שאנחנו צריכים להתאים כללים לרכיב אחר בעץ התוכן, ולגלות שהכללים המותאמים (בסדר הנכון) הם א'-ה'. הנתיב הזה כבר נמצא בעץ מכיוון שכבר חישבנו את הנתיב A-B-E-I-L. מעכשיו יהיה לנו פחות עבודה לעשות.

בואו נראה איך העץ מציל אותנו.

חלוקה למבנים

הקשרי הסגנונות מחולקים למבנים. מבנים אלה מכילים פרטי סגנון עבור קטגוריה מסוימת, כגון גבולות או צבע. כל הנכסים במבנה עוברים בירושה או לא עוברים בירושה. נכסים שעברו בירושה הם מאפיינים, אלא אם כן הוגדר על ידי הרכיב, הם עוברים בירושה מישות ההורה. מאפיינים שאינם עוברים בירושה (שנקראים מאפיינים 'איפוס') משתמשים בערכי ברירת מחדל אם הם לא מוגדרים.

העץ עוזר לנו לשמור במטמון מבנים שלמים (שמכילים את ערכי הקצה המחושבים) בעץ. הרעיון הוא שאם הצומת התחתון לא סיפק הגדרה למבנה, ניתן להשתמש ב-build ששמור במטמון בצומת העליון.

חישוב הקשרי הסגנונות באמצעות עץ הכללים

כשמחשבים את הקשר הסגנון של רכיב מסוים, קודם אנחנו מחשבים נתיב בעץ הכללים או משתמשים בנתיב קיים. לאחר מכן אנחנו מתחילים להחיל את הכללים על הנתיב כדי למלא את המבנים בהקשר הסגנון החדש. אנחנו מתחילים מהצומת התחתון של הנתיב, שהוא זה שקיבל את העדיפות הגבוהה ביותר (בדרך כלל הבורר הספציפי ביותר) וחוצה את העץ כלפי מעלה עד שהמבנה שלנו מלא. אם אין מפרט למבנה בצומת הכלל הזה, ניתן לבצע אופטימיזציה משמעותית - אנחנו נעלה את העץ עד שנמצא צומת שמציין אותו במלואו ומצביע אליו - זוהי האופטימיזציה הטובה ביותר - ה-struct כולו משותף. כך נשמרת החישוב של ערכי הסיום והזיכרון.

אם נמצא הגדרות חלקיות, נעלה את העץ עד שה-struct ימולא.

אם לא מצאנו הגדרות ל-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}

כדי לפשט את הדברים, נניח שעלינו למלא רק שני בניינים: מבנה הצבע ומבנה השוליים. מבנה הצבעים מכיל איבר אחד בלבד: הצבע מבנה השוליים מכיל את ארבעת הצדדים.

עץ הכללים שמתקבל ייראה כך (הצמתים מסומנים בשם הצומת: מספר הכלל שאליו הם מצביעים):

עץ הכללים
איור 16: עץ הכללים

עץ ההקשר ייראה כך (שם הצומת: צומת הכלל שאליו הם מפנים):

עץ ההקשר.
איור 17: עץ ההקשר

נניח שאנחנו מנתחים את ה-HTML ומגיעים לתג <div> השני. עלינו ליצור הקשר סגנון עבור הצומת הזה ולמלא את מבני הסגנון שלו.

נתאים את הכללים ונגלה שהכללים התואמים ל-<div> הם 1, 2 ו-6. המשמעות היא שכבר קיים נתיב בעץ שהרכיב שלנו יכול להשתמש בו, ואנחנו רק צריכים להוסיף אליו עוד צומת לכלל 6 (צומת F בעץ הכללים).

ניצור הקשר בסגנון ונשים אותו בעץ ההקשר. הקשר הסגנון החדש יצביע על צומת F בעץ הכללים.

עכשיו עלינו למלא את מבני הסגנון. נתחיל במילוי מבנה השוליים. מאחר שהצומת האחרון של הכלל (F) לא מתווסף למבנה השוליים, נוכל לעלות בעץ עד שנמצא מבנה שמור שחושב בהכנסת צומת קודמת ונשתמש בו. הוא נמצא בצומת B, שהוא הצומת העליון ביותר שבו הוגדרו כללי השוליים.

יש לנו הגדרה למבנה הצבע, אז אנחנו לא יכולים להשתמש ב-build ששמור במטמון. מכיוון שלצבע יש מאפיין אחד, אנחנו לא צריכים לטפס על העץ כדי למלא מאפיינים אחרים. אנו נחשב את הערך הסופי (המרת מחרוזת ל-RGB וכו') ונשמור במטמון את המבנה המחושב בצומת זה.

העבודה על רכיב <span> השני קלה עוד יותר. נתאים את הכללים ונגיע למסקנה שהוא מצביע על כלל G, כמו בחלק הקודם. מאחר שיש לנו אחים שמפנים לאותו צומת, אנחנו יכולים לשתף את כל הקשר הסגנון ולהצביע רק על ההקשר של הקטע הקודם.

עבור מבנים שמכילים כללים שעוברים בירושה מתבנית ההורה, השמירה במטמון מתבצעת בעץ ההקשר (מאפיין הצבע בפועל עובר בירושה, אבל Firefox מתייחס אליו כאיפוס ומאחסן אותו במטמון בעץ הכללים).

לדוגמה, אם הוספנו בפסקה כללים לגבי גופנים:

p {font-family: Verdana; font size: 10px; font-weight: bold}

לאחר מכן, ייתכן שרכיב הפסקה, שהוא הצאצא של div בעץ ההקשר, שיתף את אותו מבנה גופן כמו ההורה שלו. הסיבה לכך היא שלא צוינו כללי גופנים עבור הפסקה.

ב-WebKit אין עץ כללים, ההצהרות התואמות נבדקות ארבע פעמים. קודם מחילים מאפיינים לא חשובים עם עדיפות גבוהה (מאפיינים שצריך להחיל קודם כי אחרים תלויים בהם, כמו תצוגה), אחר כך עדיפות גבוהה חשובה, אחר כך עדיפות רגילה לא חשובה, ולאחר מכן כללים חשובים של עדיפות רגילה. המשמעות היא שנכסים שמופיעים כמה פעמים ייפתרו בהתאם לסדר המדורג הנכון. ניצחונות האחרונים.

לסיכום: שיתוף אובייקטי הסגנון (באופן מלא או חלק מהמבנים שבתוכם) פותר את בעיות 1 ו-3. עץ הכללים של Firefox עוזר גם להחיל את המאפיינים בסדר הנכון.

לשנות את הכללים להתאמה קלה

יש כמה מקורות לכללי הסגנון:

  1. כללי CSS, בגיליונות סגנונות חיצוניים או ברכיבי סגנון. css p {color: blue}
  2. מאפייני סגנון בתוך השורה, כמו html <p style="color: blue" />
  3. מאפיינים חזותיים של HTML (שממופים לכללי סגנון רלוונטיים) html <p bgcolor="blue" /> אפשר להתאים בקלות את שני המאפיינים האחרונים לרכיב, כי הוא הבעלים של מאפייני הסגנון, ואפשר למפות את מאפייני ה-HTML באמצעות האלמנט בתור המפתח.

כפי שצוין קודם לכן בסעיף 2, ההתאמה של כללי ה-CSS יכולה להיות מורכבת יותר. כדי לפתור את הקושי, הכללים משתנים כדי לאפשר גישה קלה יותר.

לאחר ניתוח גיליון הסגנונות, הכללים מתווספים לאחת מכמה מפות גיבוב (hash), בהתאם לבורר. קיימות מפות לפי מזהה, לפי שם מחלקה, לפי שם תג ומפה כללית לכל פריט שלא מתאים לקטגוריות האלה. אם הבורר הוא מזהה, הכלל יתווסף למפת המזהים. אם מדובר בכיתה, הוא יתווסף למפה של הכיתה וכו'.

בעקבות השינוי הזה יהיה הרבה יותר קל להתאים את הכללים. אין צורך לבדוק בכל הצהרה: אנחנו יכולים לחלץ את הכללים הרלוונטיים של רכיב מהמפות. האופטימיזציה הזו מבטלת יותר מ-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, סדר המדורגות הוא (מהנמוך לגבוה):

  1. הצהרות דפדפן
  2. הצהרות רגילות של המשתמש
  3. הצהרות רגילות של המחבר
  4. הצהרות חשובות על המחבר
  5. הצהרות חשובות למשתמש

הצהרות הדפדפן הן הכי פחות חשובות והמשתמש מבטל את המחבר רק אם ההצהרה סומנה כחשובה. הצהרות באותו סדר ימוינו לפי ספציפיות, ולאחר מכן לפי הסדר שבו הן צוינו. המאפיינים החזותיים של HTML מתורגמים להצהרות CSS תואמות . המערכת מתייחסת אליהם כאל כללי מחבר עם עדיפות נמוכה.

ספציפיות

הספציפיות של הבורר מוגדרת על ידי מפרט CSS2 באופן הבא:

  1. ספירה 1 אם ההצהרה שממנה הוא לקוח היא מאפיין 'style' ולא כלל עם סלקטור, 0, אחרת (= a)
  2. לספור את מאפייני המזהה בבורר (= b)
  3. לספור את מספר התכונות האחרות ופסאודו-מחלקות בבורר (= c)
  4. לספור את שמות הרכיבים ופסאודו-רכיבים בבורר (= 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 משתמש בסימון שמסמן אם כל גיליונות הסגנונות ברמה העליונה (כולל @ייבוא) נטענו. אם הסגנון לא נטען במלואו בעת צירוף הקובץ, נעשה שימוש בלחצני המקום והוא מסומן במסמך, והם יחושבו מחדש לאחר שגיליונות הסגנונות ייטענו.

פריסה

כשהכלי לעיבוד נוצר ונוסף לעץ, אין לו מיקום וגודל. חישוב הערכים האלה נקרא פריסה או הזרמה חוזרת.

ב-HTML נעשה שימוש במודל פריסה מבוסס זרימה. כלומר, ברוב המקרים ניתן לחשב את הגיאומטריה במעבר אחד. רכיבים בשלב מאוחר יותר "בזרימה" בדרך כלל לא משפיעים על הגיאומטריה של רכיבים מוקדמים יותר "בזרימה", כך שהפריסה יכולה להמשיך משמאל לימין, מלמעלה למטה, לאורך המסמך. יש מקרים חריגים: לדוגמה, יכול להיות שיהיה צורך ביותר ממעבר אחד לטבלאות HTML.

מערכת הקואורדינטות היא יחסית למסגרת הבסיסית. נעשה שימוש בקואורדינטות העליונות והשמאליות.

הפריסה היא תהליך רקורסיבי. הוא מתחיל בכלי לרינדור ברמה הבסיסית, שתואם לרכיב <html> של מסמך ה-HTML. הפריסה ממשיכה באופן רקורסיבי בחלק מהיררכיית הפריימים או בכולה, ומחושבת מידע גיאומטרי לכל רינדור שדורש אותו.

המיקום של הכלי לרינדור ברמה הבסיסית הוא 0,0 והמידות שלו הן אזור התצוגה – החלק הגלוי של חלון הדפדפן.

לכל הגורמים שמבצעים רינדור יש שיטת 'פריסה' או 'זרימה חוזרת'. כל גורם מאפשר מפעיל את שיטת הפריסה של הצאצאים שלו שצריכים פריסה.

מערכת ביטים מלוכלכים

כדי לא לבצע פריסה מלאה עבור כל שינוי קטן, דפדפנים משתמשים במערכת "מלוכלכת". כלי לעיבוד שהשתנה או נוסף מסמן את עצמו ואת הצאצאים שלו כ'מלוכלכים': זקוק לפריסה.

קיימים שני דגלים: 'מלוכלך' ו'ילדים מלוכלכים', כלומר, למרות שהכלי לרינדור עצמו בסדר, יש לו לפחות צאצא אחד שצריך פריסה.

פריסה גלובלית ופריסה מצטברת

אפשר להפעיל את הפריסה בכל עץ העיבוד – זוהי פריסה 'גלובלית'. הדבר יכול לנבוע מהדברים הבאים:

  1. שינוי כללי בסגנון שמשפיע על כל הגורמים שמבצעים רינדור, למשל שינוי בגודל הגופן.
  2. כתוצאה משינוי גודל של מסך

הפריסה יכולה להיות מצטברת, רק כלי הרינדור מלוכלכים פרוסים (דבר זה עלול לגרום לנזק מסוים, שיחייב פריסות נוספות).

פריסה מצטברת מופעלת (באופן אסינכרוני) כאשר כלי הרינדור מלוכלכים. לדוגמה, כאשר כלי רינדור חדשים נוספים לעץ העיבוד אחרי שתוכן נוסף הגיע מהרשת ונוסף לעץ ה-DOM.

פריסה מצטברת.
איור 18: פריסה מצטברת - רק כלי רינדור מלוכלכים והילדים שלהם פרוסים

פריסה אסינכרונית וסנכרונית

הפריסה המצטברת מתבצעת באופן אסינכרוני. Firefox מוסיף לתור 'פקודות הזרמה' עבור פריסות מצטברות, ומתזמן הבקשות מפעיל הפעלה באצווה של הפקודות האלה. ל-WebKit יש גם טיימר שמפעיל פריסה מצטברת - מתבצע מעבר בעץ ורכיבי רינדור "מלוכלכים" מתפרסמים.

סקריפטים שמבקשים פרטי סגנון, כמו "offsetHeight", יכולים להפעיל פריסה מצטברת באופן סינכרוני.

הפריסה הגלובלית מופעלת בדרך כלל באופן סינכרוני.

לפעמים הפריסה מופעלת כקריאה חוזרת (callback) אחרי פריסה ראשונית כי מאפיינים מסוימים, כמו מיקום הגלילה, השתנו.

אופטימיזציות

כאשר פריסה מופעלת על ידי שינוי גודל או שינוי במיקום של כלי הרינדור(ולא בגודל), גדלי העיבוד נלקחים ממטמון ולא מחושבים מחדש...

במקרים מסוימים, רק עץ משנה משתנה והפריסה לא מתחילה מהשורש. מצב כזה יכול להתרחש במקרים שבהם השינוי הוא מקומי ולא משפיע על הסביבה שלו, כמו טקסט שמוסיפים לשדות טקסט (אחרת, כל הקשה תגרום להפעלה של פריסה שמתחילה מהשורש).

תהליך הפריסה

הפריסה כוללת בדרך כלל את התבנית הבאה:

  1. הכלי לעיבוד הורה קובע את הרוחב שלו.
  2. הורה מתעלל בילדים ו:
    1. מציבים את כלי הרינדור הצאצא (מגדיר את ה-x וה-y).
    2. קורא לפריסת צאצא במקרה הצורך – הם מלוכלכים או שאנחנו בפריסה גלובלית, או מסיבה אחרת – המחשבת את הגובה של הילד או הילדה.
  3. הורה משתמש בגבהים המצטברים של הילדים ובגבהים של השוליים והמרווחים כדי להגדיר גובה משלו – ההגדרה הזו תשמש את ההורה של הרינדור ההורה.
  4. מגדיר את החלק המלוכלך כ-FALSE.

Firefox משתמש באובייקט "state" (nsHTMLReflowState) כפרמטר לפריסה (נקרא "reflow"). בין היתר, המצב כולל את רוחב ההורים.

הפלט של פריסת Firefox הוא אובייקט "Metrics" (nsHTMLReflowMetrics). היא תכלול את הגובה המחושב של כלי הרינדור.

חישוב הרוחב

רוחב המעבד מחושב על סמך רוחב בלוק הקונטיינר, מאפיין הסגנון "width" של כלי הרינדור, השוליים והגבולות.

לדוגמה, הרוחב של ה-div הבא:

<div style="width: 30%"/>

יחושב על ידי WebKit באופן הבא(שיטת calcWidth של class RenderBox):

  • רוחב הקונטיינר הוא המקסימום של המאגרים הזמיניםwidth ו-0. במקרה הזה, ה- availablewidth הוא הערך contentWidth שמחושב באופן הבא:
clientWidth() - paddingLeft() - paddingRight()

clientwidth ו-clientheight מייצגים את החלק הפנימי של אובייקט, לא כולל גבולות וסרגל גלילה.

  • רוחב הרכיבים הוא מאפיין הסגנון 'width'. הוא יחושב כערך מוחלט על ידי חישוב האחוז מרוחב המאגר.

  • הגבולות והמרווחים האופקיים נוספו.

עד עכשיו זה היה החישוב של "הרוחב המועדף". כעת יחושבו ערכי הרוחב המינימלי והמקסימליים.

אם הרוחב המועדף גבוה מהרוחב המקסימלי, המערכת תשתמש ברוחב המקסימלי. אם הוא קטן מהרוחב המינימלי (היחידה הקטנה ביותר שלא ניתנת לשבירה), המערכת תשתמש ברוחב המינימלי.

הערכים נשמרים במטמון למקרה שיש צורך בפריסה, אבל הרוחב לא משתנה.

ניתוק קו

כשכלי לעיבוד באמצע פריסה מחליט שיש בו צורך לקרוס, הוא נעצר ומופץ חזרה אל ההורה של הפריסה שיש לשבור. ההורה יוצר את ה-Renderers הנוספים ואת פריסת הקריאות בהם.

ציור

בשלב הציור, עוברים את עץ העיבוד והשיטה ' Paint() ' של הרינדור מופעלת כדי להציג תוכן במסך. תהליך הציור משתמש ברכיב התשתית של ממשק המשתמש.

גלובלי ומצטבר

בדומה לפריסה, הציור יכול להיות גם גלובלי - העץ כולו צבוע - או מצטבר. בציור מצטבר, חלק מהמעבדים משתנים בדרך שאינה משפיעה על העץ כולו. הכלי לעיבוד שהשתנה מבטל את התוקף של המלבן שלו במסך. בעקבות זאת, מערכת ההפעלה יכולה לראות אותו כ'אזור מלוכלך' וליצור אירוע "צבע". מערכת ההפעלה עושה זאת בצורה חכמה ומשלבת מספר אזורים לאזור אחד. ב-Chrome זה מורכב יותר, כי כלי הרינדור נמצא בתהליך שונה מזה של התהליך הראשי. Chrome מדמה במידה מסוימת את התנהגות מערכת ההפעלה. המצגת מאזינה לאירועים האלה ומעניקה את ההודעה לשורש העיבוד. חוצים את העץ עד שמגיעים לכלי הרינדור הרלוונטי. הוא יצבע מחדש (ובדרך כלל גם את הצאצאים שלו).

סדר הציור

CSS2 מגדיר את הסדר של תהליך הציור. זהו למעשה הסדר שבו הרכיבים מוערמת בהקשרי הסידור בערימה. הסדר הזה משפיע על הציור כי המקבצים נצבעים מההתחלה ועד הסוף. סדר הערימה של מעבד בלוקים הוא:

  1. צבע רקע
  2. תמונת רקע
  3. border
  4. ילדים
  5. outline

רשימת תצוגה ב-Firefox

Firefox עובר על עץ העיבוד ובונה רשימת תצוגה למלבן הצבוע הצבוע. היא מכילה את כלי הרינדור הרלוונטיים למלבן, בסדר הנכון של הרינדור (רקעים של כלי הרינדור, לאחר מכן גבולות וכו').

כך צריך לחצות את העץ פעם אחת בלבד לצביעה מחדש במקום מספר פעמים – לצבוע את כל הרקעים, אחר כך את כל התמונות, ואז את כל הגבולות וכו'.

Firefox מייעל את התהליך בכך שהוא לא מוסיף רכיבים שיסתירו, כמו רכיבים שנמצאים מתחת לרכיבים אטומים אחרים.

אחסון מלבנים ב-WebKit

לפני הצביעה מחדש, WebKit שומר את המלבן הישן כמפת סיביות (bitmap). לאחר מכן הוא מצייר רק את הדלתא בין המלבנים החדשים והישנים.

שינויים דינמיים

הדפדפנים מנסים לבצע את הפעולות המינימליות האפשריות בתגובה לשינוי. לכן, שינויים בצבע של אלמנט יגרמו רק לצביעה מחדש שלו. שינויים במיקום הרכיב יגרמו לפריסה ולצביעה מחדש של הרכיב, על הצאצאים שלו, ואולי גם על אחים ואחיות. הוספה של צומת DOM תגרום לפריסה של הצומת ולצביעה מחדש. שינויים משמעותיים, כגון הגדלת גודל הגופן של רכיב ה-"html", יגרמו לביטול התוקף של המטמון, לפריסה ולצביעה מחדש של העץ כולו.

ה-threads של מנוע העיבוד

מנוע הרינדור הוא בשרשור יחיד. כמעט הכול, מלבד פעולות ברשת, מתרחש בשרשור אחד. ב-Firefox וב-Safari, זהו ה-thread הראשי של הדפדפן. ב-Chrome, זהו ה-thread הראשי של תהליך הכרטיסיות.

פעולות רשת יכולות להתבצע על ידי מספר שרשורים מקבילים. מספר החיבורים המקבילים מוגבל (בדרך כלל 2 עד 6 חיבורים).

לולאת אירוע

ה-thread הראשי של הדפדפן הוא לולאה של אירועים. זוהי לולאה אינסופית ששומרת על התהליך חי. המערכת ממתינה לאירועים (כמו אירועי פריסה וציור) ומעבדת אותם. זהו קוד Firefox של לולאת האירוע הראשי:

while (!mExiting)
    NS_ProcessNextEvent(thread);

מודל חזותי של CSS2

הקנבס

על פי מפרט CSS2, המונח קנבס מתאר את "המרחב שבו מתבצע עיבוד המבנה": המקום שבו הדפדפן צבוע את התוכן.

אזור העריכה הוא אינסופי עבור כל מאפיין של המרחב, אך הדפדפנים בוחרים רוחב התחלתי בהתאם למידות של אזור התצוגה.

על פי www.w3.org/TR/CSS2/zindex.html, הקנבס שקוף אם הוא חלק בתוך אחר, ואם מוגדר צבע בדפדפן אחר, הוא שקוף.

מודל של תיבת CSS

מודל תיבת ה-CSS מתאר את התיבות המלבניות שנוצרות עבור רכיבים בעץ המסמך ומסודרים בהתאם למודל העיצוב החזותי.

לכל תיבה יש אזור תוכן (למשל טקסט, תמונה וכו') וסביבה אופציונלית עם מרווח פנימי, גבולות ואזורי שוליים.

דגם תיבת CSS2
איור 19: מודל תיבת CSS2

כל צומת יוצר 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.

סכמת מיקום

קיימות שלוש סכימות:

  1. רגיל: האובייקט ממוקם בהתאם למיקומו במסמך. המשמעות היא שהמיקום שלו בעץ העיבוד דומה למקומו בעץ ה-DOM נקבע בהתאם לסוג התיבה ולמידות שלו
  2. צף: האובייקט מוצב בהתחלה כמו זרימה רגילה, ולאחר מכן מזיז אותו רחוק ככל האפשר שמאלה או ימינה
  3. מוחלט: האובייקט מוצב בעץ העיבוד במקום שונה מזה שבעץ ה-DOM

סכימת המיקום מוגדרת על ידי המאפיין 'מיקום' והמאפיין 'float'.

  • סטטיות ויחסיות, גורמות לזרימה נורמלית
  • גורם מוחלט וקבוע למיקום מוחלט

במיקום הסטטי לא מוגדר מיקום, והמערכת משתמשת במיקום ברירת המחדל. בסכמות האחרות, המחבר מציין את המיקום: עליון, תחתון, שמאל, ימין.

אופן הפריסה של התיבה נקבע על סמך:

  • סוג קופסה
  • מידות התיבה
  • סכמת מיקום
  • מידע חיצוני כמו גודל התמונה וגודל המסך

סוגי קופסאות

תיבת חסימה: יוצרת בלוק - מופיעה מלבן משלו בחלון הדפדפן.

תיבת חסימה.
איור 20: תיבת חסימה

תיבה מוטבעת: אין בלוק משלה אבל נמצאת בתוך בלוק מכיל.

תיבות מוטבעות.
איור 21: תיבות מוטבעות

בלוקים מעוצבים במאונך אחד אחרי השני. השורות מעוצבות לרוחב.

חסימה ועיצוב בתוך השורה.
איור 22: עיצוב בלוקים ועיצוב בתוך השורה

תיבות מוטבעות ממוקמות בתוך שורות או "תיבות". הקווים גבוהים לפחות כמו בתיבה הגבוהה ביותר, אבל הם יכולים להיות גבוהים יותר כאשר התיבות מיושרות ל'קו בסיס'. כלומר, החלק התחתון של הרכיב מיושר בנקודה אחרת שאינה מתחת לחלק התחתון. אם רוחב המאגר לא מספיק, הקווים הפנימיים יוצבו בכמה שורות. בדרך כלל זה מה שקורה בפסקה.

קווים.
איור 23: קווים

מיקום

קרוב משפחה

מיקום יחסי - ממוקם כרגיל ולאחר מכן מועבר לפי הדלתא הדרושה.

מיקום יחסי.
איור 24: מיקום יחסי

צף

תיבה צפה זזה שמאלה או ימינה של קו. התכונה המעניינת היא שהתיבות האחרות עוברות מסביבה. קוד ה-HTML:

<p>
  <img style="float: right" src="images/image.gif" width="100" height="100">
  Lorem ipsum dolor sit amet, consectetuer...
</p>

הכתובת תיראה כך:

ציפה.
איור 25: מספר צף

מוחלט וקבוע

הפריסה מוגדרת בדיוק ללא קשר לזרימה הרגילה. הרכיב לא משתתף בתהליך הרגיל. המאפיינים הם יחסיים למאגר התגים. בקבוע, מאגר התגים הוא אזור התצוגה.

מיקום קבוע.
איור 26: מיקום קבוע

ייצוג בשכבות

הוא מצוין באמצעות מאפיין ה-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>

התוצאה תהיה:

מיקום קבוע.
איור 27: מיקום קבוע

למרות שה-div האדום קודם לזה הירוק שבסימון, והיה נצבע קודם לכן בזרימה הרגילה, המאפיין z-index גבוה יותר, ולכן הוא מתקדם יותר במקבץ ששמורה בתיבת השורש.

משאבים

  1. ארכיטקטורת דפדפן

    1. גרוססקורת', אלן. ארכיטקטורת עזר לדפדפני אינטרנט (pdf)
    2. גופטה, ויניט. איך דפדפנים פועלים – חלק 1 – ארכיטקטורה
  2. ניתוח

    1. אהו, סת'י, אולמן, מהדרים: עקרונות, טכניקות וכלים (הידועים גם בשם "ספר הדרקון"), אדיסון-וסלי, 1986
    2. ריק ג'ליף. The Bold and the Beautiful: שתי טיוטות חדשות של HTML 5.
  3. Firefox

    1. L. דייוויד ברון, Faster HTML and CSS: Layout Engine Internals for Web Developers.
    2. L. דייוויד ברון (David Baron), HTML ו-CSS מהירים יותר: Layout Engine Internals for Web Developers (סרטוני דיונים טכניים של Google)
    3. L. דייוויד ברון, Mozilla's Layout Engine
    4. L. דייוויד ברון, מסמכי תיעוד של מערכת סגנון Mozilla
    5. כריס ווטרסון, הערות לגבי HTML Reflow
    6. כריס ווטרסון, Gecko Overview
    7. אלכסנדר לרסון, החיים של בקשת HTML מסוג HTML
  4. WebKit

    1. דייוויד הייאט, Implementing CSS(חלק 1)
    2. דייוויד הייאט, An Overview of WebCore
    3. דייוויד הייאט, WebCore Rendering
    4. דייוויד הייאט, The FOUC issue
  5. מפרטי W3C

    1. מפרט HTML 4.01
    2. מפרט W3C HTML5
    3. מפרט גיליונות סגנון מדורגים ברמה 2 גרסה 1 (CSS 2.1)
  6. הוראות ל-build של דפדפנים

    1. Firefox. https://developer.mozilla.org/Build_Documentation
    2. WebKit. http://webkit.org/building/build.html

תרגומים

הדף הזה תורגם ליפנית פעמיים:

אפשר לראות את התרגומים של קוריאנית וטורקית באירוח חיצוני.

תודה לכולם.