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

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

הקדמה

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

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

פול אירלנד, קשרי מפתחים ב-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. רישות: לקריאות רשת כמו בקשות 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: התהליך הראשי של מנוע עיבוד הגקו של Mozilla

מדמויות 3 ו-4 אפשר לראות שלמרות ש-WebKit ו-Gecko משתמשים במונחים שונים מעט, אבל הזרימה זהה.

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

ניתוח – כללי

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

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

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

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

דקדוק

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

מנתח נתונים – שילוב לקסר

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

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

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

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

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

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

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

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

תרגום

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

תהליך ההידור
איור 7: תהליך הידור (compilation)

דוגמה לניתוח

באיור 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 + 3 2
מונח + 3 - 1
פעולה לתקופה 1-3
ביטוי - 1
פעולת ביטוי 1
ביטוי -

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

יצירה אוטומטית של מנתחים

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

WebKit משתמשת בשני מחוללי מנתחים ידועים: Flex כדי ליצור lexer ו-Bison כדי ליצור מנתח (יכול להיות שתיתקלו בהם עם השמות Lex ו-Yacc). קלט Flex הוא קובץ שמכיל הגדרות של ביטויים רגולריים של האסימונים. הקלט של ביסון הוא כללי התחביר של השפה בפורמט BNF.

מנתח HTML

התפקיד של מנתח ה-HTML הוא לנתח את תגי העיצוב של HTML לעץ ניתוח.

דקדוק HTML

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

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

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

יש פורמט רשמי להגדרת HTML - DTD (הגדרת סוג מסמך), אבל הוא לא דקדוק ללא הקשר.

זה נראה מוזר במבט הראשון; קוד ה-HTML די קרוב ל-XML. יש הרבה מנתחי XML זמינים. קיימת גרסת XML של HTML - XHTML - אז מה ההבדל הגדול?

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

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

DTD ב-HTML

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

יש כמה גרסאות של 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" האסימון יגרום למעבר למצב "before html" ולעיבוד מחדש של האסימון במצב הזה. פעולה זו תגרום ליצירת הרכיב HTMLHtmlElement, שיצורף לאובייקט השורש של מסמך.

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

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

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

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

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

פעולות אחרי שהניתוח מסתיים

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

אפשר לראות את האלגוריתמים המלאים של יצירת אסימונים ובניית עצים במפרט ה-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}*

&quot;ident&quot; הוא קיצור של מזהה, כמו שם מחלקה. "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*
  ;

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

מנתח CSS של WebKit

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

מנתח את קובץ ה-CSS.
איור 12: ניתוח CSS

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

סקריפטים

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

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

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

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

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

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

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

ב-Firefox, הרכיבים בעץ העיבוד קוראים 'frames'. WebKit משתמש במונחי רינדור או באובייקט רינדור.

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

לסיווג RenderObject של WebKit, סוג הבסיס של כלי הרינדור, יש את ההגדרה הבאה:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

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

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

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

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

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

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

תהליך בניית העץ

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

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

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

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

חישוב סגנון

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

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

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

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

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

    לדוגמה, הבורר המורכב הזה:

    div div div div{
    ...
    }
    

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

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

בואו נראה איך הדפדפנים מתמודדים עם הבעיות האלה:

נתוני סגנון של שיתוף

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

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

עץ הכללים של Firefox

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

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

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

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

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

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

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

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

חלוקה למבנים

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

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

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

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

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

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

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

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

הנה דוגמה: נניח שיש לנו את ה-HTML הזה

<html>
  <body>
    <div class="err" id="div1">
      <p>
        this is a <span class="big"> big error </span>
        this is also a
        <span class="big"> very  big  error</span> error
      </p>
    </div>
    <div class="err" id="div2">another error</div>
  </body>
</html>

ויש גם את הכללים הבאים:

div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

קיימים שני דגלים: "dirty" ו-"Childretys dirty" כלומר, למרות שיכול להיות שהכלי לרינדור עצמו תקין, יש לו לפחות צאצא אחד שדורש פריסה.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

תהליך הפריסה

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

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

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

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

חישוב הרוחב

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

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

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

מחושב על ידי WebKit כך: (class RenderBox method calcwidth):

  • רוחב המכל הוא המקסימלי של המכלים הזמיניםwidth ו-0. רוחב הזמין במקרה הזה הוא contentwidth שמחושב כך:
clientWidth() - paddingLeft() - paddingRight()

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

  • רוחב הרכיבים הוא ה"רוחב" style [סגנון]. הוא יחושב כערך מוחלט על ידי חישוב האחוז מרוחב המאגר.

  • עכשיו נוספו גבולות אופקיים ורווחים.

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

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

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

פריצת קו

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

ציור

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

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

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

סדר ציור

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

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

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

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

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

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

אחסון מלבן של WebKit

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

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

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

תהליכונים של מנוע הרינדור

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

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

לולאת אירוע

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

while (!mExiting)
    NS_ProcessNextEvent(thread);

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

אזור העריכה

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

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

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

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

סכימת המיקום מוגדרת על ידי ה- "position" המאפיין 'מספר ממשי', .

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

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

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

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

סוגי תיבות

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

תיבת חסימה.
איור 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>

ייראה כך:

מספר ממשי (float).
איור 25: מספר ממשי (float)

קבוע וקבוע

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

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

    1. L. דייוויד ברון (David Baron), HTML ו-CSS מהירים יותר: רכיבי Layout Engine פנימיים למפתחי אתרים.
    2. L. דייוויד ברון (David Baron), HTML ו-CSS מהירים יותר: תוכן פנימי של Layout Engine למפתחי אתרים (סרטון דיון טכנולוגי של Google)
    3. L. דיוויד ברון (David Baron), מנוע הפריסה של Mozilla
    4. L. דיוויד ברון, מסמכי תיעוד של מערכת הסגנון של Mozilla
    5. כריס ווטרסון (Chris Watson), Notes on HTML Reflow (הערות לגבי הזרמה חוזרת של HTML)
    6. כריס ווטרסון (Chris Waterson), Gecko Overview (סקירה כללית על Gecko)
    7. אלכסנדר לרסון, החיים של בקשת HTTP של HTML
  4. WebKit

    1. דייוויד הייאט (David Hyatt), הטמעת CSS(חלק 1)
    2. דייוויד הייאט (David Hyatt), סקירה כללית על WebCore
    3. דייוויד הייאט (David Hyatt), WebCore Rendering
    4. דייוויד הייאט (David Hyatt), The FOUC issue
  5. מפרטי W3C

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

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

תרגומים

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

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

תודה לכולם.