مرورگرها چگونه کار می‌کنند؟

مرورگرهای وب جزء پر استفاده ترین نرم افزارهای هستند، که تا به حال به دست بشر ساخته شده‌اند. به همین منظور، سفر اکتشافی خواهیم داشته، به اعماق این موجودات؛ تا ببینیم چه اتفاقی در پس پرده‌ی آن‌ها و موتورهای رندرشان در حال رخ دادن است.

مرورگرهای متداولی که ما روزانه با آنها سر و کار داریم، از ۷ بخش عمده تشکیل شده اند که اصلی ترین بخش آن‌ها، موتور رندرشان است. وظیفه‌ی این موتور، رندر محتوای صفحات دریافتی از سرور و پاسخگویی به تغییرات آینده است. فرآیند رندر شامل چهار مرحله‌‌ی عمده زیر می‌شود:

  1. تجزیه و ترجمه HTML و تبدیل آن به درخت DOM
  2. تشکیل درخت رندر براساس درخت DOM و درخت استایل
  3. صفحه بندی عناصر درخت رندر
  4. رسم و رنگرزی حالت گرافیکی اشیای درخت رندر

بعد از طی این چهار مرحله است که کاربر می‌توان محتوای درخواستی خود را بر روی صفحه نمایشگرش ببیند.

فهرست مطالب

  1. مقدمه
    1. مقصدمان کدام مرورگرها هستند؟
    2. یک مرورگر از چه اجزایی تشکیل شده است؟
  2. موتور رندر
    1. انواع موتورهای رندر
    2. چرخه اصلی فرآیند رندر یک صفحه‌ی HTML
    3. مثال‌های شماتیکی از فرآیند اصلی رندر
    4. تشکیل درخت DOM
      1. تجزیه «Parsing»
        1. تجزیه یا پارسینگ چیست؟
        2. گرامر
        3. تشکیل درخت تجزیه
        4. ترجمه
        5. تعاریف ثابت برای لغات و قواعد
        6. انواع تجزیه‌گرها
        7. ایجاد خودکار تجزیه‌گرها
      2. تجزیه‌گر HTML
        1. گرامر تعریف شده‌ی زبان HTML
        2. چرا گرامر HTML بدون چارچوب است؟!
        3. HTML DTD
        4. DOM
        5. الگوریتم تجزیه HTML
        6. چه اتفاق‌هایی بعد از اتمام تجزیه HTML رخ می‌دهد؟
        7. چرا مرورگرها از خطاهای HTML‌ می‌گذرند؟!
      3. تجزیه‌گر CSS
        1. تجزیه‌گر CSS موتور وب‌کیت
      4. ترتیب پردازش اسکریپت‌ها و برگه‌های استایل
        1. اسکریپت‌ها
        2. برگه‌های استایل
        3. تجزیه‌ی موازی
    5. ساخت درخت رندر
      1. رابطه درخت رندر با درخت DOM
      2. فرآیند ساخت درخت رندر
      3. محاسبه‌ی استایل
      4. فرآیند تدریجی و گام به گام تجزیه‌ی CSS
    6. صفحه بندی «layout»
      1. سیستم کمی آلوده (“dirty bit”)
      2. صفحه بندی: تدریجی و عمومی
      3. صفحه بندی: ترتیبی و موازی
    7. رنگرزی «painting»
      1. انواع فرآیندهای رنگرزی: عمومی و تدریجی
      2. ترتیب اولویت‌های رنگرزی
    8. انعطاف در برابر تغییرات
    9. جریان‌های موتور رندر کننده
      1. حلقه‌ی رخداد
  3. سخن پایانی

مقدمه

مرورگرهای وب، امروزه دیگر همه‌جا هستند؛ و یکی از اصلی‌ترین نرم افزارهایی هستند که اغلب ما در طول روز با آن‌ها سر و کار داریم. محتوای آن‌ها، صفحات وب و برنامه‌های تحت وبی هستند که در نتیجه تلاش مداوم توسعه دهندگان وب ایجاد می‌شوند. وجود آن‌ها به همان اندازه‌ای که برای مصرف کنندگان این محتوا حیاتی است، برای تولید کنندگانش نیز هست.

از دیر باز، در هر عرصه‌ای، از صنعت گرفته تا هنر، مرز باریکی بین خلاقیت، تقلید و تکرار منفعلانه وجود داشته است. در این بین، افرادی که توانسته‌اند از این مرز عبور کنند، نام‌شان را در طول تاریخ، جاودانه کرده‌اند. پیش نیاز خلاقیت و ابداع، دانش و درک عمیق است. هر چقدر این درک و دانش نسبت به اصول اولیه، ابزار و فنون بالاتر رود، جسارت برای تغییر و نوآوری نیز در شخص، بالاتر می‌رود.

استاد کاری که نتواند ابزار کار خود را به حد کفایت بشناسد، ناخواسته به بردگی آن تن خواهد داد؛ و در صورت عکس ماجرا، این ابزار است که رام استاد کار خواهد شد و تحت کنترل آن در خواهد آمد. حال هر چه تسلط یک توسعه دهنده‌ی وب بر روی این مرورگرهای آن بیشتر باشد، از طرفی با جسارت بیشتری می‌تواند دست به طراحی و تخیل بزند؛ و از طرف دیگر در مواجه با چالش‌های گوناگون پیش بینی نشده در طول کار، با اعتماد بنفس بیشتری با آن‌ها درگیر می‌شود.

در طول سال‌های متمادی که IE سهم نود درصدی از میزان استفاده از مرورگرها را در دست داشت، چیز زیادی از آن‌ها نمی‌دانستیم و به صورت یک جعبه‌ی سیاه باقی مانده بودند. اما بعد از ورود شکوهمندانه‌ی مرورگرهای متن باز و حالا با کسب بیش از ۷۵ درصدی سهم بازار، زمان مناسبی رسیده بود تا سرکی به پشت پرده‌ی موتورهای این مرورگرها بیندازیم و ببینیم چه چیزی در میان آن چند میلیون خط کد ++C در جریان است.

مقصدمان کدام مرورگرها هستند؟

در حال حاضر ۵ مرورگر اصلی که در سیستم عامل‌های رومیزی استفاده می‌شوند، عبارتند از: گوگل کروم، موزیلا فایرفاکس، ماکروسافت اینترنت اگسپلورر، اپل سافاری و اُپرا. در سیستم عامل‌های همراه نیز مرورگرهای اصلی عبارتند از: مرورگر اندروید، آیفون، ویندوز فون، اُپرا مینی، اُپرا موبایل، مرورگر UC، مرورگرهای s40/s60 نوکیا و کروم - که همه آنها به جز مرورگرهای ویندوز فون و اپرا بر اساس موتور WebKit پیاده سازی شده‌اند.

متن پیش رو بر اساس مرورگرهای اوپن سورس فایرفاکس، کروم و سافاری(که فقط بخشی‌هایی از آن اوپن سورس است) تنظیم شده است. در حال حاضر این مرورگرها به شهادت آمار سایت StatCounter در ۳ ماهه‌ی پاییز ۱۳۹۴ در ایران، به طور میانگین بیش از ۷۵ درصد سهام بازار مرورگرها را در تمامی دستگاه‌های رومیزی و همراه در اختیار دارند.

StatCounter-browser-IR-quarterly-q4-2015-barStatCounter-browser-IR-quarterly-q4-2015-bar
نمودار درصد سهم مرورگرها از بازار ایران در پاییز ۱۳۹۴

یک مرورگر از چه اجزایی تشکیل شده است؟

اجزایی اصلی یک مرورگر وب عبارتند از:

  1. رابط کاربری (UI): این لایه شامل آدرس بار، دکمه‌های عقب و جلو، منوی بوکمارک و غیره است. در کل این رابط به جز قالب پنجره‌ی مرورگر (یعنی همان ۳-۴ دکمه‌ای که در گوشه‌ی بالایی سمت راست یا چپ کادر در برگیرنده‌ی مرورگر خود می‌بینید) را شامل می‌شود. که این یعنی تمامی بخش‌های ظاهری مرورگر و عناصر موجود در محتوای درخواستی کاربر که به عنوان یک صفحه‌ی وب نمایش داده می‌شود.
  2. موتور مرورگر: مسول اصلی رسیدگی به عملیات‌های صورت گرفته بین لایه‌های UI و موتور رندر است.
  3. موتور رندر: مسولیت نمایش محتوای درخواست شده بر عهده‌ی ایشان است. برای مثال اگر محتوای درخواست شده یک سند HTML باشد، موتور رندر کدهای HTML و CSS مربوطه را بعد از تجزیه کردن (Parse) بر روی صفحه نمایش کاربر، همان طور که در حال حاضر در حال خواندن این جملات هستید، نمایش می‌دهد.
  4. شبکه: مسول عملیات‌های شبکه‌ای مانند یک در خواست HTTP است. ایشان علاوه بر اینکه خود یک رابط (Interface) مستقل از پلتفرم هستند، از پیاده سازی‌های مختلفی نیز در پلتفرم‌های مختلف، در لایه‌های پایین‌تر از خود استفاده می‌کند.
  5. بستر رابط کاربری (UI backend): به منظور رسم و نمایش عناصری مثل کومبو باگس‌ها، دکمه‌ها، فیلدها و غیره استفاده می‌شوند. این لایه هم مثل لایه‌ی شبکه از یک رابط عمومی بهره می‌برد که مختص پلتفرم خاصی نیست؛ ولی در سطوح پایین از متدهای رابط کاربری مختص همان پلتفرم هم استفاده می‌کند.
  6. مترجم جاوا اسکریپت: همان طور که از اسمش معلوم است، وظیفه‌ی تجزیه و اجرای کدهای جاوا اسکریپت را بر عهده دارد.
  7. انباره داده‌ها (Data Storage): این لایه وظیفه‌ی نگهداری از تمامی انواع داده‌های مرورگر از قبیل، کوکی‌ها، session-ها و غیره را بر عهده دارد. و نیز عهده دار مدیریت مکانیزم‌های دیگر نگهداری و پالایش داده‌ها نظیر LocalStorage، IndexedDB، WebSQL و FileSystem نیز هست.
اجزای اصلی مرورگرها
اجزای اصلی مرورگرها

این نکته‌ی مهم را در نظر داشته باشید که مرورگرهای مانند کروم، نمونه‌های (Instance) متعددی از موتور رندر خود را هم زمان اجرا می‌کنند. یعنی برای هر تب، یک موتور رندر؛ که هر تب فرآیند(Process) جداگانه‌ی خود را دارد. (می‌توانید با فشار دادن همزمان کلیدهای ترکیبی shift+Esc در مرورگر کروم خود به راحتی فرآیند‌های در حال اجرا توسط آن را مشاهده کنید.)

موتور رندر

همان طور که قبلا اشاره شد، مسولیت موتور رندر، رندر کردن است؛ که این می‌شود نمایش محتوای درخواست شده از سمت کاربر نهایی، درون پنجره‌ی مرورگر.

به طور پیش فرض موتورهای رندر کننده‌ قادر هستند تا سندهای HTML، XML و تصاویر را نمایش دهند. البته آن‌ها علاوه بر این قسم از منابع، قادرند اسناد PDF و غیره را نیز به واسطه‌ی نصب پلاگین‌های مربوطه نمایش دهند (هر چند مرورگرهای مدرن امروزی بسیاری از پلاگین‌های دیروزی مانند همین PDF خوان را در خود به صورت توکار تعبیه کرده‌اند). در هر صورت در این فصل تماماْ بر روی نحوه‌ی رندر و نمایش اسناد HTML‌ و تصاویر که به وسیله‌ی CSS نقاشی شده‌اند، تمرکز خواهیم کرد.

انواع موتورهای رندر

مرورگرهای مختلف، از موتورهای رندر کننده‌ی مختلفی نیز استفاده می‌کنند. به طوری که:

  • اینترنت اگسپلورر: Trident
  • فایرفاکس: Gecko
  • سافاری: WebKit
  • کروم و اُپرا (از نسخه‌ی ۱۵): ‌Blink (شاخه‌ای از وب‌کیت است که توسط گوگل منشعب شده)

استفاده می‌کنند.

وب‌کیت یک موتور رندر کننده‌ی متن باز است که در ابتدا به منظور استفاده در پلتفرم لینوکس کلید خورد و بعد‌ها توسط شرکت اپل به منظور استفاده در پلتفرم‌های مک و ویندوز ویرایش شد. برای جزییات بیشتر می‌توایند به وب سایت خود این موتور که به تازگی هم باز طراحی شده مراجعه کنید.

چرخه اصلی فرآیند رندر یک صفحه‌ی HTML

موتور رندر کننده کار خودش را درست از زمانی آغاز می‌کند که محتوای درخواست شده توسط لایه‌ی شبکه، از سرور تحویل گرفته شده باشد. این اطلاعات به طور معمولی در قطعات ۸ کیلوبایتی از سمت سرور تحویل داده می‌شوند.

پس از رسیدن داده‌ها، موتور رندر چهار مرحله‌ای را که در تصویر زیر مشاهده می‌کنید را به ترتیب برای هر درخواست در هر بار باید طی کند تا سند درخواستی کاربر برایش در پنچره مرورگر قابل مشاهده و استفاده شود.

جریان اصلی عملیات انجام شده توسط موتور رندر مرورگر
چرخه اصلی فرآیند رندر یک صفحه‌ی HTML توسط موتور رندر مرورگرها

موتور رندر کننده با تجزیه و تبدیل سند HTML‌ به گره‌های (node) درختی به نام «درخت محتوا» کار خود را آغاز می‌کند. موتور هم چنین داده‌های مربوط به برگه‌های استایل (stylesheet) این گره‌ها را نیز تجزیه می‌کند که شامل تمامی انواع استایل‌ها، از خارجی (external) گرفته تا درونی (inline) می‌شود. این اطلاعات بدست آماده از تجزیه داده‌های استایل‌ها به همراه دستورات گرافیکی آمده در سند HTML توسط موتور رندر برای ساخت یک درخت دیگر، به نام «درخت رندر» استفاده می‌شود.

درخت رندر، شامل نواحی مستطیلی شکلی است که مشخص کننده‌ی مختصات هر عنصر در صفحه و خصوصیات آنها مانند رنگ، نوع و اندازه قلم و غیره است. این نواحی درست به ترتیبی که قرار است این عناصر بر روی صفحه نمایش نقش ببندند، بر روی درخت از بالا به پایین چیده می‌شوند.

بعد ساخته شدن درخت رندر، موتور به سراغ فرآیند صفحه بندی (layout) عناصر می‌رود. بدان معنی که به هر یک از گره‌های درخت رندر، نسبت به اندازه و مختصات دیگر گره‌های درخت، یک مختصات (طول و عرض و عمق) مخصوص می‌دهد که باید دقیقا بر اساس آن بر روی صفحه ظاهر شوند.

مرحله‌ی بعدی رنگرزی (painting) است. هر کدام از گره‌های درخت رندر در اینجا به وسیله‌ی لایه‌ی بستر رابط کاربری (UI backend) رنگ و لعاب داده می‌شوند. یعنی همان پوسته‌ی ظاهری که ما بر روی صفحه نمایش می‌توانیم به عنوان خروجی ببینیم، در این مرحله صورت می‌پذیرد.

این نکته‌ی مهم را در نظر داشته باشید که این چهار مرحله گفته شده، یک فرآیند پیش رونده تدریجی و بلادرنگ است. به این معنی که به منظور حصول یک تجربه کاربری دلپذیر برای کاربر، موتور رندر سعی می‌کند که به محض آماده شدن اولین سری از داده‌ها از سمت سرور و دریافت آن‌ها، فرآیند رندر را شروع کند؛ و به انتظار دریافت کامل اطلاعات نشیند. در این شیوه موتور رندر هر بخش از کدهای HTML‌ که به دستش می‌رسد را شروع به تجزیه می‌کند و فرآیند را تا به مرحله آخر، یعنی نمایش داده‌های ورودی در صفحه نمایش ادامه می‌دهد. این عمل را تا جایی تکرار می‌کند که به انتهای کد‌های HTML‌ دریافتی برسد.

مثال‌های شماتیکی از فرآیند اصلی رندر

فرآیند اصلی رندر در موتور WebKit
فرآیند اصلی رندر در موتور WebKit
فرآیند اصلی رندر در موتور Gecko موزیلا
فرآیند اصلی رندر در موتور Gecko موزیلا

همان طور که در دو تصویر فوق می‌بینید، دو موتور وب‌کیت و گِکو تفاوت مختصری از نظر لغوی و صرفا نام‌گذاری مراحل دارند و عملا فرآیند اصلی در هر دو یکسان است.

گِکو درخت عناصری که مشخصات ظاهریشان تعیین شده است را درخت فریم «Frame tree» می‌نامند؛ و به هر عنصر یا گره‌ی درخت یک فریم می‌گویید. این در حالی است که همان طور که گفته شد وب‌کیت به این درخت، «درخت رندر» می‌گویید؛ که از اشیای رندر «Render Objects» تشکیل شده می‌شود. در جای دیگر هم وب کیت از اصطلاح صفحه بندی «layout» برای مشخص کردن مختصات عناصر در صفحه استفاده می‌کند؛ در حالی که گِکو آن را باز جانمایی «reflow» می‌نامند.

وب‌کیت از اصطلاح ضمیمه «Atachment» برای مرحله‌ای که گره‌های DOM‌ را با خصوصیات گرافیکی استخراجی مرتبط‌شان از CSS می‌خواهد ترکیب کند، استفاده می‌کند. یک تفاوت غیر معنایی(لغوی) جزیی دیگر این است که گِکو یک لایه اضافه بین داده‌های تجزیه شده‌ی HTML و درخت DOM دارد که آن را حوض محتوا «content sink» می‌نامد که به صورت یک کارخانه با خط تولید عناصر DOM عمل می‌کند. در ادامه به تفصیل درباره‌ی تمامی بخش‌ها صحبت می‌کنیم.

تجزیه «Parsing»

تجزیه چیست؟

تجزیه کردن بیشتر به معنای ترجمه کردن است. به این معنی که ما بیایم یک سند و متنی را که بر اساس یک گرامر معین تنظیم شده است به یک سری ساختار قابل فهم برای ماشین که به آن زبان ماشین نیز گفته می‌شود تجزیه و ترجمه کنیم. نتیجه‌ی این ترجمه معمولا یک درختی از گره‌هایی است که نشان دهنده‌ی ساختار آن سند هستند؛ که به آن درخت تجزیه یا درخت نحو (syntax) نیز می‌گویند.

برای مثال، تجزیه‌ی عبارت ۱ - ۳ + ۲ به صورت درخت زیر می‌تواند در بیاید:

اگره‌های درخت تجزیه شده‌ی یک عبارت ریاضی
گره‌های درخت تجزیه شده‌ی یک عبارت ریاضی

گرامرها

عمل تجزیه بر اساس قواعد نحوی‌ای که از پیش برای هر زبانی تعیین شده است انجام می‌شود. هر فرمتی که قرار است تجزیه شود، باید دارای یک گرامر ثابت، که تشکیل شده از دایره‌ی واژگان آن و قواعد نحوی‌اش باشد. جالب است بدانید که زبان‌های انسانی، جزئ این زبان‌ها نیستند و به همین دلیل نمی‌توان آن‌ها را با تجزیه کنندگان معمولی که داریم تجزیه و تبدیل کنیم.

تشکیل درخت تجزیه

عمل تجزیه کردن به ۲ زیر بخش اصلی «آنالیز لغوی» و «آنالیز نحوی» تقسیم می‌شود.

آنالیز لغوی به فرآیندی اطلاق می‌شود که در آن داده‌های ورودی به اجزای کوچکتری شکسته می‌شوند که به آن‌ها علامت‌های رمزی یا تُکن «Token» می‌گویند. این توکن‌ها همان واژگان تشکیل دهنده‌ی زبان‌ها هستند. آنالیز نحوی نیز همان طور که از نامش بر می‌آید، بر اساس قواعد دستوری زبان سنجیده و استخراج می‌شود.

مسولیت عملیات تجزیه بر عهده‌ی ۲ بخش مجزا است. اولی lexer (به خوانید لِگزر) (که گاهی نیز به آن «tokenizer» می‌گویند) مسول شکستن داده‌های ورودی به توکن‌های معتبر است و دومی، پارسر/تجزیه‌گر parser نام دارد که مسؤل ساخت درخت تجزیه بر پایه‌ی قواعد نحویی زبانی است که داده‌های ورودی با آن تنظیم شده‌اند. هوشمندی لِگزرها در حدی است که کارکترهای زايد مانند فاصله‌ها و خط شکن‌ها را تشخیص دهند.

مراحل تبدیل یه سند ورودی به درخت تجزیه شده
مراحل تبدیل یه سند ورودی به درخت تجزیه شده

تجزیه کردن، یک فرآیند چرخشی و تکرار شونده است. تجزیه‌گر معمولا به لِگزر برای یک توکن جدید درخواست می‌دهد و سپس به دنبال یک قاعده‌ی نحوی مطابق با آن تُکِن می‌گردد. اگر با قاعده‌ای مطابق بود، که گره‌ای را بر روی درخت تجزیه به آن اختصاص می‌دهد؛ و دوباره یک تُکن جدید درخواست می‌کند.

در صورتی که هیچ قاعده‌ی نحوی مطابق با تُکن یافت نشود، تجزیه‌گر آن را به صورت یک تُکن داخلی ذخیره می‌کند؛ و به درخواست تُکن‌های بعدی ادامه می‌دهد تا زمانی که همه قواعد مربوط به تمامی تُکن‌های ذخیره شده به صورت داخلی نیز پیدا شود. اگر تُکنی در این مرحله هم چنان بدون قاعده مانده باشد، تجزیه‌گر اعلام خطا می‌کند. این بدان معنی است که سند ورودی معتبر نبوده و در برگیرنده‌ی قواعده‌ی نحوه‌ی اشتباهی است.

ترجمه

در بسیاری از موارد، درخت تجزیه شده، محصول نهایی نیست. ورودی تجزیه شده اغلب به مصرف یک مترجم می‌رسد که آن را به فرمت دیگری تبدیل می‌کند. کامپایلر برای تبدیل کد منبع ورودی به کد ماشین نهایی، در ابتدا ورودی را به درخت تجزیه شده تبدیل می‌کند و سپس آن را به کد ماشین ترجمه می‌کند.

چرخه‌ی تالیف کد ماشین
چرخه‌ی تالیف کد ماشین

حذف شده: تشریح مراحل تشکیل درخت تجزیه و ترجمه آن با مثال عبارات ۱ - ۳ + ۲

تعاریف ثابت برای لغات و قواعد

لغات معمولا به صورت عبارات باقاعده (regular expressions) بیان می‌شوند. برای مثال زبان ما به صورت زیر تعریف می‌شود.

INTEGER: 0|[1-9][0-9]*
PLUS: +MINUS: -
INTEGER: 0|[1-9][0-9]*PLUS: +
MINUS: -

همان طور که می‌بینید، اعداد به صورت عبارات باقاعده تعریف شده اند.

قواعد نحوی عموما به صورت فرمتی که آن را BNF می‌نامند تعریف می‌شوند. بر اساس این فرمت، زبان ما به این صورت تعریف می‌شود:

expression :=  term  operation
term operation :=  PLUS | MINUS
term := INTEGER | expression

در مثال بالا گفته‌ شده که این زبان می‌تواند توسط تجزیه‌گرهای معمول تجزیه شود؛ اگر گرامر آن یک گرامر بدون چارچوب «context free grammer» باشد. در یک تعریف سر راست، به گرامری، گرامر با چارچوب آزاد می‌گویند که بتوان آن را به صورت کامل توسط فرمت BNF بیان کرد.

انواع تجزیه‌گرها

دو نوع تجزیه‌گر وجود دارد: تجزیه‌گر «بالا به پایین» و «پایین به بالا». در یک تعریف کلی، تجزیه‌گر بالا به پایین ساختار بالاترین سطح از قواعد را می‌سنجد و سعی می‌کند تا قاعده منطبق با آن را پیدا کند. در حالی که در تجزیه‌گر پایین به بالا، تجزیه‌گر با ورودی‌ها شروع می‌کند و خُرد خُرد شروع به تبدیل آن‌ها به قواعد نحوی می‌کند؛ یعنی از قواعد سطح پایین شروع می‌کند تا برسد به قواعد سطوح بالاتر. بیاید ببینیم این دو نوع تجزیه‌گر در مواجه با زبانی که پیش‌تر ساختیم چگونه عمل می‌کنند.

تجزیه‌گر بالا به پایین، از قواعده‌ای با بالاترین سطح شروع می‌کند: آن داده‌های ۳ + ۲ را به عنوان یک عبارت شناسایی می‌کند. سپس به نوبت به شناسایی ۱ - ۳ +‌۲ می رسد که به عنوان عبارتی بعدی شناسایی می‌شود (فرآیند شناسایی عبارات به صورت تدریجی‌، با تطابق دادن با قواعد دیگر است؛ اما نقطه شروع این سنجش قاعده‌ای با بالاترین مرتبه است).

تجزیه‌گر پایین به بالا، شروع به اسکن ورودی می‌کند تا زمانی که با یکی از قواعد منطبق شود. سپس ورودی را با قاعده‌ی منطبق جایگزین می‌کند. این کار را تا جایی ادامه می‌دهد که به انتهای رشته‌ی ورودی‌ برسد. عبارات منطبق موقتی در پشته‌ی تجزیه‌گر قرار می‌گیرند.

Stack Input
2 + 3 - 1
term + 3 - 1
term operation 3 - 1
expression - 1
expression operation 1
expression -

این نوع از تجزیه‌گرهای بالا به پایین، تجزیه‌گرهای شیفیتی-کاهشی «shift-reduce» هم نامیده می‌شوند. چون ورودی به سمت راست شیفت داده می‌شود (تصور کنید که اشاره‌گر ابتدا به اولین مقدار ورودی اشاره می‌کند و بعد به سمت راست حرکت می‌کند) و به تدریج قواعد نحوی را تولید می‌کند.

ایجاد خودکار تجزیه‌گرها

ابزارهای وجود دارند که می‌توانند به طور خودکار یک تجزیه‌گر تولید کنند. به این صورت که شما به آنها گرامر زبان خود را می‌خورانید-گرامری که شامل دایره واژگان و قواعد دستوری است- و آن‌ها برای شما یک تجزیه‌گر واقعی ایجاد می‌کنند. ساختن یک تجزیه‌گر نیازمند درک بالایی از تجزیه‌گرهاست که در نوع خودش ساختن یکی از آنها با دست خالی که بهینه نیز باشد،کار چندان ساده‌ای نیست. به همین دلیل ایجاد کننده‌‌های خودکار تجزیه‌گرها ابزاری بسیار مفیدی در نوع خود به شمار می‌آیند.

وب‌کیت از دو ایجاد کننده‌ی خودکار تجزیه‌گرها استفاده می‌کند که عبارتند از: فِلِگس «Flex» برای ساختن لِگزر و بایسِن «Bison» برای ساختن یک تجزیه‌گر (که شاید اسم آنها تحت عنوان Lex و Yacc به گوشتان خورده باشد). ورودی فلگس فایلی است که متشکل شده از عبارات باقاعده‌ای که تُکن‌های معتبر را تعریف کرده است؛ و ورودی بایسن هم تشکیل شده از قواعد دستوری زبان که به فرمت BNF در آمده‌اند.

تجزیه‌گر HTML

کار این تجزیه‌گر این است که دستورات HTML را به درخت تجزیه تبدیل کند.

گرامر تعریف شده‌ی زبان HTML

واژگان و قواعد دستوری زبان HTML توسط سازمان W3C در مستندات کامل این زبان آورده شده است.

چرا گرامر HTML بدون چارچوب است؟!

همان طور که در معرفی تجزیه‌گرها دیدیم، قواعد گرامری می‌توانند به شکلی تعریف شوند که از فرمت‌های نظیر BNF استفاده کنند. متاسفانه هیچ یک از تجزیه‌گرهای معمول بحث شده در فصل قبلی را نمی‌توانیم برای تجزیه‌ی HTML‌ استفاده کنیم (من فصل قبل را صرفاً برای سرگرمی نیاوردم-از آن مفاهیم در فصل‌های بعدی در شرح تجزیه‌گرهای CSS و جاوااسکریپت استفاده خواهیم کرد). گرامر مورد نیاز تجزیه‌گرها برای HTML را نمی‌توان به راحتی توسط گرامرهای بدون چارچوب تعریف کرد.

یک فرمت مشخص برای تعریف گرامر HTML‌ به نام «Document Type Definition» وجود دارد که به اختصار به آن DTD می‌گویند. اما DTD نیز یک گرامر با چارچوب آزاد نیست. قبول دارم که این موضوع شاید در نگاه اول کمی عجیب به نظر برسد؛ زیرا HTML چیزی شبیه به XML‌ است. و خب تجزیه‌گرهای زیادی برای XML‌ وجود دارد. حتی یک نوع خاصی هم از HTML وجود دارد که به آن XHTML می‌گویند! پس تفاوت در چیست؟

تفاوت در این است که HTML زیادی بخشنده «forgiving» است! به طوری که او به شما خورده‌ای نمی‌گیرد اگه حواستان نباشد و یکی از تگ‌های اصلی را جا بیندازید (مانند head)، یا یادتان برود تگی را ببندید یا باز کنید و به همین منوال. به عبارتی HTML‌ دارای یه قاعده دستوری نرم و منعطف است، برخلاف آن چیزی که در XML‌ اتفاق می‌افتد.

همین اختلاف به ظاهر جزیی، باعث یک دنیا اختلاف شده است. از سوی دیگر، همین تفاوت است که باعث محبوبیت HTML شده است. او اشتباهات شما را نادیده می‌گیرد و زندگی را برایتان شیرین! و البته از سوی دیگر نوشتن یک گرامر مشخص را بسیار سخت می‌گرداند.

HTML DTD

گرامر HTML در قالب DTD تعریف می‌شود. این قالب برای تعریف زبان‌های خانواده‌ی SGML استفاده می‌شود؛ و شامل تعاریف تمامی عناصر مجاز به همراه خصوصیات و فرزندانشان است.

DOM

خروجی درخت تجزیه، درختی است به نام درخت عناصر DOM و خصوصیات این عناصر. DOM‌ اختصار شده‌ی Document Object Model است. این شی بیان کننده‌ی و نمایش‌دهنده‌ی سند HTML و رابط عناصر آن با دنیای بیرون مانند جاوااسکریپت است. به بیان دیگر، با این شی است که سند HTML و عناصر آن معنی می‌یابند و می‌توانند با لایه‌های دیگر ارتباط برقرار کنند.

ریشه‌ی درخت DOM، شی «Document» است. که تمامی اجزای دیگر از آن ریشه گرفته‌اند. DOM یک رابطه یک-به-یک با دستورات HTML دارد. برای مثال:

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

قطعه کد وقتی به درخت DOM‌ تبدیل شود، به شکل زیر در می‌آید:

درخت DOM تشکیل شده از مارک‌آپ مثال قبلی
درخت DOM تشکیل شده از قطعه کد بالا

الگوریتم تجزیه HTML

همان‌طور که در بخش قبلی دیدیم، بنا به این دلایل، HTML‌ را نمی‌توان با تجزیه‌گرهای معمول بالا به پایین یا پایین به بالا تجزیه کرد:

  1. ذات سخاوتمند HTML.
  2. این حقیقت که مرورگرها برای مواجه و پشتیبانی از موارد شناخته شده از دستورات نامعتبر HTML دارای یک مکانیزم سنتی چشم‌ پوشی از خطا هستند.
  3. فرآیند تجزیه‌ HTML‌ متاثر از منابع مختلف است؛ در صورتی که در زبان‌های دیگر منبع در طول مدت تجزیه و تبدیل محفوظ و بدون تغییر می‌ماند. این در حالی است که HTML با دارا بودن کدی پویا، که هر لحظه ‌می‌تواند تحت تاثیر یک عنصر بیرونی (مانند یک تگ script‌ که شامل چنین فراخوانی باشد: ()document.write) قرار بگیرد و با اضافه شدن یک تُکن خارجی، باعث شود تا کل نتیجه‌ی خروجی تغییر کند.

ناتوانی در استفاده از تجزیه‌گرهای معمول، باعث آن شده است که مرورگرها خودشان دست به کار شوند و دست به ساختن تجزیه‌گرهای HTML مختص به خودشان بزنند. نمونه‌ای از این الگوریتم‌های تجزیه‌گر با جزییات در مستندات HTML5 توضیح داده شده است.

این الگوریتم از دو بخش تشکیل شده است: علامت گذاری «tokenization» و ساخت درخت «tree construction».

نمودار چرخه‌ی تجزیه‌ی HTML
نمودار چرخه‌ی تجزیه‌ی HTML

علامت گذاری همان بررسی لِگزیکال‌ها است؛ تجزیه ورودی‌ها به تُکن‌ها. از جمله‌ی تُکن‌های HTML می‌توان به تگ‌های شروع و پایان، خصوصیت‌ها «attributes» و مقادیر این خصوصیت‌ها اشاره کرد.

علامت‌گذار، تُکنی را شناسایی می‌کند و بعد آن را به سازنده‌ی درخت می‌دهد. این چرخه تا جای تکرار می‌شود که تمامی ورودی‌ها علامت‌گذاری و در درخت جانمایی شده باشند. نمودار جریان زیر نشان دهنده‌ی همین چرخه‌ است.

چرخه‌ی تجزیه‌ی HTML
چرخه‌ی تجزیه‌ی HTML

حذف شده: شرح الگوریتم علامت گذاری - شرح الگوریتم ایجاد کننده‌ی درخت DOM

چه اتفاق‌هایی بعد از اتمام تجزیه HTML رخ می‌دهد؟

در این مرحله مرورگر شی document را به عنوان شی قابل تعامل نشانه‌گذاری می‌کند (این دقیقا همان لحظه است که رخداد DOMContentLoaded توسط مرورگر فراخوانی می‌شود) و اسکریپت‌های که به صورت «deferred» نشان‌گذاری شده‌اند اجازه‌ی اجرا پیدا می‌کنند (اسکریپت‌های که دارای این خصوصیت هستند، ملزم هستند تا انتهای تجزیه سند منتظر بمانند و سپس اجرا شوند. یعنی اجازه ندارند جلوی اجرای تجزیه‌گر و لود DOM را بگیرند). بعد از اجرای این اسکریپت‌ها حالت سند به «complete» تغییر کرده و مطابق آن رخداد «load» فراخوانی می‌شود.

چرا مرورگرها از خطاهای HTML‌ می‌گذرند؟!

از مرام مرورگرها به دور است که هنگام دیدن یک خطای دستوری «Invaild Syntax» هنگام اجرای یک سند HTML آن را بروز دهند چه برسد به اینکه بخواند جلوی اجرای آن را بگیرند. بلکه با نهایت تواضع و احترام، بیشتر خطاهای موجود را خود برطرف می‌کنند و به کارشان ادامه می‌دهند؛ بدون اینکه مزاحم شما شوند، گویی آب از آب تکان نخورده است! تمامی این فداکاری‌ها، فقط و فقط به خاطر کاربر نهایی و تجربه‌ی است که او قرار است در هنگام مرور یک صفحه‌ی وب داشته باشد!

به عنوان مثال این کد HTML‌ را در نظر بگیرید:

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

در کد بالا من از چند قاعده تخطی کرده‌ام («mytag» یک تگ استاندارد نیست، رعایت نکردن تقدم سطوح در تگ‌های «div» و «p» و موارد دیگر) ولی با این حال مرورگر هم‌چنان شکایتی ندارد و حتی به صورت درست سند را نمایش می‌دهد!

مدیریت خطاها یکی از اصول ثابت و اولیه در مرورگرها است، این در حالی است که هیچ کدام از این قواعد در کمال تعجب جز خصوصیات ذاتی تدوین شده‌ی برای HTML نیست! مانند بوکمارک کردن و دکمه‌های عقب و جلو،‌ این قابلیت نیز سال‌های متمادی که به صورت پیش‌فرض و به عنوان یک سری قوانین نانوشته توسط توسعه دهندگان مرورگرها پشتیبانی می‌شود. البته HTML5 تعدادی از این پیش‌نیاز‌ها را برای مدیریت خطاهای نحوی در مستندات خویش آورده است.

تجزیه CSS

مفاهیم گفته شده درباره‌ی تجزیه‌ی داده‌ها ورودی را که گفته شد یادتان هست؟ خب، برخلاف HTML، گرامر زبان CSS جزء گرامرهای بدون چارچوب محسوب ‌می‌شود و به وسیله‌ی همان نوع تجزیه‌گرهای که برایتان شرح داده شد، قابل تجزیه است. در حقیقت قواعد دستوری و واژگان CSS به طور مشخص و ثابت در مستندات آن آورده شده است. حالا اگر علاقه‌مند بودید که بفهمید دقیقا رفتار این تجزیه‌گر چگونه است و چطور دستورات را تجزیه می‌کند؛ می‌توانید به این‌ مدخل مراجعه کنید.

تجزیه‌گر CSS موتور وب‌کیت

دیدیم که وب‌کیت از دو ایجاد کننده‌ی خودکار تجزیه‌گر به نام‌های فلیگس و بایسون استفاده می‌کند؛ که برای برای ساختن خودکار تجزیه‌گر CSS، تنها کافی است که فایل‌های گرامر زبان CSS را به آنها بخوراند. اگر به یاد داشته باشید، گفتیم که بایسون یک پارسر پایین به بالایِ شیفتیِ-کاهنده می‌سازد؛ اما در مقابل فایرفاکس از یک پارسر بالا به پایین که خودش درست کرده است استفاده می‌کند. در هر دو مورد، فایل CSS به یک شی StyleSheet تجزیه و تبدیل می‌شود. هر کدام از اشیا در بر گیرنده‌ی قواعد CSS هستند. این اشیایِ قواعد CSS علاوه‌ بر اینکه شامل اشیایِ انتخابگرها «selector» و خصوصیاتشان «declaration» هستند، شامل دیگر اشیای مرتبطشان در گرامر CSS نیز می‌شوند. شکل زیر به طور کامل گویای این است که ما به چه چیزی یک شی StyleSheet‌ می‌گوییم و آن دربردارنده‌ی چه اشیایی است.

درخت حاصل از تجزیه‌ی CSS
درخت حاصل از تجزیه‌ی CSS

ترتیب پردازش اسکریپت‌ها و برگه‌های استایل

اسکریپت‌ها

نکته: زین پس ما به جای synchronous خواهیم گفت: ترتیبی و به جای asynchronous می‌گوییم: موازی

فرآیند کلی پردازش در وب، به صورت ترتیبی و پشت سر هم است. توسعه دهندگان انتظار دارند که اسکریپت‌ها به محض اینکه تجزیه‌گر به تگ آغازین <script> می‌رسد، تجزیه و اجرا شوند. به همین دلیل تجزیه‌گر HTML تا زمانی که اجرای اسکریپت به پایان برسد، متوقف خواهد ماند. در صورتی هم که اسکریپت از نوع خارجی باشد که به یک فایل بیرونی ارجاع داده شده باشد، تجزیه‌گر باید صبر کند تا داده‌های اسکریپت از طریق لایه‌ی شبکه دریافت و تحویل داده شود تا بعد از تجزیه و اجرای اسکریپت، تجزیه‌گر اصلی HTML دوباره کارش را بتواند ادامه دهد. که خود این عمل دریافت داده‌ها از لایه شبکه هم پردازشش به صورت ترتیبی خواهد بود. این مدل از فرآیند پردازشی برای سالیان متمادی است که وجود دارد و در مستندات نسخه‌های ۴ و ۵ HTML شرحش هم آمده است.

توسعه دهندگان در مواجه با این مشکل، آنقدرها هم بی‌چاره نیستند و می‌توانند با اضافه کردن خصوصیت «defer» به تگ script از متوقف شدن تجزیه‌گر HTML جلوگیری کنند و اجرای اسکریپت را به بعد از پایان یافتن کامل فرآیند دریافت و تجزیه‌ی HTML موکول کنند. ولی با این حال آن مشکل دریافت داده‌های اسکریپت‌های خارجی به صورت یک فرآیند ترتیبی همچنان به قوت خود باقی بود که با آمدن HTML5 و اضافه شدن خصوصیت «async» این مشکل هم تا حدودی مرتفع شد. اسکریپت‌های که دارای این خصوصیت باشند، توسط مرورگر در یک جریان «thread» جداگانه‌ای به صورت موازی با جریان‌های پردازشی دیگر دریافت و تجزیه می‌شوند. با این حال هنوز مرورگرهای مدرن هم برای انجام فرآیندهای موازی نیز با محدودیت مواجه هستند. به طوری که تنها می‌توانند در لایه‌ی شبکه به ۲ الی ۶ درخواست همزمان به صورت موازی رسیدگی کنند. (این ویژگی فقط مختص منابع خارجی مانند فایل‌های CSS، تصاویر، اسکریپت‌ها و قلم‌ها است.)

برگه‌های استایل «stylesheet»

از سوی دیگر برای پردازش برگه‌های استایل نسبت به اسکریپت‌ها از مدل پردازشی دیگر استفاده می‌شود. شاید بگویید که، از آنجایی که استایل شیت‌ها نمی‌توانند تغییری در درخت DOM بدهند، دلیلی ندارد که منتظر آنها بمانیم و اجازه بدهیم که کار تجزیه HTML را مختل کنند و جلوی آن را بگیرند! بله از نظر مفهومی حق با شماست، ولی اینجا یک مشکل داریم؛ آن هم این است که در زمانی که فرآیند اصلی تجزیه‌ی HTML در حال اجرا است، اسکریپتی اطلاعاتی از یکی از برگه‌های استایل لازم داشته باشد. اگر اطلاعات استایلی خواسته شده هنوز دریافت و تجزیه نشده باشند، در خوش بینانه‌ترین حالت اگه فرآیند اجرای اسکریپت متوقف نشود، باید انتظار یک دادن یک جواب نادرست را داشته باشیم.

این مسئله شاید به نظر یک مورد خاصی به نظر برسد که احتمال وقوع آن خیلی نادر باشد، ولی تجربه نشان داده که برعکس، خیلی هم رایج است. برای جلوگیری از وقوع همچین مشکلاتی، فایرفاکس تر و خشک را باهم می‌سوزاند. یعنی تا زمانی که تمامی برگه‌های استایل صفحه دریافت و تجزیه نشده باشند، اجرای تمامی اسکریپت‌ها را متوقف می‌کند؛ حتی آن‌هایی که کاری به کار استایل نداشته باشند. اما وب‌کیت با انصاف‌تر عمل می‌کند و تنها جلوی اجرای اسکریپت‌هایی را می‌گیرد که نیاز به اطلاعات برگه‌های استایلی داشته باشند که هنوز بارگذاری نشده‌اند.

تجزیه‌‌‌ی موازی

هر دوی موتورهای وب‌کیت و گِکو عمل تجزیه موازی را به صورت بهینه‌ای انجام می‌دهند. وقتی که اسکریپتی در حال اجرا است، جریان‌های دیگر، کار تجزیه‌ی و ترجمه مابقی HTML‌ را ادامه می‌دهند؛ تا بفهمند که چه منابعی باید از لایه شبکه دریافت و بارگذاری شوند؛ تا برای بارگذاریشان اقدام کنند. در این روش، منابع می‌توانند به صورت خطوط ارتباطی موازی از هم دریافت و بارگذاری شوند که حاصل این کار باعث کاهش زمان بارگذاری کل صفحه می‌شود. به همین دلیل به این روش تجزیه‌ی موازی می‌گویند.

البته توجه داشته باشید که این موضوع تجزیه‌گرهای موازی، تنها به تجزیه‌گرهایی مربوط می‌شود که به منابع خارجی مانند اسکریپت‌ها، برگه‌های استایل، تصاویر و فونت‌های اشاره دارند که درخت DOM را تغییر نمی‌دهند! - یعنی تجزیه‌گر اصلی HTML بدون توجه به فرآیند دریافت و بارگذاری آن‌ها می‌توانند کار خودش را انجام دهد؛ و کاری به کار هم نداشته باشند و هر کدام به طور مستقل بتوانند کارشان را به پایان ببرند.

ساخت درخت رندر

زمانی که درخت DOM تکمیل شد، مرورگر شروع به ایجاد درختی دیگر، به نام درخت رندر می‌کند. گره‌های تشکیل دهنده‌ی این درخت، عناصر گرافیکی صفحه هستند. این عناصر درست به همان ترتیبی که در صفحه نمایش باید ظاهر شوند، از بالا به پایین بر روی درخت رندر جاگذاری می‌شوند. هدف از تشکیل این درخت، آسان کردن لایه‌ی رنگرزی است که با کمترین دردسر بتواند با ترتیب درست به نقاشی عناصر تشکیل دهنده‌ی صفحه بپردازد.

فایرفاکس به این عناصر گرافیکی و به نوعی گره‌های درخت رندر، «فریم» می‌گوید. در حالی که وب‌کیت از لفظ «رندر شده‌ها» یا «اشیای رندر» برای آن‌ها استفاده می‌کند.

یک شی رندر می‌داند که چطور خود و بچه‌هایش را رنگرزی و صفحه بندی (لایه بندی؛ اینکه جایگاهش در صفحه، از نظر طول و عرض دقیقا کجای صفحه است) بکند.

کلاس RenderObject وب‌کیت که کدش را در قسمت زیر مشاهده می‌کنید، کلاس پایه‌ی همین اشیای رندر یا فریم‌ها هستند:

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 box معروف است که در CSS2 اضافه شده است (برای دیدن این مستطیل در مرورگر خود کافی است خاصیت outline عنصر مورد نظرتان را با 1px solid red مقدار دهی کنید). این ناحیه همان طور که گفته شد مشخص کننده‌ی محدوده‌ی عنصر بر روی صفحه نمایش است که نمود اطلاعات هندسی عنصر مانند طول و عرض و موقعیت عنصر در صفحه است.

نوع این ناحیه نیز متاثر از مقدار خصوصیت display استایل عنصر مورد نظر است که مطابق است با گره‌ی متناظرش. در قطعه کد زیر که از موتور وب‌کیت استخراج شده، موتور بر اساس آن تصمیم می‌گیرد که چه نوع شی رندری باید برای گره‌ی متناظر در درخت 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;
}

رابطه درخت رندر با درخت DOM

اشیای رندر یا همان فریم‌ها با گره‌های درخت DOM در ارتباط هستند؛ ولی رابطه‌ی آن از نوع یک-به-یک نیست. عناصر غیر-گرافیکی DOM (همان‌های که در صفحه نمایش داده نمی‌شوند و مقدار display آنها برابر با none است) در درخت رندر آورده نمی‌شوند. مانند تگ “head” که همیشه مخفی است (مقدار خصوصیت display تگ “head” به صورت پیش فرض برابر با none است). اما این برای عناصر که خصوصیت visibility آن‌ها برابر با hidden است، صدق نمی‌کند و این عناصر در درخت رندر آورده می‌شوند.

عناصری از DOM هستند که به طور همزمان با چند عنصر گرافیکی در ارتباط هستند. این عناصر معمولا دارای ساختار پیچیده‌ای هستند که نمی‌شود آنها را تنها با یک ناحیه‌ی مستطیلی تنها توصیف کرد. مانند عنصر “select” که خود به ۳ شی رندر مرتبط است؛ که عبارتند از: یکی برای ناحیه نمایشی (مستطیل اصلی)، یکی برای لیست باز شونده (همان “drop down”) و یکی نیز برای دکمه. و البته وقتی که متن هر یک از موارد لیست یا دکمه به خاطر نامناسب بودن عرض تعیین شده برایش به چند خط بشکند، برای هر خط، درخت رندر یک شی رندر در نظر می‌گیرد.

مثالی دیگر از این عناصر چند شی ای می‌توان به استفاده‌ی نابجای عناصر HTML اشاره کرد. طبق مستندات CSS یک عنصر inline یا باید در بر گیرنده‌ی فقط یک عنصر از نوع block باشد یا فقط از نوع inline. در صورتی که از ترکیب این دو نوع باهم در درون آن استفاده شود باعث ایجاد یک شی block بی نام و نشان می‌شود که عناصر inline را در بر گرفته است (به اصطلاح آنها را “wrap” می‌کند).

بعضی از اشیای رندر «فریم» هم هستند که به یک گره از DOM مرتبط شده‌اند؛ ولی نه به همان ترتیبی که در درخت رندر قرار گرفته‌اند. عناصر float و absolute-ی که خارج از جریان کلی صفحه موقعیت دهی شده باشند؛ در جای دیگری، از آنجایی که در درخت رندر برایشان تعریف شده است، جانمایی می‌شوند.

the-render-tree-and-the-corresponding-dom-tree
درخت رند و درخت DOM متناظر با آن. Viewport یک بلاک در برگیرنده‌ای اولیه است که به صورت پیش فرض فراخوانی می‌شود. در وب‌کیت به عنوان یک شی RenderView شناخته می‌شود.

فرآیند ساخت درخت رندر

در فایرفاکس، ایجاد کننده درخت رندر به عنوان یک منتظر «listerner» برای آپدیت‌های DOM عمل می‌کند. به طوری که او عمل ساختن فریم را به متد اصلی سازنده فریم FrameConstructor محول «delegate» می‌کند و این متد سازنده است که استایل‌ها را محاسبه می‌کند (ببینید بخش محاسبه استایل را) و فریم را تشکیل می‌دهد.

در وب‌کیت، فرآیند محاسبه استایل و تشکیل شی‌های رندر، ضمیمه «attachment» نامیده می‌شود. هر گره‌ی درخت DOM، یک متد attach دارد. فرآیند اجرای ضمیمه هم به صورت ترتیبی است؛ به این صورت که وقتی گره‌ای به درخت DOM اضافه می‌شود، متد attach گره‌ی تازه از راه رسیده فراخوانی می‌شود.

به موجب پردازش دو تگ html و body صفحه‌‌ی HTML گره‌ی ریشه‌ی درخت رندر ساخته می‌شود. شی ریشه‌ی درخت رندر طبق آن چیزی که در مستندات رسمی CSS آمده است، یک بلوک در برگیرنده با این تعریف است: بلوکی با بالاترین سطح، که در برگیرنده‌ی تمامی بلوک‌های موجود در صفحه است. از طرفی هم این همان بلوکی است که به شی «viewport» ابعاد می‌دهد (منظور همان ابعاد ناحیه‌ای است که پنجره نمایش مرورگر در صفحه نمایشگر اشغال کرده است). فایرفاکس این بلوک را ViewPortFrame می‌خواند و وب‌کیت نیز آن را RenderView. این شی دقیقا همان شی است که گره‌ی document از درخت DOM به آن اشاره می‌کند. مابقی درخت رندر هم با جانمایی گره‌های بعدی درخت DOM در گره‌های متناظرشان در درخت رندر به عنوان فرزندان شی RenderView ساخته می‌شود.

محاسبه‌ی استایل

لازمه‌ی ساختن درخت رندر، محاسبه‌ی خصوصیات گرافیکی هر کدام از اشیای آن است؛ که این امر از طریق محاسبه‌ی تک تک خصوصیات استایلی مربوط به هر عنصر صفحه قابل حصول است.

وقتی می‌گوییم استایل، منظورمان کلیه‌ی صفحه‌هات استایلی است که از مبادی مختلف گرد آوری شده‌اند. این مبادی عبارتند از:‌ استایل‌های درونی عناصر «inline» (منظور هم آنهایی است که به صورت خصوصیت style برای یک تگ تعریف می‌شوند و هم آنهایی که در داخل تگ <style> قرار می‌گیرند.)، خصوصیت‌های گرافیکی HTML (مانند bgcolor)، فایل‌های CSS بیرونی و در نهایت استایل پیش‌فرضی که خود مرورگر تعریف کرده است که به آنها «مصدر» “origin” می‌گویند.

اصلا چرا می‌گوییم «محاسبه‌ی» استایل‌ها؟! نمی‌گوییم مثلا اضافه کردن استایل‌ها. به خاطر این‌که همان طور که در بند قبلی به آن اشاره شد، استایل‌های یک عنصر از مبادی متعددی قابل حصول هستند که هر یک نسبت به دیگری اولویت خاصی دارد و از طرفی نحوه‌ی تعریف کردن قواعد استایل‌ها در صفحه‌هات استایلی نیز دارای ترتیب و نظم خاصی است که سطوحی از اولویت‌های مختلف را شامل می‌شوند؛ که همه‌ی این‌ها نیازمند یک سری محاسباتی ریاضی است تا ترتیب صحیح نحوه‌ی اِعمال این استایل‌ها مشخص شود. به دلیل این پیچیدگی‌ها، محاسبات استایل‌ها برای مرورگرها با مشکلات و سختی‌هایی همراه است که آنها عبارتند از:

  1. داده‌های استایلی یک سازه‌ی عظیمی از ساختمان داده‌ها را ایجاد می‌کند که نگهداری این حجم عظیم از خصوصیات، بعضا در سایت‌های بزرگ باعث بروز مشکلات مربوط به کمبود حافظه (از نوع رَم) می‌شود.
  2. پیدا کردن قواعد استایلی مربوط به هر عنصر در صورتی که بهینه نباشند، باعث بروز مشکلاتی در کارایی (منظور همان پرفورمنس است) بارگذاری صفحه می‌شود. پیمایش لیست کامل قواعد استایلی برای پیدا کردن عناصر مورد نظر، بار پردازشی سنگینی بر دوش مرورگر می‌گذارد. انتخابگرها «selector» می‌توانند از ساختار پیچیده‌ای تشکیل داشته باشند که موجب شود در فرایند پردازش تعیین مسیر درست بر روی درخت DOM به مسیرهایی بر بخوریم که در ابتدا به نظر امید بخش و قابل حصول می‌رسند، ولی در آخر معلوم شود که مسیر اشتباهی بوده و همین طور برعکس. برای مثال بیاید انتخابگر زیر بررسی کنیم:
div div div div {
  ...
}

انتخابگر بالا به این معنی است که قواعد مورد نظر باید به یک تگ «div» که خودش زیر مجموعه‌ی سه تگ «div» است، اعمال شود. از آنجایی که انتخابگر‌ها از راست به چپ خوانده می‌شوند، ما در پایین‌ترین سطح درخت، یعنی برگ‌ها، تمامی گره‌های «div» را مشخص می‌کنیم و بعد آنهایی را نگهداریم که در گره‌های والدش یک تگ «div» باشد؛ و این کار را تا جایی به سمت بالا ادامه می‌دهیم که تمام مسیر‌هایی که روی درخت مطابق با قاعده‌ی انتخابگر ما است، حاصل شود. حالا مشکل جایی بروز می‌کند که ما مسیری را ممکن است طی کنیم که نهایتا به دو تگ تو در توی «div» یا ۳ تگ تو در تو می‌انجامد که خب، هیچ کدامشان قابل قبول نیستند. همین رفتن و برگشتن‌های بی‌حاصل مرورگر، وقتی تعداد انتخابگر‌های یک سایت بیش تر از چند صد تا باشد، باعث کاهش کارایی آن خواهد شد. 3. پیچیدگی اعمال قواعد یا همان خصوصیات گرافیکی عناصر با در نظر گرفتن ترتیب و اولیت‌های تعریف شده برای قواعد.

خوش بختانه مرورگرها نرم‌افزارهای پرتلاشی هستند و سعی کرده‌اند تا به روش‌های مختلف و با بهره بردن از الگوریتم‌های بهینه تا آنجایی که امکان دارد بر این مشکلات فایق آیند؛ ولی شکی نیست که توسعه‌دهندگان باید به روش‌های ناصحیح و صحیح توسعه‌ی CSS تسلط کامل داشته باشند تا بتوانند تا آنجایی که می‌توانند کار را برای مرورگر‌ها آسان‌تر کنند؛ که در نهایت به کمک هم بتوانند یک تجربه‌ی فوق العاده برای کاربر رقم بزنند.

فرآیند تدریجی و گام به گام تجزیه‌ی CSS

وب‌کیت از یک پرچم «flag» برای نشان‌دار کردن تمامی صفحات استایلی (حتی imports@) که به صورت کامل بارگذاری شده است، استفاده می‌کند. اگر استایلی هنگام عملیات ضمیه کردن «attaching» به طور کامل بارگذاری نشده باشد، استایل پیش فرض اعمال می‌شود و وقتی که استایل بارگذاری شد، دوباره محاسبات مربوط به آن عنصر انجام می‌گیرد.

صفحه بندی «layout»

وقتی که اشیای رندر ایجاد شدند و به درخت رندر اضافه می‌شوند، هنوز اندازه «size» و موقیعتشان «position» در صفحه مشخص نشده است. محاسبه این مقادیر بر عهده‌ی قسمت صفحه بندی یا باز جانمایی «reflow» است.

سند HTML از یک مدلی شبیه حرکت جریان رود «flow» برای صفحه بندی استفاده می‌کند. بدان معنی که بیشتر اوقات ممکن است که محاسبات هندسی تنها در یک جهت صورت بگیرد (اشاره به جریان آب رودخانه دارد که تنها به یک سو و جهت شناور است). عناصری که بعداً به جریان صفحه اضافه ‌می‌شوند، عناصری که از قبل در جریان بوده‌اند را از لحاظ مقادیر هندسی‌شان، تحت تاثیر قرار نمی‌دهند. به همین دلیل از آنجایی که جهت متون در HTML به خاطر زبان انگلیسی به طور پیش فرض چپ به راست در نظر گرفته شده است، این جریان صفحه بندی عناصر صفحه نیز تحت تاثیر آن به طور پیش فرض به صورت «چپ به راست» و از «بالا به پایین» است.

layout-rect-calculate-the-position-size
مثالی از نحوه‌ی محاسبه مختصات یک پاراگراف و صفحه بندی آن

سیستم مختصات دهی عناصر صفحه، وابسته به فریم ریشه است؛ که به طور پیش فرض مبدا آن در گوشه‌ی بالایی سمت چپ صفحه مرورگر است در زبان‌های چپ به راست. این نقطه با مختصات 0,0 مشخص می‌شود.

فرآیند پردازشی مربود به صفحه بندی به صورت بازگشتی است. این فرآیند از شی رندر ریشه که به تگ «html» اشاره می‌کند، شروع می‌شود و به صورت بازگشتی با پیمایش بعضی یا تمامی فریم‌های زیر مجموعه‌ای خود و محاسبه اطلاعات هندسی هر کدام از فریم‌ها که نیاز بود ادامه پیدا می‌کند. تمامی فریم‌ها دارای یک متد layout یا reflow هستند، که در صورت نیاز این وظیفه‌ی والد است که متد صفحه بندی فرزندانش را فراخوانی کند. در ویدیوی زیر به راحتی می‌توانید فرآیند صفحه بندی در مرورگر فایرفاکس را مشاهده کنید. (ویدیو بر روی سرویس یوتوب آپلود شده و برای دیدن آن نیاز به پراکسی دارید متاسفانه.)

سیستم کمی آلوده «dirty bit»

صفحه بندی فرآیند پر هزینه‌ است، خصوصا برای دستگاه‌های که از منابع پردازشی پایین‌تری بهره می‌برند؛ مانند گوشی‌های همراه. مرورگر برای اینکه برای هر تغییر کوچکی در صفحه مجبور نشود که کلا صفحه را دوباره صفحه بندی کند، از سیستم «کمی/مقداری/ذره‌ای-چرک/کثیف/آلوده» استفاده می‌کند. این سیستم هر فریمی که تغییر یا اضافه می‌شود به درخت رندر را، خودش و فرزندانش را به عنوان «آلوده» علامت گذاری می‌کند؛ که این یعنی، این ناحیه (فریم) نیاز به صفحه بندی مجدد دارد.

در کل دو حالت در این سیستم وجود دارد: «آلوده» و «بچه‌های آلوده»؛ که این دومی به این معنی که ممکن است خود فریم تمیز باشد و نیازی به صفحه بندی مجدد نداشته باشه، ولی بچه‌هایش آلوده شده باشند و نیازمند صفحه‌بندی مجدد باشند.

صفحه بندی: تدریجی و عمومی

صفحه بندی می‌تواند بر کل درخت رندر، اِعمال شود؛ که به این نوع می‌گوییم، صفحه بندی عمومی «global». صفحه بندی عمومی در صورتی که اتفاقات زیر رخ دهد بر روی درخت رندر اِعمال می‌شود:

  1. یکی از استایل‌های عمومی تغییر کند. مثلا اگه اندازه‌ی فونت تغییر کند، تمامی فریم‌ها تحت تاثیر قرار می‌گیرند.
  2. در صورتی که پنجره مرورگر تغییر سایز بدهد.

نوع دیگر از صفحه بندی، صفحه بندی به صورت تدریجی «incremental»است؛ که وقتی که تغییرات رخ داده در حد یک ناحیه‌ی محلی باشد، که باعث تنها آلوده شدن یک فریم خاص از درخت رندر شده باشد بر روی درخت رندر اِعمال می‌شود. این نوع صفحه بندی به صورت موازی «asynchronously» بر فریم‌ها اعمال می‌شود. برای مثال وقتی که یک فریم جدید به درخت رندر اضافه می‌شود، فرآیند صفحه بندی صبر می‌کند تا اگر محتوای دیگری هم از لایه شبکه قرار است برسد به طور کامل بارگذاری و به درخت DOM‌ اضافه شود و بعد از آن فریم‌ جدید را صفحه بندی کند.

Incremental-layout
صفحه بندی تدریجی - تنها فریم‌های کثیف و فرزندانشان صفحه بندی می‌شوند.

صفحه بندی: ترتیبی و موازی

همان طور که اشاره شد، صفحه بندی تدریجی به صورت موازی «asynchronously» اعمال می‌شود. فایرفاکس دستورات باز نشانی را برای اعمال صفحه بندی تدریجی صف بندی می‌کند و با تعیین یک زمان بندی به صورت دسته‌ای اقدام به اعمال این دستورات بر روی فریم‌ها می‌کند. در مقابل، وب‌کیت نیز دارای یک تایمر است که صفحه بندی تدریجی را اجرا می‌کند - به این صورت که درخت رندر را برای پیدا کردن فریم‌هایی که آلوده شده‌اند پیمایش می‌کند.

وقتی که اسکریپتی در زمان اجرا، درخواست اطلاعات نظیر مقدار offsetHeight را از درخت استایل بکند، موجب این می‌شود که صفحه بندی دیگر نتواند به صورت موازی اعمال شود؛ و در این حالت باید به صورت ترتیبی انجام گیرد. صفحه بندی‌های عمومی هم معمولاً به صورت ترتیبی اعمال می‌شوند. نتایج این نوع تغییرات عمومی اصلاً خوش آیند مرورگر نیست. خصوصا در دستگاه‌‌های همراه که نیازمند آن هستند که محتوا را در فریم‌های ۶۰ ثانیه‌ای رندر و نمایش دهند. اگر عمر باقی بود سعی می‌کنم در آیند بیشتر در این بار بنویسم. اما فعلاً برای آشنایی بیشتر با فرآیند صفحه بندی و نحوه محاسبات هندسی عناصر می‌توانید به این پیوند‌ها مراجع کنید.

رنگرزی «painting»

در این مرحله، درخت رندر، گره به گره پیمایش می‌شود و با فراخوانی متد paint هر شی رندر، باعث می‌شود تا محتوای این گره‌ها بر روی صفحه نمایش رسم و نقاشی شوند. لایه‌ی رنگرزی از کلاس‌های رابط کاربری سیستم عامل استفاده می‌کند.

انواع فرآیندهای رنگرزی: عمومی و تدریجی

همانند لایه‌ی صفحه بندی، لایه‌ی رنگرزی هم می‌توانند به صورت عمومی - وقتی که کل درخت رندر نیازمند نقاشی باشد - و نیز تدریجی رخ دهد. رنگرزی تدریجی وقتی اعمال می‌شود که تغییر رخ داده تنها بر برخی از گره‌های درخت رندر تاثیر گذاشته باشد، نه بر کل درخت.

با تغییر کردن یک فریم، ناحیه‌ی مستطیلی آن بر روی صفحه نمایش غیر معتبر می‌شود (شما همان حالت آلوده شدن در صفحه بندی را در نظر بگیرید) که این امر موجب آن می‌شود تا سیستم عامل با دیدن یک «منطقه‌ی آلوده» رخداد رنگرزی را فراخوانی کند. سیستم عامل این عمل را بسیار هوشمندانه انجام می‌دهد؛ به این ترتیب که سعی می‌کند تا آنجایی که امکان دارد این مناطق را در هم ادغام کند تا از بار پردازشی بکاهد. وقتی فریمی تغییر می‌کند، پیامی حاوی نشانی محدوده‌ی آلوده شده به ریشه درخت رندر فرستاده می‌شود و پیمایش درخت برای پیدا کردن گره‌ی آلوده شده شروع می‌شود؛ و تا زمانی که گره‌ی مورد نظر پیدا شود و متد paint آن (و در صورت نیاز فرزندانش نیز) برای رنگرزی دوباره فراخوانی شود.

در مرورگر کروم این عملیات از این هم پیچیده‌تر است؛ چون فرآیند جداگانه‌ای از فرآیند اصلی برای این کار در نظر گرفته شده است. مدیریت این فرآیند به کل بر عهده‌ی پردازنده‌ی واحد گرافیکی «GPU» سیستم است؛ که مسول این چنین فرآیندهایی است. برای به کار گیری این پردازنده، کروم با کمی تغییرات تقریبا رفتار سیستم عامل را شبیه سازی کرده و در کنترل این پردازنده مانند آن عمل می‌کند. قبل از رنگرزی مجدد، وب‌کیت وضعیت ناحیه آلوده را قبل از آلوده شدن به صورت یک تصویر «bitmap» ذخیره می‌کند؛ و سپس با مقایسه تصاویر قبل و بعد از آلودگی، تنها نواحی را که آلوده شده اند؛ مجدد اقدام به رنگرزی می‌کند.

ترتیب اولویت‌های رنگرزی

در مستندات CSS2 فرآیند رنگرزی الویت بندی شده است . به موجب این اولویت بندی، فرآیند رنگرزی از عقب ترین بلوک آغاز می‌شود تا به جلویی ترین بلوک برسد (جلویی ترین بلوک، نزدیک‌ترین بلوک به کاربر است در صفحه نمایش؛ که از همان سیستم «z-index» پیروی می‌کند). اولویت بندی بلوک‌های هر فریم به صورت زیر است:

  1. رنگ پس زمینه
  2. تصویر پس زمینه
  3. حاشیه «border»
  4. فرزندان
  5. نوار بیرونی «outline»

انعطاف در برابر تغییرات

مرورگرها سعی می‌کنند تا آنجایی که امکان پذیر است با انجام کمترین کار نسبت به تغییرات محتوا و رفتارهای کاربر واکنش نشان دهند؛ به همین دلیل، همه تغییرات دارای هزینه‌ی یکسانی برای مرورگر نیستند. برای مثال تغییر رنگ یک عنصر، تنها باعث انجام عملیات رنگرزی مجدد بر روی خود آن عنصر می‌شود. یا تغییرات موقعیت یک عنصر باعث این می‌شود که مرورگر مجبور شود عملیات‌های صفحه بندی و رنگرزی را برای خود آن عنصر، فرزندانش و همسایگانش نیز انجام دهد. وقوع تغییرات بنیادی مانند تغییر سایز فونت تگ «html»، موجب نامعتبر شدن کش‌های ایجاد شده توسط موتور رندر شده و موتور مجبور می‌شود تا کل درخت رندر را دوباره از نو صفحه بندی و رنگرزی کند.

این وظیفه‌ی توسعه دهندگان است که مراقب باشند تا مرورگر چه از سوی کد آن‌ها به طور مستقیم و چه به واسطه‌ی رفتار کاربران به طور غیر مستقیم دچار چنین زحماتی نشود و تا جایی که امکان دارد از رفتارهایی که باعث تغییرات بنیادی می‌شود، اجتناب کند. من سعی خواهم کرد تا در مطالب آتی در این باره به طور مفصل صحبت کنم. زیرا این مبحث یکی از اصلی‌ترین عوامل تاثیرگذار بر کارایی وب سایت‌های واکنش‌گرا در مرورگرهای موبایل است؛ ولی تا آن زمان می‌توانید نقداً با مراجعه به سایت csstriggers.com از هزینه‌ی تغییر هر یک از خصوصیات استایلی عناصر صفحه آگاه شوید.

جریان‌های موتور رندر کننده

موتور رندر تنها دارای یک جریان «thread» است. تقریبا همه فرآیندها در مرورگرها به جز عملیات‌های شبکه‌ای، توسط همین جریان پردازش می‌شوند. در فایرفاکس و سافاری، این جریان، جریان اصلی پردازشی مرورگر است. در کروم نیز همان طور که پیش از این عنوان شد، هر تب، یک جریان اصلی مخصوص به خودش را دارد.

تنها لایه‌ی شبکه است که مجاز است به طور موازی و همزمان عملیات‌های خودش را در قالب جریان‌های جداگانه از هم انجام دهد. البته مرورگر تعداد کانکشن‌های که به این صورت می‌تواند برقرار کند، محدود است (معمولا بین ۲ الی ۶ کانکشن متغییر است).

حلقه‌ی رخداد

از طرفی، جریان اصلی مرورگرها یک حلقه‌ی بی‌نهایت از نوع رخدادها «event» است. وظیفه‌ی اصلی این حلقه بی‌نهایت، هوشیار نگاه داشتن جریان اصلی نسبت به تغییرات احتمالی (مانند رخدادهای که موجب صفحه بندی و رنگرزی مجدد می‌شوند) و واکنش نشان دادن نسبت به آنها است. کد زیر نشان دهنده‌ی حلقه‌ی رخداد اصلی در مرورگر فایرفاکس است:

while (!mExiting)
  NS_ProcessNextEvent(thread);

سخن پایانی

مایه‌ی اصلی این متن، بر اساس تحقیقات توسعه دهنده‌ی اسرائیلی‌، خانوم «Tali Garsiel»، که در سایت خود منتشر کرده‌اند زده شده است. در بعضی از قسمت‌ها که به نظرم رسیده نیاز به عمیق شدن به آن شدتی که ایشان شده اند، نبوده؛ من راه خودم را از ایشان جدا کرده ام و سعی کردم، یا یک شرح مختصر از کلیات آن قسمت داشته باشم؛ یا به کل آن‌ها را نادیده بگیرم. در انتهای این بخش‌ها به همان قسمت از مقاله‌ی اصلی ارجاع داده‌ام تا دوستان علاقمند بتوانند راحت‌تر به عمق‌های بعدی نفوذ کنند. در مواردی هم که احساس کرده‌ام نیاز به توضیح بیشتر بوده، سعی کرده‌ام تا آن جایی که سوادم اجازه می‌داده مطلب را بیشتر بشکافم. در هر صورت امیدوارم از خواندن این ترجمه-تالیف لذت برده باشید.

به پایان آمد این دفتر، حکایت هم چنان باقی است… .

اگر نشانگر بارگزاری زیر چرخید و هیچ اتفاقی نیفتاد و کامنت‌ها لود نشد، با عرض پوزش باید عرض کنم که متاسفانه شما بدون پراکسی اینجا هستید و از اونجایی که فیلترچی سرویس مورد نظر را مسدود کرده است، تا وقتی که با پراکسی صفحه را مجدداْ لود نفرمایید نمی‌توانید کامنت‌ها را مشاهده کنید :((