เบื้องหลังเว็บเบราว์เซอร์สมัยใหม่
บทนำ
คู่มือที่ครอบคลุมเกี่ยวกับการทำงานภายในของ WebKit และ Gecko นี้เป็นผลมาจากการวิจัยจำนวนมากที่ดำเนินการโดย Tali Garsiel นักพัฒนาซอฟต์แวร์ชาวอิสราเอล ในช่วง 2-3 ปีที่ผ่านมา เธอได้ตรวจสอบข้อมูลที่เผยแพร่ทั้งหมดเกี่ยวกับข้อมูลภายในของเบราว์เซอร์ และใช้เวลาส่วนใหญ่ในการอ่านซอร์สโค้ดของเว็บเบราว์เซอร์ เธอเขียนว่า
ในฐานะนักพัฒนาเว็บ การเรียนรู้การทำงานภายในของเบราว์เซอร์จะช่วยให้คุณตัดสินใจได้ดีขึ้นและทราบเหตุผลเบื้องหลังแนวทางปฏิบัติแนะนำในการพัฒนา แม้ว่าเอกสารนี้จะค่อนข้างยาว แต่เราขอแนะนำให้คุณใช้เวลาศึกษาอย่างละเอียด แล้วคุณจะชอบ
Paul Irish, Chrome Developer Relations
บทนำ
เว็บเบราว์เซอร์เป็นซอฟต์แวร์ที่มีการใช้งานอย่างแพร่หลายที่สุด ในคู่มือเบื้องต้นนี้ ผมจะอธิบาย
วิธีการทำงานเบื้องหลังของโมเดลเหล่านี้ เราจะดูว่าจะเกิดอะไรขึ้นเมื่อคุณพิมพ์ google.com ในแถบที่อยู่จนกว่าคุณจะเห็นหน้า Google บนหน้าจอเบราว์เซอร์
เบราว์เซอร์ที่เราจะพูดถึง
ปัจจุบันมีเบราว์เซอร์หลัก 5 รายการที่ใช้บนเดสก์ท็อป ได้แก่ Chrome, Internet Explorer, Firefox, Safari และ Opera ในอุปกรณ์เคลื่อนที่ เบราว์เซอร์หลักๆ ได้แก่ Android Browser, iPhone, Opera Mini และ Opera Mobile, UC Browser, เบราว์เซอร์ Nokia S40/S60 และ Chrome ซึ่งทั้งหมดนี้ (ยกเว้นเบราว์เซอร์ Opera) ใช้ WebKit เราจะยกตัวอย่างจากเบราว์เซอร์โอเพนซอร์สอย่าง Firefox และ Chrome รวมถึง Safari (ซึ่งเป็นโอเพนซอร์สบางส่วน) จากสถิติของ StatCounter (ข้อมูล ณ เดือนมิถุนายน 2013) Chrome, Firefox และ Safari มีสัดส่วนการใช้งานเบราว์เซอร์ในเดสก์ท็อปทั่วโลกประมาณ 71% ในอุปกรณ์เคลื่อนที่ เบราว์เซอร์ Android, iPhone และ Chrome มีสัดส่วนการใช้งานประมาณ 54%
ฟังก์ชันหลักของเบราว์เซอร์
ฟังก์ชันหลักของเบราว์เซอร์คือการแสดงทรัพยากรบนเว็บที่คุณเลือก โดยการส่งคำขอไปยังเซิร์ฟเวอร์และแสดงทรัพยากรนั้นในหน้าต่างเบราว์เซอร์ โดยปกติแล้วทรัพยากรจะเป็นเอกสาร HTML แต่อาจเป็น PDF, รูปภาพ หรือเนื้อหาประเภทอื่นๆ ก็ได้ ผู้ใช้จะระบุตำแหน่งของทรัพยากรโดยใช้ URI (Uniform Resource Identifier)
วิธีที่เบราว์เซอร์ตีความและแสดงไฟล์ HTML จะระบุไว้ในข้อกำหนด HTML และ CSS ข้อกำหนดเหล่านี้ได้รับการดูแลโดยองค์กร W3C (World Wide Web Consortium) ซึ่งเป็นองค์กรมาตรฐานสำหรับเว็บ เป็นเวลาหลายปีที่เบราว์เซอร์เป็นไปตามข้อกำหนดเพียงบางส่วนและพัฒนาส่วนขยายของตนเอง ซึ่งทำให้เกิดปัญหาความเข้ากันได้อย่างร้ายแรงสำหรับผู้เขียนเว็บ ปัจจุบันเบราว์เซอร์ส่วนใหญ่เป็นไปตามข้อกำหนดไม่มากก็น้อย
อินเทอร์เฟซผู้ใช้ของเบราว์เซอร์มีหลายอย่างที่เหมือนกัน องค์ประกอบอินเทอร์เฟซผู้ใช้ที่พบบ่อยมีดังนี้
- แถบที่อยู่สำหรับแทรก URI
- ปุ่มย้อนกลับและไปข้างหน้า
- ตัวเลือกการบุ๊กมาร์ก
- ปุ่มรีเฟรชและหยุดสําหรับรีเฟรชหรือหยุดการโหลดเอกสารปัจจุบัน
- ปุ่มหน้าแรกที่นำคุณไปยังหน้าแรก
น่าแปลกที่อินเทอร์เฟซผู้ใช้ของเบราว์เซอร์ไม่ได้ระบุไว้ในข้อกำหนดอย่างเป็นทางการใดๆ แต่มาจากแนวทางปฏิบัติแนะนำที่กำหนดขึ้นจากประสบการณ์หลายปีและจากการที่เบราว์เซอร์เลียนแบบกันและกัน ข้อกำหนด HTML5 ไม่ได้กำหนดองค์ประกอบ UI ที่เบราว์เซอร์ต้องมี แต่แสดงรายการองค์ประกอบทั่วไปบางอย่าง ซึ่งรวมถึงแถบที่อยู่ แถบสถานะ และแถบเครื่องมือ แน่นอนว่ามีฟีเจอร์ที่ใช้ได้เฉพาะในเบราว์เซอร์บางตัว เช่น Download Manager ของ Firefox
โครงสร้างพื้นฐานระดับสูง
องค์ประกอบหลักของเบราว์เซอร์มีดังนี้
- อินเทอร์เฟซผู้ใช้: ซึ่งรวมถึงแถบที่อยู่ ปุ่มย้อนกลับ/ไปข้างหน้า เมนูบุ๊กมาร์ก ฯลฯ ทุกส่วนของจอแสดงผลเบราว์เซอร์ ยกเว้นหน้าต่างที่คุณเห็นหน้าที่ขอ
- เครื่องมือเบราว์เซอร์: จัดระเบียบการดำเนินการระหว่าง UI กับเครื่องมือแสดงผล
- เครื่องมือแสดงผล: มีหน้าที่แสดงเนื้อหาที่ขอ ตัวอย่างเช่น หากเนื้อหาที่ขอเป็น HTML เครื่องมือแสดงผลจะแยกวิเคราะห์ HTML และ CSS แล้วแสดงเนื้อหาที่แยกวิเคราะห์แล้วบนหน้าจอ
- เครือข่าย: สำหรับการเรียกใช้เครือข่าย เช่น คำขอ HTTP โดยใช้การติดตั้งใช้งานที่แตกต่างกันสำหรับแพลตฟอร์มต่างๆ ที่อยู่เบื้องหลังอินเทอร์เฟซที่ไม่ขึ้นอยู่กับแพลตฟอร์ม
- แบ็กเอนด์ UI: ใช้สำหรับวาดวิดเจ็ตพื้นฐาน เช่น ช่องตัวเลือกรวมและหน้าต่าง แบ็กเอนด์นี้แสดงอินเทอร์เฟซทั่วไปที่ไม่เจาะจงแพลตฟอร์ม โดยจะใช้วิธีการอินเทอร์เฟซผู้ใช้ของระบบปฏิบัติการที่อยู่เบื้องหลัง
- โปรแกรมแปลภาษา JavaScript ใช้เพื่อแยกวิเคราะห์และเรียกใช้โค้ด JavaScript
- พื้นที่เก็บข้อมูล นี่คือเลเยอร์ความต่อเนื่อง เบราว์เซอร์อาจต้องบันทึกข้อมูลทุกประเภทไว้ในเครื่อง เช่น คุกกี้ นอกจากนี้ เบราว์เซอร์ยังรองรับกลไกการจัดเก็บข้อมูล เช่น localStorage, IndexedDB, WebSQL และ FileSystem ด้วย
โปรดทราบว่าเบราว์เซอร์อย่าง Chrome จะเรียกใช้เครื่องมือแสดงผลหลายอินสแตนซ์ โดย 1 อินสแตนซ์ต่อ 1 แท็บ แต่ละแท็บจะทำงานในกระบวนการที่แยกกัน
เครื่องมือแสดงผล
หน้าที่ของเครื่องมือแสดงผลคือการแสดงผล ซึ่งก็คือการแสดงเนื้อหาที่ขอในหน้าจอเบราว์เซอร์
โดยค่าเริ่มต้น เครื่องมือแสดงผลจะแสดงเอกสารและรูปภาพ HTML และ XML ได้ โดยสามารถแสดงข้อมูลประเภทอื่นๆ ผ่านปลั๊กอินหรือส่วนขยาย เช่น แสดงเอกสาร PDF โดยใช้ปลั๊กอินโปรแกรมอ่าน PDF อย่างไรก็ตาม ในบทนี้เราจะมุ่งเน้นไปที่กรณีการใช้งานหลัก ซึ่งก็คือการแสดง HTML และรูปภาพที่จัดรูปแบบโดยใช้ CSS
เบราว์เซอร์ต่างๆ ใช้เครื่องมือแสดงผลที่แตกต่างกัน เช่น Internet Explorer ใช้ Trident, Firefox ใช้ Gecko, Safari ใช้ WebKit Chrome และ Opera (ตั้งแต่เวอร์ชัน 15) ใช้ Blink ซึ่งเป็น Fork ของ WebKit
WebKit เป็นเครื่องมือแสดงผลแบบโอเพนซอร์สซึ่งเริ่มต้นจากการเป็นเครื่องมือสำหรับแพลตฟอร์ม Linux และได้รับการแก้ไขโดย Apple เพื่อรองรับ Mac และ Windows
ขั้นตอนหลัก
เครื่องมือแสดงผลจะเริ่มรับเนื้อหาของเอกสารที่ขอจากเลเยอร์เครือข่าย โดยปกติแล้วจะดำเนินการในกลุ่มขนาด 8kB
หลังจากนั้น ขั้นตอนพื้นฐานของเครื่องมือแสดงผลจะเป็นดังนี้
เครื่องมือแสดงผลจะเริ่มแยกวิเคราะห์เอกสาร HTML และแปลงองค์ประกอบเป็นโหนด DOM ในโครงสร้างที่เรียกว่า "โครงสร้างเนื้อหา" เครื่องมือจะแยกวิเคราะห์ข้อมูลสไตล์ทั้งในไฟล์ CSS ภายนอกและในองค์ประกอบสไตล์ ระบบจะใช้ข้อมูลการจัดรูปแบบร่วมกับคำสั่งภาพใน HTML เพื่อสร้างโครงสร้างอีกแบบหนึ่ง นั่นคือโครงสร้างการแสดงผล
Render Tree ประกอบด้วยสี่เหลี่ยมผืนผ้าที่มีแอตทริบิวต์ภาพ เช่น สีและขนาด สี่เหลี่ยมผืนผ้าอยู่ในลำดับที่ถูกต้องเพื่อแสดงบนหน้าจอ
หลังจากสร้างแผนผังการแสดงผลแล้ว แผนผังจะผ่านกระบวนการ "เลย์เอาต์" ซึ่งหมายความว่าคุณต้องกำหนดพิกัดที่แน่นอนให้กับแต่ละโหนดว่าควรปรากฏที่ใดบนหน้าจอ ขั้นตอนถัดไปคือการวาด โดยระบบจะข้ามผ่านแผนผังการแสดงผลและวาดแต่ละโหนดโดยใช้เลเยอร์แบ็กเอนด์ของ UI
คุณควรเข้าใจว่ากระบวนการนี้จะค่อยเป็นค่อยไป เครื่องมือแสดงผลจะพยายามแสดงเนื้อหาบนหน้าจอโดยเร็วที่สุดเพื่อประสบการณ์ของผู้ใช้ที่ดีขึ้น โดยจะไม่รอจนกว่าจะแยกวิเคราะห์ HTML ทั้งหมดก่อนที่จะเริ่มสร้างและจัดเลย์เอาต์แผนผังการแสดงผล ระบบจะแยกวิเคราะห์และแสดงเนื้อหาบางส่วน ขณะที่กระบวนการจะดำเนินต่อไปกับเนื้อหาที่เหลือซึ่งมาจากเครือข่ายอย่างต่อเนื่อง
ตัวอย่างขั้นตอนหลัก
จากรูปที่ 3 และ 4 คุณจะเห็นว่าแม้ WebKit และ Gecko จะใช้คำศัพท์ที่แตกต่างกันเล็กน้อย แต่ขั้นตอนก็เหมือนกัน
Gecko เรียกแผนผังขององค์ประกอบที่จัดรูปแบบเป็นภาพว่า "แผนผังเฟรม" แต่ละองค์ประกอบคือเฟรม WebKit ใช้คำว่า "Render Tree" และประกอบด้วย "Render Objects" WebKit ใช้คำว่า "เลย์เอาต์" สำหรับการวางองค์ประกอบ ส่วน Gecko เรียกว่า "การจัดเรียงใหม่" "ไฟล์แนบ" เป็นคำศัพท์ของ WebKit สำหรับการเชื่อมต่อโหนด DOM และข้อมูลภาพเพื่อสร้างแผนผังการแสดงผล ความแตกต่างเล็กน้อยที่ไม่ใช่เชิงความหมายคือ Gecko มีเลเยอร์เพิ่มเติมระหว่าง HTML และแผนผัง DOM ซึ่งเรียกว่า "Content Sink" และเป็นโรงงานสำหรับสร้างองค์ประกอบ DOM เราจะพูดถึงแต่ละส่วนของขั้นตอนต่อไปนี้
การแยกวิเคราะห์ - ทั่วไป
เนื่องจากการแยกวิเคราะห์เป็นกระบวนการที่สำคัญมากภายในเครื่องมือแสดงผล เราจะเจาะลึกเรื่องนี้กันอีกสักหน่อย มาเริ่มด้วยการแนะนำสั้นๆ เกี่ยวกับการแยกวิเคราะห์กันก่อน
การแยกวิเคราะห์เอกสารหมายถึงการแปลเอกสารเป็นโครงสร้างที่โค้ดใช้ได้ ผลลัพธ์ของการแยกวิเคราะห์มักจะเป็นแผนผังของโหนดที่แสดงโครงสร้างของเอกสาร ซึ่งเรียกว่าโครงสร้างการแยกวิเคราะห์หรือโครงสร้างไวยากรณ์
เช่น การแยกวิเคราะห์นิพจน์ 2 + 3 - 1 อาจแสดงผลเป็นแผนภูมิต่อไปนี้
ไวยากรณ์
การแยกวิเคราะห์จะอิงตามกฎไวยากรณ์ที่เอกสารปฏิบัติตาม ซึ่งก็คือภาษาหรือรูปแบบที่เขียน ทุกรูปแบบที่คุณแยกวิเคราะห์ได้ต้องมีไวยากรณ์เชิงกำหนดซึ่งประกอบด้วยคำศัพท์และกฎไวยากรณ์ ซึ่งเรียกว่าไวยากรณ์แบบไม่พึ่งบริบท ภาษาของมนุษย์ไม่ใช่ภาษาประเภทนี้ จึงไม่สามารถแยกวิเคราะห์ด้วยเทคนิคการแยกวิเคราะห์แบบเดิมได้
Parser - Lexer combination
การแยกวิเคราะห์แบ่งออกเป็น 2 กระบวนการย่อย ได้แก่ การวิเคราะห์เชิงคำศัพท์และการวิเคราะห์ไวยากรณ์
การวิเคราะห์เชิงคำศัพท์คือกระบวนการแยกอินพุตออกเป็นโทเค็น โทเค็นคือคำศัพท์ของภาษา ซึ่งก็คือคอลเล็กชันขององค์ประกอบที่ถูกต้อง ในภาษามนุษย์ คำศัพท์จะประกอบด้วยคำทั้งหมดที่ปรากฏในพจนานุกรมของภาษานั้นๆ
การวิเคราะห์ไวยากรณ์คือการใช้กฎไวยากรณ์ของภาษา
โดยปกติแล้วโปรแกรมแยกวิเคราะห์จะแบ่งงานออกเป็น 2 ส่วน ได้แก่ Lexer (บางครั้งเรียกว่า Tokenizer) ซึ่งมีหน้าที่แบ่งอินพุตออกเป็นโทเค็นที่ถูกต้อง และโปรแกรมแยกวิเคราะห์ซึ่งมีหน้าที่สร้างโครงสร้างการแยกวิเคราะห์โดยการวิเคราะห์โครงสร้างเอกสารตามกฎไวยากรณ์ของภาษา
Lexer จะรู้วิธีตัดอักขระที่ไม่เกี่ยวข้อง เช่น ช่องว่างและการขึ้นบรรทัดใหม่
กระบวนการแยกวิเคราะห์เป็นกระบวนการที่ต้องทำซ้ำ โดยปกติแล้วโปรแกรมแยกวิเคราะห์จะขอโทเค็นใหม่จาก Lexer และพยายามจับคู่โทเค็นกับกฎไวยากรณ์ข้อใดข้อหนึ่ง หากตรงกับกฎ ระบบจะเพิ่มโหนดที่สอดคล้องกับโทเค็นลงในโครงสร้างการแยกวิเคราะห์ และโปรแกรมแยกวิเคราะห์จะขอโทเค็นอื่น
หากไม่มีกฎที่ตรงกัน โปรแกรมแยกวิเคราะห์จะจัดเก็บโทเค็นไว้ภายใน และขอโทเค็นต่อไปจนกว่าจะพบกฎที่ตรงกับโทเค็นทั้งหมดที่จัดเก็บไว้ภายใน หากไม่พบกฎ พาร์เซอร์จะแสดงข้อยกเว้น ซึ่งหมายความว่าเอกสารไม่ถูกต้องและมีข้อผิดพลาดทางไวยากรณ์
การแปล
ในหลายกรณี แผนผังการแยกวิเคราะห์ไม่ใช่ผลลัพธ์สุดท้าย การแยกวิเคราะห์มักใช้ในการแปล ซึ่งก็คือการแปลงเอกสารอินพุตเป็นรูปแบบอื่น ตัวอย่างหนึ่งคือการรวบรวมข้อมูล คอมไพเลอร์ที่คอมไพล์ซอร์สโค้ดเป็นรหัสเครื่องจะแยกวิเคราะห์ซอร์สโค้ดเป็นแผนผังการแยกวิเคราะห์ก่อน แล้วจึงแปลแผนผังเป็นเอกสารรหัสเครื่อง
ตัวอย่างการแยกวิเคราะห์
ในรูปที่ 5 เราได้สร้างต้นไม้แยกวิเคราะห์จากนิพจน์ทางคณิตศาสตร์ ลองกำหนดภาษาทางคณิตศาสตร์อย่างง่ายและดูขั้นตอนการแยกวิเคราะห์กัน
ไวยากรณ์:
- องค์ประกอบของไวยากรณ์ภาษาคือ นิพจน์ คำ และการดำเนินการ
- ภาษาของเราสามารถรวมนิพจน์ได้หลายรายการ
- นิพจน์จะกำหนดเป็น "เทอม" ตามด้วย "การดำเนินการ" ตามด้วยเทอมอื่น
- การดำเนินการคือโทเค็นบวกหรือโทเค็นลบ
- คำคือโทเค็นจำนวนเต็มหรือนิพจน์
มาวิเคราะห์ข้อมูลที่ป้อน 2 + 3 - 1 กัน
สตริงย่อยแรกที่ตรงกับกฎคือ 2: ตามกฎข้อที่ 5 สตริงย่อยนี้เป็นคำ
รายการที่ตรงกันรายการที่ 2 คือ 2 + 3 ซึ่งตรงกับกฎข้อที่ 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 ได้ทั้งหมด ดูคำจำกัดความอย่างเป็นทางการได้ที่บทความของ Wikipedia เกี่ยวกับไวยากรณ์ที่ปราศจากบริบท
ประเภทของโปรแกรมแยกวิเคราะห์
โปรแกรมแยกวิเคราะห์มี 2 ประเภท ได้แก่ โปรแกรมแยกวิเคราะห์จากบนลงล่างและโปรแกรมแยกวิเคราะห์จากล่างขึ้นบน คำอธิบายที่เข้าใจง่ายคือโปรแกรมแยกวิเคราะห์จากบนลงล่างจะตรวจสอบโครงสร้างระดับสูงของไวยากรณ์และพยายามค้นหากฎที่ตรงกัน ตัวแยกวิเคราะห์แบบล่างขึ้นบนจะเริ่มต้นด้วยอินพุตและค่อยๆ แปลงเป็นกฎไวยากรณ์ โดยเริ่มจากกฎระดับต่ำจนกว่าจะเป็นไปตามกฎระดับสูง
มาดูกันว่าโปรแกรมแยกวิเคราะห์ทั้ง 2 ประเภทจะแยกวิเคราะห์ตัวอย่างของเราอย่างไร
โปรแกรมแยกวิเคราะห์จากบนลงล่างจะเริ่มจากกฎระดับที่สูงกว่า โดยจะระบุ 2 + 3 เป็นนิพจน์ จากนั้นจะระบุ 2 + 3 - 1 เป็นนิพจน์ (กระบวนการระบุนิพจน์จะพัฒนาขึ้น โดยจับคู่กับกฎอื่นๆ แต่จุดเริ่มต้นคือกฎระดับสูงสุด)
โปรแกรมแยกวิเคราะห์แบบล่างขึ้นบนจะสแกนอินพุตจนกว่าจะตรงกับกฎ จากนั้นระบบจะแทนที่อินพุตที่ตรงกันด้วยกฎ ซึ่งจะดำเนินการต่อไปจนกว่าจะสิ้นสุดอินพุต ระบบจะวางนิพจน์ที่ตรงกันบางส่วนไว้ในสแต็กของโปรแกรมแยกวิเคราะห์
โปรแกรมแยกวิเคราะห์แบบล่างขึ้นบนประเภทนี้เรียกว่าโปรแกรมแยกวิเคราะห์แบบ Shift-Reduce เนื่องจากอินพุตจะเลื่อนไปทางขวา (ลองนึกภาพเคอร์เซอร์ที่ชี้ไปที่จุดเริ่มต้นของอินพุตก่อนแล้วเลื่อนไปทางขวา) และค่อยๆ ลดลงเป็นกฎไวยากรณ์
การสร้างโปรแกรมแยกวิเคราะห์โดยอัตโนมัติ
มีเครื่องมือที่สร้างโปรแกรมแยกวิเคราะห์ได้ คุณป้อนไวยากรณ์ของภาษาของคุณ ซึ่งก็คือคำศัพท์และกฎไวยากรณ์ แล้วเครื่องมือจะสร้างโปรแกรมแยกวิเคราะห์ที่ใช้งานได้ การสร้างโปรแกรมแยกวิเคราะห์ต้องอาศัยความเข้าใจอย่างลึกซึ้งเกี่ยวกับการแยกวิเคราะห์ และการสร้างโปรแกรมแยกวิเคราะห์ที่เพิ่มประสิทธิภาพด้วยตนเองไม่ใช่เรื่องง่าย ดังนั้นเครื่องมือสร้างโปรแกรมแยกวิเคราะห์จึงมีประโยชน์มาก
WebKit ใช้เครื่องมือสร้างโปรแกรมแยกวิเคราะห์ที่รู้จักกันดี 2 รายการ ได้แก่ Flex สำหรับสร้าง Lexer และ Bison สำหรับสร้างโปรแกรมแยกวิเคราะห์ (คุณอาจพบเครื่องมือเหล่านี้ในชื่อ Lex และ Yacc) อินพุต Flex คือไฟล์ที่มีคำจำกัดความของโทเค็นในรูปแบบนิพจน์ทั่วไป อินพุตของ Bison คือกฎไวยากรณ์ภาษาในรูปแบบ BNF
โปรแกรมแยกวิเคราะห์ HTML
หน้าที่ของโปรแกรมแยกวิเคราะห์ HTML คือการแยกวิเคราะห์มาร์กอัป HTML เป็นโครงสร้างการแยกวิเคราะห์
ไวยากรณ์ HTML
คำศัพท์และไวยากรณ์ของ HTML จะกำหนดไว้ในข้อกำหนดที่สร้างโดยองค์กร W3C
ดังที่เราได้เห็นในบทนำเกี่ยวกับการแยกวิเคราะห์ ไวยากรณ์ของไวยากรณ์สามารถกำหนดอย่างเป็นทางการได้โดยใช้รูปแบบต่างๆ เช่น BNF
น่าเสียดายที่หัวข้อเกี่ยวกับโปรแกรมแยกวิเคราะห์แบบดั้งเดิมทั้งหมดใช้กับ HTML ไม่ได้ (เราไม่ได้พูดถึงหัวข้อเหล่านี้เพื่อความสนุก แต่จะใช้ในการแยกวิเคราะห์ CSS และ JavaScript) HTML ไม่สามารถกำหนดได้ง่ายๆ ด้วยไวยากรณ์ที่ไม่มีบริบทซึ่งโปรแกรมแยกวิเคราะห์ต้องการ
มีรูปแบบที่เป็นทางการสำหรับการกำหนด HTML - DTD (Document Type Definition) - แต่ไม่ใช่ไวยากรณ์ที่ไม่มีบริบท
ซึ่งอาจดูแปลกในตอนแรก เนื่องจาก HTML ค่อนข้างใกล้เคียงกับ XML มีโปรแกรมแยกวิเคราะห์ XML ให้เลือกใช้มากมาย มีรูปแบบ XML ของ HTML - XHTML - แล้วความแตกต่างที่สำคัญคืออะไร
ความแตกต่างคือแนวทาง HTML จะ "ยืดหยุ่น" กว่า โดยจะช่วยให้คุณละเว้นแท็กบางแท็ก (ซึ่งจะเพิ่มโดยนัยในภายหลัง) หรือบางครั้งก็ละเว้นแท็กเริ่มต้นหรือแท็กสิ้นสุด และอื่นๆ ได้ โดยรวมแล้ว ไวยากรณ์ของ JSON จะเป็นไวยากรณ์ที่ "ยืดหยุ่น" ตรงข้ามกับไวยากรณ์ที่เข้มงวดและซับซ้อนของ XML
รายละเอียดที่ดูเหมือนเล็กน้อยนี้สร้างความแตกต่างได้อย่างมาก ในแง่หนึ่ง นี่คือเหตุผลหลักที่ทำให้ HTML ได้รับความนิยมอย่างมาก เนื่องจาก HTML จะไม่สนใจข้อผิดพลาดของคุณและทำให้ชีวิตของผู้เขียนเว็บง่ายขึ้น ในทางกลับกัน การเขียนไวยากรณ์ที่เป็นทางการก็ทำได้ยาก ดังนั้นโดยสรุปแล้ว พาร์เซอร์ทั่วไปจึงแยกวิเคราะห์ HTML ได้ไม่ง่ายนัก เนื่องจากไวยากรณ์ของ HTML ไม่ได้เป็นแบบไม่ขึ้นกับบริบท โปรแกรมแยกวิเคราะห์ XML ไม่สามารถแยกวิเคราะห์ HTML ได้
HTML DTD
คำจำกัดความ HTML อยู่ในรูปแบบ DTD รูปแบบนี้ใช้เพื่อกำหนดภาษาของตระกูล SGML รูปแบบนี้ประกอบด้วยคำจำกัดความขององค์ประกอบที่อนุญาตทั้งหมด แอตทริบิวต์ และลำดับชั้นขององค์ประกอบเหล่านั้น ดังที่เราได้เห็นก่อนหน้านี้ HTML DTD ไม่ได้สร้างไวยากรณ์ที่ไม่มีบริบท
DTD มีหลายรูปแบบ โหมดเข้มงวดจะเป็นไปตามข้อกำหนดเท่านั้น แต่โหมดอื่นๆ จะรองรับมาร์กอัปที่เบราว์เซอร์ใช้ในอดีต โดยมีวัตถุประสงค์เพื่อความเข้ากันได้แบบย้อนหลังกับเนื้อหาเก่า ดู DTD แบบเข้มงวดปัจจุบันได้ที่ www.w3.org/TR/html4/strict.dtd
DOM
แผนผังเอาต์พุต ("แผนผังการแยกวิเคราะห์") คือแผนผังขององค์ประกอบ DOM และโหนดแอตทริบิวต์ DOM ย่อมาจาก Document Object Model (แบบจำลองออบเจ็กต์เอกสาร) ซึ่งเป็นการนำเสนอออบเจ็กต์ของเอกสาร HTML และอินเทอร์เฟซขององค์ประกอบ HTML ต่อโลกภายนอก เช่น JavaScript
รากของโครงสร้างคือออบเจ็กต์ "Document"
DOM มีความสัมพันธ์แบบหนึ่งต่อหนึ่งกับมาร์กอัปเกือบทั้งหมด เช่น
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
มาร์กอัปนี้จะแปลเป็นแผนผัง DOM ต่อไปนี้
DOM ได้รับการระบุโดยองค์กร W3C เช่นเดียวกับ HTML ดู www.w3.org/DOM/DOMTR ซึ่งเป็นข้อกำหนดทั่วไปสำหรับการจัดการเอกสาร โมดูลที่เฉพาะเจาะจงจะอธิบายองค์ประกอบที่เฉพาะเจาะจงของ HTML ดูคำจำกัดความของ HTML ได้ที่ www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html
เมื่อเราบอกว่าแผนผังมีโหนด DOM หมายความว่าแผนผังสร้างขึ้นจากองค์ประกอบที่ใช้อินเทอร์เฟซ DOM อย่างใดอย่างหนึ่ง เบราว์เซอร์ใช้การติดตั้งใช้งานที่เป็นรูปธรรมซึ่งมีแอตทริบิวต์อื่นๆ ที่เบราว์เซอร์ใช้ภายใน
อัลกอริทึมการแยกวิเคราะห์
ดังที่เราได้เห็นในส่วนก่อนหน้านี้ HTML ไม่สามารถแยกวิเคราะห์โดยใช้โปรแกรมแยกวิเคราะห์แบบบนลงล่างหรือล่างขึ้นบนตามปกติได้
ซึ่งมีสาเหตุดังนี้
- ลักษณะที่ยืดหยุ่นของภาษา
- ความจริงที่ว่าเบราว์เซอร์มีความสามารถในการยอมรับข้อผิดพลาดแบบดั้งเดิมเพื่อรองรับกรณีที่ทราบกันดีเกี่ยวกับ HTML ที่ไม่ถูกต้อง
- กระบวนการแยกวิเคราะห์เป็นแบบ Reentrant สำหรับภาษาอื่นๆ แหล่งที่มาจะไม่เปลี่ยนแปลงระหว่างการแยกวิเคราะห์ แต่ใน HTML โค้ดแบบไดนามิก (เช่น องค์ประกอบสคริปต์ที่มีการเรียก
document.write()) สามารถเพิ่มโทเค็นเพิ่มเติมได้ ดังนั้นกระบวนการแยกวิเคราะห์จึงแก้ไขอินพุตจริง
เบราว์เซอร์จะสร้างโปรแกรมแยกวิเคราะห์ที่กำหนดเองเพื่อแยกวิเคราะห์ HTML เนื่องจากไม่สามารถใช้เทคนิคการแยกวิเคราะห์ปกติได้
อัลกอริทึมการแยกวิเคราะห์มีอธิบายไว้อย่างละเอียดในข้อกำหนดของ HTML5 อัลกอริทึมประกอบด้วย 2 ขั้นตอน ได้แก่ การแยกโทเค็นและการสร้างโครงสร้างต้นไม้
การแยกโทเค็นคือการวิเคราะห์คำศัพท์ โดยแยกวิเคราะห์อินพุตเป็นโทเค็น โทเค็น HTML ได้แก่ แท็กเริ่มต้น แท็กสิ้นสุด ชื่อแอตทริบิวต์ และค่าแอตทริบิวต์
เครื่องมือสร้างโทเค็นจะจดจำโทเค็น ส่งโทเค็นไปยังเครื่องมือสร้างโครงสร้าง และใช้อักขระถัดไปเพื่อจดจำโทเค็นถัดไป และทำเช่นนี้ไปเรื่อยๆ จนกว่าจะสิ้นสุดอินพุต
อัลกอริทึมการแปลงข้อมูลเป็นโทเค็น
เอาต์พุตของอัลกอริทึมคือโทเค็น HTML อัลกอริทึมจะแสดงเป็นเครื่องสถานะ แต่ละสถานะจะใช้อักขระอย่างน้อย 1 ตัวของสตรีมอินพุตและอัปเดตสถานะถัดไปตามอักขระเหล่านั้น การตัดสินใจจะขึ้นอยู่กับสถานะการแยกโทเค็นปัจจุบันและสถานะการสร้างโครงสร้างต้นไม้ ซึ่งหมายความว่าอักขระที่ใช้ตัวเดียวกันจะให้ผลลัพธ์ที่แตกต่างกันสำหรับสถานะถัดไปที่ถูกต้อง ทั้งนี้ขึ้นอยู่กับสถานะปัจจุบัน อัลกอริทึมนี้ซับซ้อนเกินกว่าจะอธิบายได้อย่างครบถ้วน ดังนั้นเรามาดูตัวอย่างง่ายๆ ที่จะช่วยให้เข้าใจหลักการกัน
ตัวอย่างพื้นฐาน - การแยกโทเค็น HTML ต่อไปนี้
<html>
<body>
Hello world
</body>
</html>
สถานะเริ่มต้นคือ "สถานะข้อมูล"
เมื่อพบอักขระ < สถานะจะเปลี่ยนเป็น "สถานะเปิดแท็ก"
การใช้อักขระ a-z จะทำให้เกิดการสร้าง "โทเค็นแท็กเริ่มต้น" สถานะจะเปลี่ยนเป็น "สถานะชื่อแท็ก"
เราจะอยู่ในสถานะนี้จนกว่าจะใช้อักขระ > ระบบจะเพิ่มอักขระแต่ละตัวต่อท้ายชื่อโทเค็นใหม่ ในกรณีของเรา โทเค็นที่สร้างขึ้นคือโทเค็น html
เมื่อถึงแท็ก > ระบบจะส่งโทเค็นปัจจุบันและสถานะจะเปลี่ยนกลับเป็น "สถานะข้อมูล"
ระบบจะจัดการแท็ก <body> ด้วยขั้นตอนเดียวกัน
จนถึงตอนนี้ ระบบได้ส่งแท็ก html และ body แล้ว ตอนนี้เรากลับมาที่ "สถานะข้อมูล" แล้ว
การใช้ H อักขระของ Hello world จะทำให้เกิดการสร้างและปล่อยโทเค็นอักขระ ซึ่งจะดำเนินต่อไปจนกว่าจะถึง < ของ </body> เราจะปล่อยโทเค็นอักขระสำหรับอักขระแต่ละตัวของ Hello world
ตอนนี้เรากลับมาที่ "สถานะเปิดแท็ก" แล้ว
การใช้อินพุตถัดไป / จะทำให้เกิดการสร้าง end tag token และย้ายไปยัง "สถานะชื่อแท็ก" เราจะอยู่ในสถานะนี้ต่อไปจนกว่าจะถึง > จากนั้นระบบจะส่งโทเค็นแท็กใหม่และเราจะกลับไปที่ "สถานะข้อมูล"
ระบบจะถือว่าอินพุต </html> เป็นเหมือนกรณีที่แล้ว
อัลกอริทึมการสร้างแผนผัง
เมื่อสร้างโปรแกรมแยกวิเคราะห์ ระบบจะสร้างออบเจ็กต์เอกสาร ในระหว่างขั้นตอนการสร้างแผนผัง แผนผัง DOM ที่มีเอกสารอยู่ในรูทจะได้รับการแก้ไขและจะมีการเพิ่มองค์ประกอบลงในแผนผัง ตัวสร้างต้นไม้จะประมวลผลแต่ละโหนดที่สร้างขึ้นโดย Tokenizer สำหรับโทเค็นแต่ละรายการ ข้อกำหนดจะระบุว่าองค์ประกอบ DOM ใดเกี่ยวข้องกับโทเค็นนั้นและจะสร้างขึ้นสำหรับโทเค็นนี้ ระบบจะเพิ่มองค์ประกอบลงในแผนผัง DOM และสแต็กขององค์ประกอบที่เปิดอยู่ สแต็กนี้ใช้เพื่อแก้ไขแท็กที่ซ้อนกันไม่ตรงกันและแท็กที่ไม่ได้ปิด นอกจากนี้ อัลกอริทึมยังอธิบายเป็นเครื่องสถานะด้วย สถานะเหล่านี้เรียกว่า "โหมดการแทรก"
มาดูขั้นตอนการสร้างต้นไม้สำหรับอินพุตตัวอย่างกัน
<html>
<body>
Hello world
</body>
</html>
อินพุตสำหรับขั้นตอนการสร้างโครงสร้างต้นไม้คือลำดับของโทเค็นจากขั้นตอนการแยกโทเค็น โหมดแรกคือ "โหมดเริ่มต้น" การได้รับโทเค็น "html" จะทำให้เปลี่ยนไปใช้โหมด "ก่อน html" และประมวลผลโทเค็นอีกครั้งในโหมดนั้น ซึ่งจะทำให้เกิดการสร้างองค์ประกอบ HTMLHtmlElement ซึ่งจะต่อท้ายออบเจ็กต์ Document ระดับรูท
สถานะจะเปลี่ยนเป็น "ก่อนส่วนหัว" จากนั้นระบบจะได้รับโทเค็น "body" ระบบจะสร้าง HTMLHeadElement โดยนัยแม้ว่าเราจะไม่มีโทเค็น "head" และจะเพิ่มลงในโครงสร้าง
ตอนนี้เราจะย้ายไปที่โหมด "ในส่วน head" แล้วไปที่ "หลังส่วน head" ระบบจะประมวลผลโทเค็น body อีกครั้ง มีการสร้างและแทรก HTMLBodyElement และโหมดจะเปลี่ยนเป็น "in body"
ตอนนี้ระบบได้รับโทเค็นอักขระของสตริง "Hello world" แล้ว อักขระแรกจะทำให้เกิดการสร้างและการแทรกโหนด "ข้อความ" และอักขระอื่นๆ จะต่อท้ายโหนดนั้น
การรับโทเค็นสิ้นสุดเนื้อหาจะทำให้เกิดการโอนไปยังโหมด "หลังเนื้อหา" ตอนนี้เราจะได้รับแท็กปิด html ซึ่งจะย้ายเราไปยังโหมด "after after body" การรับโทเค็นจุดสิ้นสุดของไฟล์จะสิ้นสุดการแยกวิเคราะห์
การดำเนินการเมื่อการแยกวิเคราะห์เสร็จสิ้น
ในขั้นตอนนี้ เบราว์เซอร์จะทำเครื่องหมายเอกสารเป็นแบบอินเทอร์แอกทีฟและเริ่มแยกวิเคราะห์สคริปต์ที่อยู่ในโหมด "เลื่อนเวลา" ซึ่งเป็นสคริปต์ที่ควรเรียกใช้หลังจากแยกวิเคราะห์เอกสารแล้ว จากนั้นระบบจะตั้งค่าสถานะเอกสารเป็น "เสร็จสมบูรณ์" และเริ่มเหตุการณ์ "โหลด"
คุณสามารถดูอัลกอริทึมทั้งหมดสำหรับการแยกโทเค็นและการสร้างโครงสร้างแบบต้นไม้ได้ในข้อกำหนด HTML5
ความคลาดเคลื่อนที่ยอมรับได้ของข้อผิดพลาดในเบราว์เซอร์
คุณจะไม่ได้รับข้อผิดพลาด "ไวยากรณ์ไม่ถูกต้อง" ในหน้า HTML เบราว์เซอร์จะแก้ไขเนื้อหาที่ไม่ถูกต้องและดำเนินการต่อ
ลองดูตัวอย่าง HTML ต่อไปนี้
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
ฉันต้องละเมิดกฎประมาณ 1 ล้านข้อ ("mytag" ไม่ใช่แท็กมาตรฐาน การฝังองค์ประกอบ "p" และ "div" ที่ไม่ถูกต้อง และอื่นๆ) แต่เบราว์เซอร์ยังคงแสดงผลอย่างถูกต้องและไม่แสดงข้อผิดพลาด ดังนั้นโค้ดโปรแกรมแยกวิเคราะห์จำนวนมากจึงเป็นการแก้ไขข้อผิดพลาดของผู้เขียน HTML
การจัดการข้อผิดพลาดค่อนข้างสอดคล้องกันในเบราว์เซอร์ต่างๆ แต่น่าแปลกใจที่ไม่ได้เป็นส่วนหนึ่งของข้อกำหนด HTML เช่นเดียวกับการบุ๊กมาร์กและปุ่มย้อนกลับ/ไปข้างหน้า ซึ่งเป็นสิ่งที่พัฒนาขึ้นในเบราว์เซอร์ในช่วงหลายปีที่ผ่านมา มีโครงสร้าง HTML ที่ไม่ถูกต้องซึ่งทราบกันดีว่าเกิดขึ้นซ้ำในหลายเว็บไซต์ และเบราว์เซอร์พยายามแก้ไขโครงสร้างดังกล่าวในลักษณะที่สอดคล้องกับเบราว์เซอร์อื่นๆ
ข้อกำหนด HTML5 กำหนดข้อกำหนดเหล่านี้บางส่วน (WebKit สรุปเรื่องนี้ไว้อย่างดีในความคิดเห็นที่จุดเริ่มต้นของคลาสโปรแกรมแยกวิเคราะห์ HTML)
โปรแกรมแยกวิเคราะห์จะแยกวิเคราะห์อินพุตที่แปลงเป็นโทเค็นลงในเอกสารเพื่อสร้างโครงสร้างเอกสาร หากเอกสารมีรูปแบบถูกต้อง การแยกวิเคราะห์จะทำได้ง่าย
แต่เราต้องจัดการกับเอกสาร HTML จำนวนมากที่ไม่ได้มีรูปแบบที่ถูกต้อง ดังนั้นโปรแกรมแยกวิเคราะห์จึงต้องยอมรับข้อผิดพลาดได้
เราต้องจัดการกับเงื่อนไขข้อผิดพลาดต่อไปนี้เป็นอย่างน้อย
- องค์ประกอบที่เพิ่มเข้ามาถูกห้ามอย่างชัดแจ้งภายในแท็กด้านนอกบางแท็ก ในกรณีนี้ เราควรปิดแท็กทั้งหมดจนถึงแท็กที่ห้ามองค์ประกอบ และเพิ่มองค์ประกอบหลังจากนั้น
- เราไม่ได้รับอนุญาตให้เพิ่มองค์ประกอบโดยตรง อาจเป็นไปได้ว่าผู้เขียนเอกสารลืมแท็กบางแท็กที่อยู่ระหว่างนั้น (หรือแท็กที่อยู่ระหว่างนั้นเป็นแท็กที่ไม่บังคับ) ซึ่งอาจเป็นกรณีที่เกิดขึ้นกับแท็กต่อไปนี้ HTML HEAD BODY TBODY TR TD LI (มีแท็กอื่นอีกไหม)
- เราต้องการเพิ่มองค์ประกอบบล็อกภายในองค์ประกอบแบบอินไลน์ ปิดองค์ประกอบแบบอินไลน์ทั้งหมดจนถึงองค์ประกอบแบบบล็อกที่สูงขึ้นถัดไป
- หากวิธีนี้ไม่ได้ผล ให้ปิดองค์ประกอบจนกว่าเราจะได้รับอนุญาตให้เพิ่มองค์ประกอบ หรือละเว้นแท็ก
มาดูตัวอย่างความคลาดเคลื่อนของข้อผิดพลาด WebKit กัน
</br> แทน <br>
บางเว็บไซต์ใช้ </br> แทน <br> เพื่อให้เข้ากันได้กับ IE และ Firefox ทาง WebKit จะถือว่าค่านี้เป็น <br>
รหัส:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
โปรดทราบว่าการจัดการข้อผิดพลาดเป็นแบบภายใน ซึ่งจะไม่แสดงต่อผู้ใช้
ตารางที่ไม่มีความเกี่ยวข้อง
ตารางที่หลุดออกมาคือตารางที่อยู่ภายในตารางอื่น แต่ไม่ได้อยู่ภายในเซลล์ของตาราง
เช่น
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit จะเปลี่ยนลำดับชั้นเป็นตารางพี่น้อง 2 ตาราง ดังนี้
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
รหัส:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit ใช้สแต็กสำหรับเนื้อหาองค์ประกอบปัจจุบัน โดยจะดึงตารางด้านในออกจากสแต็กตารางด้านนอก ตอนนี้ตารางจะอยู่ระดับเดียวกัน
องค์ประกอบแบบฟอร์มที่ซ้อนกัน
ในกรณีที่ผู้ใช้ใส่แบบฟอร์มไว้ในแบบฟอร์มอื่น ระบบจะไม่สนใจแบบฟอร์มที่ 2
รหัส:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
ลำดับชั้นของแท็กที่ลึกเกินไป
ซึ่งความคิดเห็นก็เป็นเครื่องพิสูจน์ได้เป็นอย่างดี
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for (HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) { }
return i != cMaxRedundantTagDepth;
}
แท็กปิด html หรือ body อยู่ผิดที่
ขอย้ำอีกครั้งว่าความคิดเห็นนี้เป็นเครื่องพิสูจน์ได้เป็นอย่างดี
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
ดังนั้นผู้เขียนเว็บโปรดระวัง เว้นแต่คุณต้องการปรากฏเป็นตัวอย่างในข้อมูลโค้ดความทนทานต่อข้อผิดพลาดของ WebKit ให้เขียน HTML ที่มีรูปแบบถูกต้อง
การแยกวิเคราะห์ CSS
ยังจำแนวคิดการแยกวิเคราะห์ในบทนำได้ไหม CSS ไม่เหมือนกับ HTML ตรงที่เป็นไวยากรณ์ที่ไม่มีบริบทและสามารถแยกวิเคราะห์ได้โดยใช้โปรแกรมแยกวิเคราะห์ประเภทต่างๆ ที่อธิบายไว้ในบทนำ อันที่จริงแล้ว ข้อกำหนด CSS จะกำหนดไวยากรณ์คำศัพท์และไวยากรณ์ของ CSS
มาดูตัวอย่างกัน
ไวยากรณ์คำศัพท์ (คำศัพท์) กำหนดโดยนิพจน์ทั่วไปสำหรับโทเค็นแต่ละรายการ ดังนี้
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
"ident" ย่อมาจากตัวระบุ เช่น ชื่อคลาส "name" คือรหัสองค์ประกอบ (ซึ่งอ้างอิงโดย "#")
ไวยากรณ์ของไวยากรณ์อธิบายไว้ใน BNF
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
คำอธิบาย:
ชุดกฎมีโครงสร้างดังนี้
div.error, a.error {
color:red;
font-weight:bold;
}
div.error และ a.error เป็นตัวเลือก ส่วนที่อยู่ภายในวงเล็บปีกกาประกอบด้วยกฎที่ใช้โดยชุดกฎนี้
โครงสร้างนี้กำหนดไว้อย่างเป็นทางการในคำจำกัดความต่อไปนี้
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
ซึ่งหมายความว่าชุดกฎคือตัวเลือก หรืออาจเป็นตัวเลือกหลายรายการที่คั่นด้วยคอมมาและเว้นวรรค (S หมายถึงเว้นวรรค) ชุดกฎประกอบด้วยวงเล็บปีกกาและภายในวงเล็บปีกกาจะมีการประกาศหรืออาจมีการประกาศหลายรายการที่คั่นด้วยเครื่องหมายเซมิโคลอน "declaration" และ "selector" จะกำหนดไว้ในคำจำกัดความ BNF ต่อไปนี้
โปรแกรมแยกวิเคราะห์ WebKit CSS
WebKit ใช้เครื่องมือสร้างโปรแกรมแยกวิเคราะห์ Flex และ Bison เพื่อสร้างโปรแกรมแยกวิเคราะห์โดยอัตโนมัติจากไฟล์ไวยากรณ์ CSS ดังที่คุณทราบจากข้อมูลเบื้องต้นเกี่ยวกับโปรแกรมแยกวิเคราะห์ Bison จะสร้างโปรแกรมแยกวิเคราะห์แบบ Shift-Reduce จากล่างขึ้นบน Firefox ใช้โปรแกรมแยกวิเคราะห์แบบบนลงล่างที่เขียนด้วยตนเอง ในทั้ง 2 กรณี ระบบจะแยกวิเคราะห์ไฟล์ CSS แต่ละไฟล์เป็นออบเจ็กต์ StyleSheet แต่ละออบเจ็กต์จะมีกฎ CSS ออบเจ็กต์กฎ CSS ประกอบด้วยออบเจ็กต์ตัวเลือกและออบเจ็กต์การประกาศ รวมถึงออบเจ็กต์อื่นๆ ที่สอดคล้องกับไวยากรณ์ CSS
ลำดับการประมวลผลสำหรับสคริปต์และสไตล์ชีต
สคริปต์
โมเดลของเว็บเป็นแบบซิงโครนัส ผู้เขียนคาดหวังว่าสคริปต์จะได้รับการแยกวิเคราะห์และดำเนินการทันทีเมื่อโปรแกรมแยกวิเคราะห์ไปถึงแท็ก <script>
การแยกวิเคราะห์เอกสารจะหยุดชั่วคราวจนกว่าสคริปต์จะทำงาน
หากสคริปต์เป็นแบบภายนอก คุณต้องดึงข้อมูลทรัพยากรจากเครือข่ายก่อน ซึ่งจะดำเนินการแบบซิงโครนัสด้วย และการแยกวิเคราะห์จะหยุดลงจนกว่าจะดึงข้อมูลทรัพยากรมาได้
ซึ่งเป็นรูปแบบที่ใช้กันมาหลายปีและยังระบุไว้ในข้อกำหนด HTML4 และ 5 ด้วย
ผู้เขียนสามารถเพิ่มแอตทริบิวต์ "defer" ลงในสคริปต์ได้ ซึ่งในกรณีนี้จะไม่หยุดการแยกวิเคราะห์เอกสารและจะทำงานหลังจากแยกวิเคราะห์เอกสารแล้ว HTML5 เพิ่มตัวเลือกในการทำเครื่องหมายสคริปต์เป็นแบบอะซิงโครนัส เพื่อให้แยกวิเคราะห์และดำเนินการโดยชุดข้อความอื่นได้
การแยกวิเคราะห์แบบคาดเดา
ทั้ง WebKit และ Firefox ต่างก็ทำการเพิ่มประสิทธิภาพนี้ ขณะเรียกใช้สคริปต์ เทรดอื่นจะแยกวิเคราะห์ส่วนที่เหลือของเอกสารและค้นหาว่าต้องโหลดทรัพยากรอื่นๆ ใดจากเครือข่ายและโหลดทรัพยากรเหล่านั้น วิธีนี้จะช่วยให้โหลดทรัพยากรในการเชื่อมต่อแบบขนานได้ และความเร็วโดยรวมจะดีขึ้น หมายเหตุ: Parser เชิงคาดเดาจะแยกวิเคราะห์เฉพาะการอ้างอิงไปยังทรัพยากรภายนอก เช่น สคริปต์ภายนอก สไตล์ชีต และรูปภาพ โดยจะไม่แก้ไขแผนผัง DOM ซึ่งจะปล่อยให้เป็นหน้าที่ของ Parser หลัก
สไตล์ชีต
ในทางกลับกัน สไตล์ชีตมีโมเดลที่แตกต่างกัน ในทางแนวคิด ดูเหมือนว่าเนื่องจากสไตล์ชีตไม่ได้เปลี่ยนแผนผัง DOM จึงไม่มีเหตุผลที่จะต้องรอสไตล์ชีตและหยุดการแยกวิเคราะห์เอกสาร อย่างไรก็ตาม ปัญหาหนึ่งที่อาจเกิดขึ้นคือสคริปต์ขอข้อมูลสไตล์ในระหว่างขั้นตอนการแยกวิเคราะห์เอกสาร หากยังไม่ได้โหลดและแยกวิเคราะห์สไตล์ สคริปต์จะได้รับคำตอบที่ไม่ถูกต้อง และเห็นได้ชัดว่าปัญหานี้ทำให้เกิดปัญหามากมาย ซึ่งดูเหมือนจะเป็นกรณีที่เกิดขึ้นไม่บ่อยนัก แต่ก็พบได้ทั่วไป Firefox จะบล็อกสคริปต์ทั้งหมดเมื่อมีสไตล์ชีตที่ยังคงโหลดและแยกวิเคราะห์อยู่ WebKit จะบล็อกสคริปต์เฉพาะเมื่อสคริปต์พยายามเข้าถึงพร็อพเพอร์ตี้สไตล์บางอย่างที่อาจได้รับผลกระทบจากสไตล์ชีตที่ไม่ได้โหลด
การสร้างโครงสร้างการแสดงผล
ขณะที่กำลังสร้างแผนผัง DOM เบราว์เซอร์จะสร้างแผนผังอีกแผนผังหนึ่งขึ้นมา ซึ่งก็คือแผนผังการแสดงผล แผนผังนี้เป็นองค์ประกอบภาพตามลำดับที่จะแสดง ซึ่งเป็นการแสดงภาพของเอกสาร วัตถุประสงค์ของแผนผังนี้คือเพื่อให้สามารถแสดงเนื้อหาตามลำดับที่ถูกต้อง
Firefox เรียกองค์ประกอบในแผนผังการแสดงผลว่า "เฟรม" WebKit ใช้คำว่าโปรแกรมแสดงผลหรือออบเจ็กต์การแสดงผล
Renderer จะรู้วิธีจัดวางและแสดงผลตัวมันเองและองค์ประกอบย่อย
คลาส RenderObject ของ WebKit ซึ่งเป็นคลาสพื้นฐานของโปรแกรมแสดงผลมีคำจำกัดความต่อไปนี้
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
ตัวแสดงผลแต่ละตัวจะแสดงพื้นที่สี่เหลี่ยมผืนผ้าซึ่งมักจะสอดคล้องกับกล่อง CSS ของโหนด ตามที่อธิบายไว้ในข้อกำหนด CSS2 ซึ่งรวมถึงข้อมูลทางเรขาคณิต เช่น ความกว้าง ความสูง และตำแหน่ง
ประเภทกล่องจะได้รับผลกระทบจากค่า "display" ของแอตทริบิวต์สไตล์ที่เกี่ยวข้องกับโหนด (ดูส่วนการคำนวณสไตล์) โค้ด WebKit สำหรับการตัดสินใจว่าจะสร้างโปรแกรมแสดงผลประเภทใดสำหรับโหนด DOM ตามแอตทริบิวต์การแสดงผลมีดังนี้
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
นอกจากนี้ ระบบยังพิจารณาประเภทองค์ประกอบด้วย เช่น ตัวควบคุมแบบฟอร์มและตารางจะมีเฟรมพิเศษ
ใน WebKit หากองค์ประกอบต้องการสร้างโปรแกรมแสดงผลพิเศษ องค์ประกอบนั้นจะลบล้างเมธอด createRenderer()
ตัวแสดงผลจะชี้ไปยังออบเจ็กต์สไตล์ที่มีข้อมูลที่ไม่ใช่เรขาคณิต
ความสัมพันธ์ของแผนผังการแสดงผลกับแผนผัง DOM
ตัวแสดงผลจะสอดคล้องกับองค์ประกอบ DOM แต่ความสัมพันธ์ไม่ใช่แบบหนึ่งต่อหนึ่ง ระบบจะไม่แทรกองค์ประกอบ DOM ที่ไม่แสดงผลในต้นไม้การแสดงผล ตัวอย่างเช่น องค์ประกอบ "head" นอกจากนี้ องค์ประกอบที่มีการกำหนดค่าการแสดงผลเป็น "none" จะไม่ปรากฏในแผนผัง (ในขณะที่องค์ประกอบที่มีระดับการเข้าถึงเป็น "hidden" จะปรากฏในแผนผัง)
มีองค์ประกอบ DOM ที่สอดคล้องกับออบเจ็กต์ภาพหลายรายการ โดยปกติแล้ว องค์ประกอบเหล่านี้จะมีโครงสร้างที่ซับซ้อนซึ่งไม่สามารถอธิบายได้ด้วยสี่เหลี่ยมผืนผ้าเดียว ตัวอย่างเช่น องค์ประกอบ "เลือก" มีตัวแสดงผล 3 ตัว ได้แก่ ตัวแสดงผลสำหรับพื้นที่แสดงผล ตัวแสดงผลสำหรับกล่องรายการแบบเลื่อนลง และตัวแสดงผลสำหรับปุ่ม นอกจากนี้ เมื่อข้อความถูกแบ่งออกเป็นหลายบรรทัดเนื่องจากความกว้างไม่เพียงพอสำหรับบรรทัดเดียว ระบบจะเพิ่มบรรทัดใหม่เป็นโปรแกรมแสดงผลเพิ่มเติม
อีกตัวอย่างหนึ่งของโปรแกรมแสดงผลหลายโปรแกรมคือ HTML ที่เสียหาย ตามข้อกำหนด CSS องค์ประกอบในบรรทัดต้องมีเฉพาะองค์ประกอบบล็อกหรือองค์ประกอบในบรรทัดเท่านั้น ในกรณีที่มีเนื้อหาแบบผสม ระบบจะสร้างตัวแสดงผลบล็อกแบบไม่ระบุตัวตนเพื่อรวมองค์ประกอบในบรรทัด
ออบเจ็กต์การแสดงผลบางรายการสอดคล้องกับโหนด DOM แต่ไม่ได้อยู่ในตำแหน่งเดียวกันในต้นไม้ องค์ประกอบที่ลอยอยู่และองค์ประกอบที่วางตำแหน่งแบบสัมบูรณ์จะอยู่นอกโฟลว์ โดยจะวางไว้ในส่วนอื่นของต้นไม้และแมปกับเฟรมจริง เฟรมตัวยึดตำแหน่งคือตำแหน่งที่ควรมีรูปภาพ
ขั้นตอนการสร้างต้นไม้
ใน Firefox งานนำเสนอจะได้รับการลงทะเบียนเป็น Listener สำหรับการอัปเดต DOM
การนำเสนอจะมอบสิทธิ์การสร้างเฟรมให้กับ FrameConstructor และตัวสร้างจะแก้ไขสไตล์ (ดูการคำนวณสไตล์) และสร้างเฟรม
ใน WebKit กระบวนการแก้ไขสไตล์และการสร้างโปรแกรมแสดงผลเรียกว่า "การแนบ" โหนด DOM ทุกโหนดมีเมธอด "attach" การแนบเป็นแบบซิงโครนัส การแทรกโหนดลงในแผนผัง DOM จะเรียกใช้เมธอด "attach" ของโหนดใหม่
การประมวลผลแท็ก html และ body จะส่งผลให้เกิดการสร้างรากของแผนผังการแสดงผล
ออบเจ็กต์การแสดงผลรูทจะสอดคล้องกับสิ่งที่ข้อกำหนด CSS เรียกว่าบล็อกที่มีอยู่ ซึ่งก็คือบล็อกบนสุดที่มีบล็อกอื่นๆ ทั้งหมด ขนาดขององค์ประกอบนี้คือวิวพอร์ต ซึ่งก็คือขนาดพื้นที่แสดงผลของหน้าต่างเบราว์เซอร์
Firefox เรียกสิ่งนี้ว่า ViewPortFrame และ WebKit เรียกสิ่งนี้ว่า RenderView
นี่คือออบเจ็กต์การแสดงผลที่เอกสารชี้ไป
ส่วนที่เหลือของแผนผังจะสร้างขึ้นเป็นการแทรกโหนด DOM
ดูข้อกำหนด CSS2 เกี่ยวกับโมเดลการประมวลผล
การคำนวณรูปแบบ
การสร้างแผนผังการแสดงผลต้องคำนวณพร็อพเพอร์ตี้ภาพของออบเจ็กต์การแสดงผลแต่ละรายการ ซึ่งทำได้โดยการคำนวณพร็อพเพอร์ตี้สไตล์ของแต่ละองค์ประกอบ
สไตล์ประกอบด้วยสไตล์ชีตจากต้นทางต่างๆ องค์ประกอบสไตล์ในบรรทัด และพร็อพเพอร์ตี้ภาพใน HTML (เช่น พร็อพเพอร์ตี้ "bgcolor") ซึ่งจะแปลเป็นพร็อพเพอร์ตี้สไตล์ CSS ที่ตรงกันในภายหลัง
ต้นทางของสไตล์ชีตคือสไตล์ชีตเริ่มต้นของเบราว์เซอร์ สไตล์ชีตที่ผู้เขียนหน้าเว็บจัดเตรียมไว้ และสไตล์ชีตของผู้ใช้ ซึ่งเป็นสไตล์ชีตที่ผู้ใช้เบราว์เซอร์จัดเตรียมไว้ (เบราว์เซอร์ช่วยให้คุณกำหนดสไตล์ที่ชื่นชอบได้ เช่น ใน Firefox จะทำได้โดยการวางสไตล์ชีตในโฟลเดอร์ "Firefox Profile")
การคำนวณสไตล์ทำให้เกิดปัญหาบางอย่าง ดังนี้
- ข้อมูลสไตล์เป็นโครงสร้างขนาดใหญ่มากซึ่งมีพร็อพเพอร์ตี้สไตล์จำนวนมาก ซึ่งอาจทำให้เกิดปัญหาเกี่ยวกับหน่วยความจำได้
การค้นหากฎที่ตรงกันสำหรับแต่ละองค์ประกอบอาจทำให้เกิดปัญหาด้านประสิทธิภาพหากไม่ได้เพิ่มประสิทธิภาพ การตรวจสอบรายการกฎทั้งหมดสำหรับแต่ละองค์ประกอบเพื่อค้นหารายการที่ตรงกันเป็นงานที่หนักหน่วง ตัวเลือกอาจมีโครงสร้างที่ซับซ้อนซึ่งอาจทำให้กระบวนการจับคู่เริ่มต้นในเส้นทางที่ดูเหมือนจะดีแต่กลับไม่เป็นผล และต้องลองใช้เส้นทางอื่น
ตัวอย่างเช่น ตัวเลือกแบบผสมนี้
div div div div{ ... }หมายความว่ากฎมีผลกับ
<div>ซึ่งเป็นองค์ประกอบย่อยของ div 3 รายการ สมมติว่าคุณต้องการตรวจสอบว่ากฎมีผลกับองค์ประกอบ<div>ที่ระบุหรือไม่ คุณเลือกเส้นทางหนึ่งในโครงสร้างแบบต้นไม้เพื่อตรวจสอบ คุณอาจต้องข้ามแผนผังโหนดขึ้นไปเพื่อดูว่ามี div เพียง 2 รายการและกฎไม่มีผล จากนั้นคุณต้องลองใช้เส้นทางอื่นๆ ในแผนผังการใช้กฎเกี่ยวข้องกับกฎแบบเรียงซ้อนที่ค่อนข้างซับซ้อนซึ่งกำหนดลำดับชั้นของกฎ
มาดูกันว่าเบราว์เซอร์ต่างๆ เผชิญกับปัญหาเหล่านี้อย่างไร
การแชร์ข้อมูลสไตล์
โหนด WebKit อ้างอิงออบเจ็กต์สไตล์ (RenderStyle) ออบเจ็กต์เหล่านี้สามารถแชร์โดยโหนดในบางเงื่อนไข โหนดเป็นโหนดระดับเดียวกันหรือโหนดระดับเดียวกันที่อยู่คนละสาขา และมีลักษณะดังนี้
- องค์ประกอบต้องอยู่ในสถานะเมาส์เดียวกัน (เช่น องค์ประกอบหนึ่งจะอยู่ใน :hover ขณะที่อีกองค์ประกอบหนึ่งไม่อยู่ไม่ได้)
- องค์ประกอบทั้ง 2 ไม่ควรมีรหัส
- ชื่อแท็กควรตรงกัน
- แอตทริบิวต์คลาสควรตรงกัน
- ชุดแอตทริบิวต์ที่แมปต้องเหมือนกัน
- สถานะลิงก์ต้องตรงกัน
- สถานะการโฟกัสต้องตรงกัน
- องค์ประกอบทั้งสองไม่ควรได้รับผลกระทบจากตัวเลือกแอตทริบิวต์ โดยที่ "ได้รับผลกระทบ" หมายถึงการมีตัวเลือกที่ตรงกันซึ่งใช้ตัวเลือกแอตทริบิวต์ในตำแหน่งใดก็ได้ภายในตัวเลือก
- ต้องไม่มีแอตทริบิวต์สไตล์ในบรรทัดในองค์ประกอบ
- ต้องไม่มีตัวเลือกพี่น้องที่ใช้งานอยู่เลย WebCore จะเปิดสวิตช์ส่วนกลางเมื่อพบตัวเลือกพี่น้องและปิดใช้การแชร์สไตล์สำหรับทั้งเอกสารเมื่อมีตัวเลือกดังกล่าว ซึ่งรวมถึงตัวเลือก + และตัวเลือกอย่าง :first-child และ :last-child
แผนผังกฎของ Firefox
Firefox มีแผนผังเพิ่มเติม 2 แผนผังเพื่อให้คำนวณสไตล์ได้ง่ายขึ้น ได้แก่ แผนผังกฎและแผนผังบริบทสไตล์ WebKit ยังมีออบเจ็กต์สไตล์ด้วย แต่ไม่ได้จัดเก็บไว้ในต้นไม้เหมือนต้นไม้บริบทสไตล์ มีเพียงโหนด DOM เท่านั้นที่ชี้ไปยังสไตล์ที่เกี่ยวข้อง
บริบทของสไตล์มีค่าสิ้นสุด ระบบจะคำนวณค่าโดยใช้กฎการจับคู่ทั้งหมดตามลำดับที่ถูกต้อง และดำเนินการจัดการที่แปลงค่าเหล่านั้นจากค่าเชิงตรรกะเป็นค่าที่เป็นรูปธรรม ตัวอย่างเช่น หากค่าเชิงตรรกะเป็นเปอร์เซ็นต์ของหน้าจอ ระบบจะคำนวณและแปลงค่าเป็นหน่วยสัมบูรณ์ แนวคิดเรื่องแผนผังกฎนั้นชาญฉลาดมาก ซึ่งช่วยให้สามารถแชร์ค่าเหล่านี้ระหว่างโหนดเพื่อหลีกเลี่ยงการคำนวณอีกครั้ง ซึ่งจะช่วยประหยัดพื้นที่ด้วย
กฎที่ตรงกันทั้งหมดจะจัดเก็บไว้ในโครงสร้างแบบต้นไม้ โหนดด้านล่างในเส้นทางจะมีลำดับความสำคัญสูงกว่า แผนผังนี้มีเส้นทางทั้งหมดสำหรับการจับคู่กฎที่พบ การจัดเก็บกฎจะดำเนินการแบบ Lazy ระบบจะไม่คำนวณต้นไม้ที่จุดเริ่มต้นสำหรับทุกโหนด แต่เมื่อใดก็ตามที่ต้องคำนวณสไตล์โหนด ระบบจะเพิ่มเส้นทางที่คำนวณแล้วลงในต้นไม้
แนวคิดคือการมองเส้นทางต้นไม้เป็นคำในพจนานุกรม สมมติว่าเราคำนวณแผนผังกฎนี้แล้ว
สมมติว่าเราต้องจับคู่กฎสำหรับองค์ประกอบอื่นในโครงสร้างเนื้อหา และพบว่ากฎที่ตรงกัน (ตามลำดับที่ถูกต้อง) คือ B-E-I เรามีเส้นทางนี้ในแผนผังอยู่แล้วเนื่องจากเราได้คำนวณเส้นทาง A-B-E-I-L ไปแล้ว ตอนนี้เราจะมีงานที่ต้องทำน้อยลง
มาดูว่าแผนผังช่วยประหยัดเวลาทำงานได้อย่างไร
การแบ่งออกเป็นโครงสร้าง
บริบทของสไตล์จะแบ่งออกเป็นโครงสร้าง โครงสร้างเหล่านั้นมีข้อมูลสไตล์สำหรับหมวดหมู่หนึ่งๆ เช่น เส้นขอบหรือสี พร็อพเพอร์ตี้ทั้งหมดในโครงสร้างจะรับค่าหรือไม่รับค่าก็ได้ พร็อพเพอร์ตี้ที่รับค่ามาคือพร็อพเพอร์ตี้ที่รับค่ามาจากองค์ประกอบหลัก เว้นแต่จะกำหนดโดยองค์ประกอบนั้นๆ พร็อพเพอร์ตี้ที่ไม่ได้สืบทอด (เรียกว่าพร็อพเพอร์ตี้ "รีเซ็ต") จะใช้ค่าเริ่มต้นหากไม่ได้กำหนดไว้
ต้นไม้ช่วยเราด้วยการแคชโครงสร้างทั้งหมด (ซึ่งมีค่าสิ้นสุดที่คำนวณแล้ว) ไว้ในต้นไม้ แนวคิดก็คือ หากโหนดด้านล่างไม่ได้ระบุคำจำกัดความของโครงสร้าง ระบบจะใช้โครงสร้างที่แคชไว้ในโหนดด้านบนได้
การคำนวณบริบทของสไตล์โดยใช้แผนผังกฎ
เมื่อคำนวณบริบทของสไตล์สำหรับองค์ประกอบหนึ่งๆ เราจะคำนวณเส้นทางในแผนผังกฎก่อนหรือใช้เส้นทางที่มีอยู่ จากนั้นเราจะเริ่มใช้กฎในเส้นทางเพื่อเติมโครงสร้างในบริบทสไตล์ใหม่ เราจะเริ่มที่โหนดด้านล่างของเส้นทาง ซึ่งเป็นโหนดที่มีลำดับความสำคัญสูงสุด (โดยปกติจะเป็นตัวเลือกที่เจาะจงที่สุด) และข้ามแผนผังขึ้นไปจนกว่าโครงสร้างจะเต็ม หากไม่มีข้อกำหนดสำหรับโครงสร้างในโหนดกฎนั้น เราจะเพิ่มประสิทธิภาพได้อย่างมาก โดยเราจะขึ้นไปตามโครงสร้างต้นไม้จนกว่าจะพบโหนดที่ระบุไว้อย่างสมบูรณ์และชี้ไปที่โหนดนั้น ซึ่งเป็นการเพิ่มประสิทธิภาพที่ดีที่สุด เนื่องจากมีการแชร์โครงสร้างทั้งหมด ซึ่งจะช่วยประหยัดการคำนวณค่าสิ้นสุดและหน่วยความจำ
หากพบคำจำกัดความบางส่วน เราจะไล่ขึ้นไปตามโครงสร้างจนกว่าจะเติมโครงสร้างได้ครบถ้วน
หากไม่พบคำจำกัดความสำหรับโครงสร้างของเรา ในกรณีที่โครงสร้างเป็นประเภท "ที่รับค่ามา" เราจะชี้ไปยังโครงสร้างของระดับบนในแผนผังบริบท ในกรณีนี้ เรายังแชร์โครงสร้างได้สำเร็จด้วย หากเป็นโครงสร้างรีเซ็ต ระบบจะใช้ค่าเริ่มต้น
หากโหนดที่เฉพาะเจาะจงที่สุดเพิ่มค่า เราจะต้องคำนวณเพิ่มเติมเพื่อแปลงค่าดังกล่าวเป็นค่าจริง จากนั้นเราจะแคชผลลัพธ์ในโหนดต้นไม้เพื่อให้โหนดย่อยใช้ได้
ในกรณีที่องค์ประกอบมีพี่น้องที่ชี้ไปยังโหนดต้นไม้เดียวกัน องค์ประกอบเหล่านั้นจะแชร์บริบทสไตล์ทั้งหมดร่วมกันได้
มาดูตัวอย่างกัน สมมติว่าเรามี 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}
เพื่อให้เข้าใจง่ายขึ้น สมมติว่าเราต้องกรอกข้อมูลในโครงสร้างเพียง 2 รายการ ได้แก่ โครงสร้างสีและโครงสร้างระยะขอบ โครงสร้างสีมีสมาชิกเพียง 1 รายการ ได้แก่ สี โครงสร้างระยะขอบมี 4 ด้าน
แผนผังกฎที่ได้จะมีลักษณะดังนี้ (โหนดจะมีชื่อโหนดกำกับไว้: หมายเลขของกฎที่โหนดชี้ไป)
แผนผังบริบทจะมีลักษณะดังนี้ (ชื่อโหนด: โหนดกฎที่ชี้ไป)
สมมติว่าเราแยกวิเคราะห์ HTML และไปที่แท็ก <div> ที่ 2 เราจำเป็นต้องสร้างบริบทของสไตล์สำหรับโหนดนี้และกรอกโครงสร้างสไตล์
เราจะจับคู่กฎและพบว่ากฎที่ตรงกันสำหรับ <div> คือ 1, 2 และ 6
ซึ่งหมายความว่ามีเส้นทางที่มีอยู่แล้วในแผนผังที่องค์ประกอบของเราใช้ได้ และเราเพียงแค่ต้องเพิ่มโหนดอื่นลงในเส้นทางนั้นสำหรับกฎข้อที่ 6 (โหนด F ในแผนผังกฎ)
เราจะสร้างบริบทของสไตล์และใส่ไว้ในโครงสร้างบริบท บริบทสไตล์ใหม่จะชี้ไปที่โหนด F ในแผนผังกฎ
ตอนนี้เราต้องกรอกโครงสร้างสไตล์ เราจะเริ่มด้วยการกรอกโครงสร้างมาร์จิน เนื่องจากโหนดกฎสุดท้าย (F) ไม่ได้เพิ่มลงในโครงสร้างระยะขอบ เราจึงสามารถขึ้นไปตามโครงสร้างจนกว่าจะพบโครงสร้างที่แคชไว้ซึ่งคำนวณในการแทรกโหนดก่อนหน้าและใช้โครงสร้างนั้น เราจะพบกฎดังกล่าวในโหนด B ซึ่งเป็นโหนดบนสุดที่ระบุกฎมาร์จิ้น
เรามีคำจำกัดความสำหรับโครงสร้างสี ดังนั้นเราจึงใช้โครงสร้างที่แคชไว้ไม่ได้ เนื่องจากสีมีแอตทริบิวต์เดียว เราจึงไม่จำเป็นต้องขึ้นไปตามโครงสร้างเพื่อกรอกแอตทริบิวต์อื่นๆ เราจะคำนวณค่าสิ้นสุด (แปลงสตริงเป็น RGB ฯลฯ) และแคชโครงสร้างที่คำนวณแล้วในโหนดนี้
การทำงานกับองค์ประกอบ <span> ที่ 2 จะง่ายยิ่งขึ้น เราจะจับคู่กฎและสรุปว่ากฎดังกล่าวชี้ไปยังกฎ G เช่นเดียวกับช่วงก่อนหน้า
เนื่องจากเรามีพี่น้องที่ชี้ไปยังโหนดเดียวกัน เราจึงแชร์บริบทสไตล์ทั้งหมดและชี้ไปยังบริบทของ Span ก่อนหน้าได้
สำหรับโครงสร้างที่มีกฎที่รับค่ามาจากระดับบนสุด การแคชจะทำในแผนผังบริบท (พร็อพเพอร์ตี้สีจะรับค่ามาจริงๆ แต่ Firefox จะถือว่าเป็นการรีเซ็ตและแคชไว้ในแผนผังกฎ)
ตัวอย่างเช่น หากเราเพิ่มกฎสำหรับแบบอักษรในย่อหน้า
p {font-family: Verdana; font size: 10px; font-weight: bold}
จากนั้นองค์ประกอบย่อหน้าซึ่งเป็นองค์ประกอบย่อยของ div ในโครงสร้างบริบทจะใช้โครงสร้างแบบอักษรเดียวกันกับองค์ประกอบหลักได้ ซึ่งจะเกิดขึ้นหากไม่ได้ระบุกฎแบบอักษรสำหรับย่อหน้า
ใน WebKit ซึ่งไม่มีแผนผังกฎ ระบบจะข้ามการประกาศที่ตรงกัน 4 ครั้ง ระบบจะใช้พร็อพเพอร์ตี้ที่มีลำดับความสำคัญสูงแต่ไม่สำคัญก่อน (พร็อพเพอร์ตี้ที่ควรใช้ก่อนเนื่องจากพร็อพเพอร์ตี้อื่นๆ ขึ้นอยู่กับพร็อพเพอร์ตี้ดังกล่าว เช่น การแสดงผล) จากนั้นจึงใช้กฎที่มีลำดับความสำคัญสูงและสำคัญ ตามด้วยกฎที่มีลำดับความสำคัญปกติแต่ไม่สำคัญ และกฎที่มีลำดับความสำคัญปกติและสำคัญ ซึ่งหมายความว่าพร็อพเพอร์ตี้ที่ปรากฏหลายครั้งจะได้รับการแก้ไขตามลำดับการเรียงซ้อนที่ถูกต้อง ผู้เล่นคนสุดท้ายจะเป็นผู้ชนะ
สรุปก็คือ การแชร์ออบเจ็กต์สไตล์ (ทั้งหมดหรือบางโครงสร้างภายใน) จะช่วยแก้ปัญหาที่ 1 และ 3 ได้ นอกจากนี้ แผนผังกฎของ Firefox ยังช่วยในการใช้พร็อพเพอร์ตี้ตามลำดับที่ถูกต้องด้วย
การปรับเปลี่ยนกฎเพื่อให้จับคู่ได้ง่าย
กฎสไตล์มีแหล่งที่มาหลายแหล่ง ดังนี้
- กฎ CSS ไม่ว่าจะอยู่ในสไตล์ชีตภายนอกหรือในองค์ประกอบสไตล์
css p {color: blue} - แอตทริบิวต์รูปแบบแทรกในบรรทัด เช่น
html <p style="color: blue" /> - แอตทริบิวต์ภาพ HTML (ซึ่งแมปกับกฎสไตล์ที่เกี่ยวข้อง)
html <p bgcolor="blue" />2 รายการสุดท้ายจะจับคู่กับองค์ประกอบได้ง่ายเนื่องจากเป็นเจ้าของแอตทริบิวต์สไตล์ และแอตทริบิวต์ HTML สามารถแมปได้โดยใช้องค์ประกอบเป็นคีย์
ดังที่ระบุไว้ก่อนหน้านี้ในปัญหาที่ 2 การจับคู่กฎ CSS อาจทำได้ยากกว่า เพื่อแก้ปัญหาความยากนี้ จึงมีการปรับเปลี่ยนกฎเพื่อให้เข้าถึงได้ง่ายขึ้น
หลังจากแยกวิเคราะห์สไตล์ชีตแล้ว ระบบจะเพิ่มกฎลงในแฮชแมปรายการใดรายการหนึ่งตามตัวเลือก โดยมีแผนที่ตามรหัส ตามชื่อคลาส ตามชื่อแท็ก และแผนที่ทั่วไปสำหรับสิ่งที่ไม่เข้ากับหมวดหมู่เหล่านั้น หากตัวเลือกเป็นรหัส ระบบจะเพิ่มกฎลงในแผนที่รหัส หากเป็นคลาส ระบบจะเพิ่มกฎลงในแผนที่คลาส เป็นต้น
การจัดการนี้ช่วยให้จับคู่กฎได้ง่ายขึ้นมาก คุณไม่จำเป็นต้องดูทุกการประกาศ เนื่องจากเราสามารถดึงกฎที่เกี่ยวข้องสำหรับองค์ประกอบจากแผนที่ได้ การเพิ่มประสิทธิภาพนี้จะกำจัดกฎออกไปกว่า 95% เพื่อไม่ให้ต้องพิจารณากฎเหล่านั้นในระหว่างกระบวนการจับคู่(4.1)
ลองดูกฎสไตล์ต่อไปนี้เป็นตัวอย่าง
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
ระบบจะแทรกกฎแรกไว้ในการแมปคลาส ส่วนอาร์กิวเมนต์ที่ 2 จะอยู่ในแมปรหัส และอาร์กิวเมนต์ที่ 3 จะอยู่ในแมปแท็ก
สำหรับส่วนย่อย HTML ต่อไปนี้
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
ก่อนอื่น เราจะพยายามค้นหากฎสำหรับองค์ประกอบ p แผนผังคลาสจะมีคีย์ "error" ซึ่งเป็นที่ที่พบกฎสำหรับ "p.error" องค์ประกอบ div จะมีกฎที่เกี่ยวข้องในการจับคู่รหัส (คีย์คือรหัส) และการจับคู่แท็ก ดังนั้นงานเดียวที่เหลืออยู่คือการค้นหาว่ากฎใดที่ดึงข้อมูลโดยคีย์ที่ตรงกันจริงๆ
ตัวอย่างเช่น หากกฎสำหรับ div คือ
table div {margin: 5px}
ระบบจะยังคงดึงข้อมูลจากแผนที่แท็กเนื่องจากคีย์เป็นตัวเลือกด้านขวาสุด แต่จะไม่ตรงกับองค์ประกอบ div ของเราซึ่งไม่มีระดับบนสุดของตาราง
ทั้ง WebKit และ Firefox ต่างก็ทำการปรับเปลี่ยนนี้
ลำดับการเรียงซ้อนของสไตล์ชีต
ออบเจ็กต์สไตล์มีพร็อพเพอร์ตี้ที่สอดคล้องกับแอตทริบิวต์ภาพทุกรายการ (แอตทริบิวต์ CSS ทั้งหมดแต่เป็นแบบทั่วไปมากกว่า) หากกฎที่ตรงกันไม่ได้กำหนดพร็อพเพอร์ตี้ไว้ ออบเจ็กต์รูปแบบองค์ประกอบหลักจะรับค่าพร็อพเพอร์ตี้บางรายการได้ พร็อพเพอร์ตี้อื่นๆ จะมีค่าเริ่มต้น
ปัญหาจะเกิดขึ้นเมื่อมีคำจำกัดความมากกว่า 1 รายการ ซึ่งลำดับแบบเรียงซ้อนจะช่วยแก้ปัญหานี้ได้
การประกาศพร็อพเพอร์ตี้สไตล์อาจปรากฏในสไตล์ชีตหลายรายการ และปรากฏหลายครั้งในสไตล์ชีตเดียวกัน ซึ่งหมายความว่าลำดับการใช้กฎมีความสำคัญมาก ซึ่งเรียกว่าลำดับ "แบบเรียงซ้อน" ตามข้อกำหนด CSS2 ลำดับการเรียงซ้อน (จากต่ำไปสูง) มีดังนี้
- การประกาศของเบราว์เซอร์
- การประกาศปกติของผู้ใช้
- การประกาศปกติของผู้เขียน
- ประกาศสำคัญของผู้เขียน
- การประกาศสำคัญของผู้ใช้
การประกาศของเบราว์เซอร์มีความสำคัญน้อยที่สุด และผู้ใช้จะลบล้างผู้เขียนได้ก็ต่อเมื่อมีการทำเครื่องหมายการประกาศว่าสำคัญเท่านั้น การประกาศที่มีลำดับเดียวกันจะจัดเรียงตามความเฉพาะเจาะจง แล้วจึงจัดเรียงตามลำดับที่ระบุ แอตทริบิวต์ภาพ HTML จะได้รับการแปลเป็นการประกาศ CSS ที่ตรงกัน ระบบจะถือว่ากฎเหล่านี้เป็นกฎผู้เขียนที่มีลำดับความสำคัญต่ำ
ลักษณะเฉพาะ
ความเฉพาะเจาะจงของตัวเลือกจะกำหนดโดยข้อกำหนด CSS2 ดังนี้
- นับ 1 หากการประกาศมาจากแอตทริบิวต์ "style" แทนที่จะเป็นกฎที่มีตัวเลือก และนับ 0 หากไม่ใช่ (= a)
- นับจำนวนแอตทริบิวต์รหัสในตัวเลือก (= b)
- นับจำนวนแอตทริบิวต์และคลาสเทียมอื่นๆ ในตัวเลือก (= c)
- นับจำนวนชื่อองค์ประกอบและองค์ประกอบจำลองในตัวเลือก (= d)
การเชื่อมโยงตัวเลข 4 ตัว a-b-c-d (ในระบบตัวเลขที่มีฐานขนาดใหญ่) จะให้ความเฉพาะเจาะจง
ฐานตัวเลขที่คุณต้องใช้จะกำหนดโดยจำนวนสูงสุดที่คุณมีในหมวดหมู่ใดหมวดหมู่หนึ่ง
เช่น หาก a=14 คุณสามารถใช้ฐานเลขฐาน 16 ได้ ในกรณีที่เกิดขึ้นได้ยากซึ่ง a=17 คุณจะต้องใช้ฐานตัวเลข 17 หลัก สถานการณ์หลังอาจเกิดขึ้นกับตัวเลือกดังนี้ html body div div p… (17 แท็กในตัวเลือกของคุณ… ไม่น่าเป็นไปได้)
ตัวอย่างมีดังต่อไปนี้
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
กำลังจัดเรียงกฎ
หลังจากจับคู่กฎแล้ว ระบบจะจัดเรียงกฎตามกฎแบบเรียงซ้อน
WebKit ใช้การจัดเรียงแบบบับเบิลสำหรับรายการขนาดเล็ก และการจัดเรียงแบบผสานสำหรับรายการขนาดใหญ่
WebKit ใช้การจัดเรียงโดยลบล้างโอเปอเรเตอร์ > สำหรับกฎต่อไปนี้
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) ? r1.position() > r2.position() : spec1 > spec2;
}
กระบวนการที่ค่อยเป็นค่อยไป
WebKit ใช้แฟล็กที่ทำเครื่องหมายว่าสไตล์ชีตระดับบนสุดทั้งหมด (รวมถึง @imports) โหลดแล้วหรือไม่ หากโหลดสไตล์ไม่สมบูรณ์เมื่อแนบ ระบบจะใช้ตัวยึดตำแหน่งและทำเครื่องหมายไว้ในเอกสาร และจะคำนวณใหม่เมื่อโหลดสไตล์ชีตแล้ว
เลย์เอาต์
เมื่อสร้างและเพิ่มตัวแสดงผลลงในต้นไม้ ตัวแสดงผลจะไม่มีตำแหน่งและขนาด การคำนวณค่าเหล่านี้เรียกว่าเลย์เอาต์หรือการจัดเรียงใหม่
HTML ใช้โมเดลเลย์เอาต์แบบโฟลว์ ซึ่งหมายความว่าส่วนใหญ่แล้วจะคำนวณเรขาคณิตได้ในรอบเดียว องค์ประกอบที่อยู่ "ในโฟลว์" ทีหลังมักจะไม่ส่งผลต่อเรขาคณิตขององค์ประกอบที่อยู่ "ในโฟลว์" ก่อนหน้า ดังนั้นเลย์เอาต์จึงสามารถดำเนินการจากซ้ายไปขวาและจากบนลงล่างในเอกสารได้ แต่ก็มีข้อยกเว้น เช่น ตาราง HTML อาจต้องใช้การส่งมากกว่า 1 ครั้ง
ระบบพิกัดจะสัมพันธ์กับเฟรมรูท ระบบจะใช้พิกัดด้านบนและด้านซ้าย
เลย์เอาต์เป็นกระบวนการที่ทำซ้ำ โดยเริ่มที่ตัวแสดงผลระดับรูท ซึ่งสอดคล้องกับองค์ประกอบ <html> ของเอกสาร HTML เลย์เอาต์จะทำงานซ้ำๆ ต่อไปผ่านลำดับชั้นเฟรมบางส่วนหรือทั้งหมด โดยจะคำนวณข้อมูลทางเรขาคณิตสำหรับโปรแกรมแสดงผลแต่ละโปรแกรมที่ต้องการข้อมูลดังกล่าว
ตำแหน่งของโปรแกรมแสดงผลรูทคือ 0,0 และขนาดของโปรแกรมแสดงผลรูทคือวิวพอร์ต ซึ่งเป็นส่วนที่มองเห็นได้ของหน้าต่างเบราว์เซอร์
โปรแกรมแสดงผลทั้งหมดมีเมธอด "เลย์เอาต์" หรือ "จัดเรียงใหม่" โดยโปรแกรมแสดงผลแต่ละรายการจะเรียกใช้เมธอดเลย์เอาต์ขององค์ประกอบย่อยที่ต้องใช้เลย์เอาต์
ระบบบิตสกปรก
เบราว์เซอร์จะใช้ระบบ "บิตสกปรก" เพื่อไม่ให้ต้องจัดเลย์เอาต์ทั้งหมดสำหรับการเปลี่ยนแปลงเล็กๆ น้อยๆ ทุกครั้ง Renderer ที่มีการเปลี่ยนแปลงหรือเพิ่มจะทำเครื่องหมายตัวเองและองค์ประกอบย่อยเป็น "ไม่สมบูรณ์" ซึ่งหมายความว่าต้องมีเลย์เอาต์
มีแฟล็ก 2 รายการ ได้แก่ "dirty" และ "children are dirty" ซึ่งหมายความว่าแม้ว่าตัวแสดงผลเองอาจไม่มีปัญหา แต่ก็มีองค์ประกอบย่อยอย่างน้อย 1 รายการที่ต้องใช้เลย์เอาต์
เลย์เอาต์แบบรวมและแบบเพิ่มขึ้น
เลย์เอาต์สามารถทริกเกอร์ได้ในแผนผังการแสดงผลทั้งหมด ซึ่งก็คือเลย์เอาต์ "ส่วนกลาง" ซึ่งอาจเกิดขึ้นได้จากสาเหตุต่อไปนี้
- การเปลี่ยนแปลงสไตล์ส่วนกลางที่ส่งผลต่อโปรแกรมแสดงผลทั้งหมด เช่น การเปลี่ยนแปลงขนาดแบบอักษร
- เนื่องจากมีการปรับขนาดหน้าจอ
เลย์เอาต์สามารถเพิ่มขึ้นได้ โดยจะจัดวางเฉพาะตัวแสดงผลที่สกปรกเท่านั้น (ซึ่งอาจทำให้เกิดความเสียหายบางอย่างที่ต้องใช้เลย์เอาต์เพิ่มเติม)
เลย์เอาต์ที่เพิ่มขึ้นจะทริกเกอร์ (แบบอะซิงโครนัส) เมื่อโปรแกรมแสดงผลไม่ถูกต้อง เช่น เมื่อมีการเพิ่มโปรแกรมแสดงผลใหม่ต่อท้ายแผนผังการแสดงผลหลังจากเนื้อหาเพิ่มเติมมาจากเครือข่ายและเพิ่มลงในแผนผัง DOM
เลย์เอาต์แบบอะซิงโครนัสและแบบซิงโครนัส
ระบบจะจัดเลย์เอาต์แบบเพิ่มทีละส่วนแบบอะซิงโครนัส Firefox จะจัดคิว "คำสั่งจัดเรียงใหม่" สำหรับเลย์เอาต์ที่เพิ่มขึ้น และตัวจัดกำหนดการจะเรียกใช้คำสั่งเหล่านี้เป็นกลุ่ม นอกจากนี้ WebKit ยังมีตัวจับเวลาที่เรียกใช้เลย์เอาต์แบบเพิ่มทีละส่วน โดยจะมีการข้ามผ่านโครงสร้างและแสดงผลเลย์เอาต์ "ที่ยังไม่สมบูรณ์"
สคริปต์ที่ขอข้อมูลสไตล์ เช่น "offsetHeight" สามารถทริกเกอร์เลย์เอาต์ที่เพิ่มขึ้นพร้อมกันได้
โดยปกติแล้ว เลย์เอาต์ส่วนกลางจะทริกเกอร์พร้อมกัน
บางครั้งเลย์เอาต์จะถูกทริกเกอร์เป็น Callback หลังจากเลย์เอาต์เริ่มต้นเนื่องจากแอตทริบิวต์บางอย่าง เช่น ตำแหน่งการเลื่อนมีการเปลี่ยนแปลง
การเพิ่มประสิทธิภาพ
เมื่อเลย์เอาต์ถูกทริกเกอร์โดย "การปรับขนาด" หรือการเปลี่ยนแปลงตำแหน่งของโปรแกรมแสดงผล(ไม่ใช่ขนาด) ระบบจะนำขนาดของโปรแกรมแสดงผลมาจากแคชและไม่คำนวณใหม่…
ในบางกรณี ระบบจะแก้ไขเฉพาะแผนผังย่อยและเลย์เอาต์จะไม่เริ่มต้นจากรูท กรณีนี้อาจเกิดขึ้นได้เมื่อการเปลี่ยนแปลงเกิดขึ้นเฉพาะจุดและไม่ส่งผลต่อสิ่งรอบข้าง เช่น ข้อความที่แทรกในช่องข้อความ (มิฉะนั้นการกดแป้นพิมพ์ทุกครั้งจะทริกเกอร์เลย์เอาต์ที่เริ่มต้นจากรูท)
ขั้นตอนการจัดเลย์เอาต์
โดยปกติแล้วเลย์เอาต์จะมีรูปแบบดังนี้
- ตัวแสดงผลหลักจะกำหนดความกว้างของตัวเอง
- ผู้ปกครองจะไปที่ส่วน "บุตรหลาน" และทำดังนี้
- วางตัวแสดงผลย่อย (ตั้งค่า x และ y)
- เรียกใช้เลย์เอาต์ย่อยหากจำเป็น เช่น เลย์เอาต์ย่อยไม่ถูกต้อง หรือเราอยู่ในเลย์เอาต์ส่วนกลาง หรือด้วยเหตุผลอื่นๆ ซึ่งจะคำนวณความสูงของเลย์เอาต์ย่อย
- องค์ประกอบหลักจะใช้ความสูงสะสมขององค์ประกอบย่อย รวมถึงความสูงของระยะขอบและระยะห่างจากขอบเพื่อกำหนดความสูงของตัวเอง ซึ่งจะใช้โดยองค์ประกอบหลักของโปรแกรมแสดงผลหลัก
- ตั้งค่าบิตที่สกปรกเป็นเท็จ
Firefox ใช้ออบเจ็กต์ "สถานะ" (nsHTMLReflowState) เป็นพารามิเตอร์สำหรับเลย์เอาต์ (เรียกว่า "การจัดเรียงใหม่") สถานะดังกล่าวรวมถึงความกว้างขององค์ประกอบหลักด้วย
เอาต์พุตของเลย์เอาต์ Firefox คือออบเจ็กต์ "เมตริก" (nsHTMLReflowMetrics) ซึ่งจะมีความสูงที่คำนวณโดยโปรแกรมแสดงผล
การคำนวณความกว้าง
ความกว้างของโปรแกรมแสดงผลจะคำนวณโดยใช้ความกว้างของบล็อกคอนเทนเนอร์ พร็อพเพอร์ตี้ "width" ของสไตล์ของโปรแกรมแสดงผล ระยะขอบ และเส้นขอบ
เช่น ความกว้างของ div ต่อไปนี้
<div style="width: 30%"/>
WebKit จะคำนวณดังต่อไปนี้(คลาส RenderBox เมธอด calcWidth)
- ความกว้างของคอนเทนเนอร์คือค่าสูงสุดของ availableWidth ของคอนเทนเนอร์และ 0 availableWidth ในกรณีนี้คือ contentWidth ซึ่งคํานวณได้ดังนี้
clientWidth() - paddingLeft() - paddingRight()
clientWidth และ clientHeight แสดงถึงภายในของออบเจ็กต์ที่ไม่รวมเส้นขอบและแถบเลื่อน
ความกว้างขององค์ประกอบคือแอตทริบิวต์สไตล์ "width" ระบบจะคำนวณเป็นค่าสัมบูรณ์โดยการคำนวณเปอร์เซ็นต์ของความกว้างคอนเทนเนอร์
เพิ่มเส้นขอบแนวนอนและระยะห่างจากขอบแล้ว
ทั้งหมดนี้คือการคำนวณ "ความกว้างที่ต้องการ" ตอนนี้ระบบจะคำนวณความกว้างขั้นต่ำและสูงสุด
หากความกว้างที่ต้องการมากกว่าความกว้างสูงสุด ระบบจะใช้ความกว้างสูงสุด หากความกว้างน้อยกว่าความกว้างขั้นต่ำ (หน่วยที่เล็กที่สุดที่แบ่งไม่ได้) ระบบจะใช้ความกว้างขั้นต่ำ
ระบบจะแคชค่าไว้ในกรณีที่จำเป็นต้องใช้เลย์เอาต์ แต่ความกว้างจะไม่เปลี่ยนแปลง
การขึ้นบรรทัดใหม่
เมื่อโปรแกรมแสดงผลที่อยู่ตรงกลางเลย์เอาต์ตัดสินใจว่าต้องแบ่ง โปรแกรมแสดงผลจะหยุดและส่งต่อไปยังระดับบนของเลย์เอาต์ว่าต้องแบ่ง โดยผู้ปกครองจะสร้างโปรแกรมแสดงผลเพิ่มเติมและเรียกใช้เลย์เอาต์ในโปรแกรมแสดงผลเหล่านั้น
การวาดภาพ
ในขั้นตอนการวาด ระบบจะข้ามผ่านแผนผังการแสดงผลและเรียกใช้เมธอด "paint()" ของโปรแกรมแสดงผลเพื่อแสดงเนื้อหาบนหน้าจอ การวาดใช้คอมโพเนนต์โครงสร้างพื้นฐานของ UI
ทั่วโลกและเพิ่มขึ้น
การวาดภาพอาจเป็นแบบทั่วโลกเช่นเดียวกับเลย์เอาต์ โดยจะวาดทั้งโครงสร้างหรือวาดแบบเพิ่มทีละส่วนก็ได้ ในการวาดภาพแบบเพิ่มทีละส่วน ตัวแสดงผลบางตัวจะเปลี่ยนแปลงในลักษณะที่ไม่ส่งผลต่อทั้งโครงสร้าง ตัวแสดงผลที่เปลี่ยนแปลงจะทำให้สี่เหลี่ยมผืนผ้าบนหน้าจอใช้งานไม่ได้ ซึ่งทำให้ระบบปฏิบัติการมองว่าพื้นที่ดังกล่าวเป็น "พื้นที่ที่ต้องอัปเดต" และสร้างเหตุการณ์ "วาด" ระบบปฏิบัติการจะดำเนินการอย่างชาญฉลาดและรวมหลายภูมิภาคเข้าเป็นหนึ่งเดียว ใน Chrome จะมีความซับซ้อนมากกว่าเนื่องจากโปรแกรมแสดงผลอยู่ในกระบวนการที่แตกต่างจากกระบวนการหลัก Chrome จะจำลองลักษณะการทำงานของระบบปฏิบัติการในระดับหนึ่ง การนำเสนอจะรอรับเหตุการณ์เหล่านี้และมอบสิทธิ์ข้อความให้กับรูทการแสดงผล ระบบจะข้ามแผนผังจนกว่าจะถึงโปรแกรมแสดงผลที่เกี่ยวข้อง โดยจะวาดตัวเองใหม่ (และมักจะวาดองค์ประกอบย่อยด้วย)
ลำดับการวาดภาพ
CSS2 กำหนดลำดับของกระบวนการวาด ซึ่งจริงๆ แล้วเป็นลำดับที่องค์ประกอบซ้อนกันในบริบทการซ้อน ลำดับนี้ส่งผลต่อการวาดเนื่องจากระบบจะวาดกองซ้อนจากด้านหลังไปด้านหน้า ลำดับการซ้อนกันของโปรแกรมแสดงผลบล็อกมีดังนี้
- สีพื้นหลัง
- ภาพพื้นหลัง
- border
- เด็ก
- outline
รายการแสดงผล Firefox
Firefox จะตรวจสอบแผนผังการแสดงผลและสร้างรายการแสดงผลสำหรับสี่เหลี่ยมผืนผ้าที่วาด โดยจะมีโปรแกรมแสดงผลที่เกี่ยวข้องกับสี่เหลี่ยมผืนผ้าในลำดับการวาดที่ถูกต้อง (พื้นหลังของโปรแกรมแสดงผล จากนั้นจึงเป็นเส้นขอบ ฯลฯ)
ด้วยวิธีนี้ ระบบจะทำการข้ามผ่านโครงสร้างเพียงครั้งเดียวเพื่อวาดใหม่ แทนที่จะทำหลายครั้ง เช่น วาดพื้นหลังทั้งหมด จากนั้นวาดรูปภาพทั้งหมด แล้ววาดเส้นขอบทั้งหมด เป็นต้น
Firefox เพิ่มประสิทธิภาพกระบวนการโดยไม่เพิ่มองค์ประกอบที่จะซ่อน เช่น องค์ประกอบที่อยู่ใต้องค์ประกอบทึบแสงอื่นๆ ทั้งหมด
พื้นที่เก็บข้อมูลสี่เหลี่ยมผืนผ้า WebKit
ก่อนที่จะวาดใหม่ WebKit จะบันทึกสี่เหลี่ยมผืนผ้าเก่าเป็นบิตแมป จากนั้นจะแสดงเฉพาะความแตกต่างระหว่างสี่เหลี่ยมผืนผ้าใหม่และเก่า
การเปลี่ยนแปลงแบบไดนามิก
เบราว์เซอร์จะพยายามดำเนินการให้น้อยที่สุดเพื่อตอบสนองต่อการเปลี่ยนแปลง ดังนั้นการเปลี่ยนแปลงสีขององค์ประกอบจะทำให้เกิดการทาสีองค์ประกอบใหม่เท่านั้น การเปลี่ยนแปลงตำแหน่งองค์ประกอบจะทำให้เกิดเลย์เอาต์และการทาสีองค์ประกอบใหม่ รวมถึงองค์ประกอบย่อยและอาจรวมถึงองค์ประกอบระดับเดียวกันด้วย การเพิ่มโหนด DOM จะทำให้เกิดเลย์เอาต์และการวาดโหนดใหม่ การเปลี่ยนแปลงครั้งใหญ่ เช่น การเพิ่มขนาดแบบอักษรขององค์ประกอบ "html" จะทำให้แคชไม่ถูกต้อง รวมถึงจัดเลย์เอาต์และวาดแผนผังทั้งหมดใหม่
ชุดข้อความของเครื่องมือแสดงผล
เครื่องมือแสดงผลเป็นแบบเทรดเดี่ยว การดำเนินการเกือบทั้งหมด ยกเว้นการดำเนินการของเครือข่าย จะเกิดขึ้นในชุดข้อความเดียว ใน Firefox และ Safari เทรดนี้คือเทรดหลักของเบราว์เซอร์ ใน Chrome จะเป็นเธรดหลักของกระบวนการแท็บ
การดำเนินการของเครือข่ายสามารถทำได้โดยเทรดแบบขนานหลายเทรด จำนวนการเชื่อมต่อพร้อมกันมีจำกัด (โดยปกติคือ 2-6 การเชื่อมต่อ)
วนรอบเหตุการณ์
เทรดหลักของเบราว์เซอร์คือการวนรอบเหตุการณ์ ซึ่งเป็นลูปที่ไม่มีที่สิ้นสุดที่ทำให้กระบวนการดำเนินต่อไป โดยจะรอเหตุการณ์ (เช่น เหตุการณ์เลย์เอาต์และการวาด) และประมวลผลเหตุการณ์เหล่านั้น โค้ด Firefox สำหรับวนรอบเหตุการณ์หลักมีดังนี้
while (!mExiting)
NS_ProcessNextEvent(thread);
โมเดลภาพ CSS2
Canvas
ตามข้อกำหนด CSS2 คำว่า Canvas หมายถึง "พื้นที่ที่แสดงผลโครงสร้างการจัดรูปแบบ" ซึ่งเป็นตำแหน่งที่เบราว์เซอร์แสดงเนื้อหา
Canvas จะไม่มีที่สิ้นสุดสำหรับแต่ละมิติข้อมูลของพื้นที่ทำงาน แต่เบราว์เซอร์จะเลือกความกว้างเริ่มต้นตามขนาดของวิวพอร์ต
ตามข้อมูลจาก www.w3.org/TR/CSS2/zindex.html Canvas จะโปร่งใสหากอยู่ภายใน Canvas อื่น และจะได้รับสีที่เบราว์เซอร์กำหนดหากไม่ได้อยู่ภายใน Canvas อื่น
โมเดล CSS Box
โมเดลกล่อง CSS อธิบายกล่องสี่เหลี่ยมผืนผ้าที่สร้างขึ้นสำหรับองค์ประกอบในแผนผังเอกสารและจัดวางตามโมเดลการจัดรูปแบบภาพ
แต่ละกล่องจะมีพื้นที่เนื้อหา (เช่น ข้อความ รูปภาพ ฯลฯ) และพื้นที่เสริมรอบๆ ได้แก่ ระยะห่างจากขอบ เส้นขอบ และระยะขอบ
แต่ละโหนดจะสร้างกล่องดังกล่าว 0…n กล่อง
องค์ประกอบทั้งหมดมีพร็อพเพอร์ตี้ "display" ที่กำหนดประเภทของกล่องที่จะสร้างขึ้น
ตัวอย่าง
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
ค่าเริ่มต้นคือ "inline" แต่สไตล์ชีตของเบราว์เซอร์อาจตั้งค่าเริ่มต้นอื่นๆ เช่น การแสดงผลเริ่มต้นสำหรับองค์ประกอบ "div" คือบล็อก
ดูตัวอย่างสไตล์ชีตเริ่มต้นได้ที่ www.w3.org/TR/CSS2/sample.html
รูปแบบการกำหนดตำแหน่ง
โดยมี 3 รูปแบบดังนี้
- ปกติ: วัตถุจะอยู่ในตำแหน่งตามตำแหน่งในเอกสาร ซึ่งหมายความว่าตำแหน่งในแผนผังการแสดงผลจะเหมือนกับตำแหน่งในแผนผัง DOM และจัดวางตามประเภทและขนาดของกล่อง
- ลอย: ระบบจะจัดวางออบเจ็กต์เหมือนโฟลว์ปกติก่อน จากนั้นจึงย้ายไปทางซ้ายหรือขวาให้มากที่สุด
- สัมบูรณ์: ระบบจะวางออบเจ็กต์ในแผนผังการแสดงผลในตำแหน่งที่ต่างจากในแผนผัง DOM
รูปแบบการจัดตำแหน่งจะกำหนดโดยพร็อพเพอร์ตี้ "position" และแอตทริบิวต์ "float"
- static และ relative ทำให้เกิดโฟลว์ปกติ
- absolute และ fixed ทำให้เกิดการกำหนดตำแหน่งแบบสัมบูรณ์
ในการจัดตำแหน่งแบบคงที่ จะไม่มีการกำหนดตำแหน่งและจะใช้การจัดตำแหน่งเริ่มต้น ในรูปแบบอื่นๆ ผู้เขียนจะระบุตำแหน่งเป็นด้านบน ด้านล่าง ด้านซ้าย หรือด้านขวา
เลย์เอาต์ของกล่องจะกำหนดโดยสิ่งต่อไปนี้
- ประเภทกล่อง
- ขนาดกล่อง
- รูปแบบการกำหนดตำแหน่ง
- ข้อมูลภายนอก เช่น ขนาดรูปภาพและขนาดหน้าจอ
ประเภทกล่อง
กล่องบล็อก: สร้างบล็อก - มีสี่เหลี่ยมผืนผ้าของตัวเองในหน้าต่างเบราว์เซอร์
กล่องในบรรทัด: ไม่มีบล็อกของตัวเอง แต่อยู่ภายในบล็อกที่มีอยู่
บล็อกจะจัดรูปแบบตามแนวตั้งต่อกันไปเรื่อยๆ โฆษณาในบรรทัดจะจัดรูปแบบในแนวนอน
กล่องในบรรทัดจะอยู่ภายในบรรทัดหรือ "กล่องบรรทัด" บรรทัดจะมีความสูงอย่างน้อยเท่ากับกล่องที่สูงที่สุด แต่ก็อาจสูงกว่าได้เมื่อกล่องจัดแนว "เส้นฐาน" ซึ่งหมายความว่าส่วนล่างขององค์ประกอบจะจัดแนวที่จุดของกล่องอื่นที่ไม่ใช่ด้านล่าง หากความกว้างของคอนเทนเนอร์ไม่เพียงพอ ระบบจะวางองค์ประกอบแบบอินไลน์ไว้หลายบรรทัด ซึ่งมักจะเกิดขึ้นในย่อหน้า
การอธิบายหัวข้อต่างๆ แก่ลูกค้า
ญาติ
การจัดตำแหน่งแบบสัมพัทธ์ - จัดตำแหน่งตามปกติแล้วย้ายตามเดลต้าที่ต้องการ
แบบลอย
กล่องลอยจะเลื่อนไปทางซ้ายหรือขวาของบรรทัด ฟีเจอร์ที่น่าสนใจคือกล่องอื่นๆ จะไหลไปรอบๆ HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
ซึ่งจะมีลักษณะดังนี้
สัมบูรณ์และคงที่
เลย์เอาต์จะได้รับการกำหนดอย่างชัดเจนโดยไม่คำนึงถึงขั้นตอนปกติ องค์ประกอบนี้ไม่ได้มีส่วนร่วมในขั้นตอนปกติ ขนาดจะสัมพันธ์กับคอนเทนเนอร์ ในตำแหน่งคงที่ คอนเทนเนอร์คือวิวพอร์ต
การนำเสนอแบบหลายชั้น
ซึ่งระบุโดยพร็อพเพอร์ตี้ CSS ของดัชนีลำดับ Z ซึ่งแสดงถึงมิติที่ 3 ของกล่อง นั่นคือตำแหน่งตาม "แกน Z"
กล่องจะแบ่งออกเป็นสแต็ก (เรียกว่าบริบทการซ้อน) ในแต่ละสแต็ก องค์ประกอบด้านหลังจะแสดงก่อน และองค์ประกอบด้านหน้าจะแสดงด้านบน ซึ่งอยู่ใกล้กับผู้ใช้มากกว่า ในกรณีที่ทับซ้อนกัน องค์ประกอบที่อยู่ด้านหน้าสุดจะซ่อนองค์ประกอบก่อนหน้า
กองจะเรียงตามพร็อพเพอร์ตี้ดัชนีลำดับ Z กล่องที่มีพร็อพเพอร์ตี้ "z-index" จะสร้างสแต็กในเครื่อง วิวพอร์ตมีสแต็กด้านนอก
ตัวอย่าง
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div
style="z-index: 3;background-color:red; width: 1in; height: 1in; ">
</div>
<div
style="z-index: 1;background-color:green;width: 2in; height: 2in;">
</div>
</p>
ผลลัพธ์ที่ได้จะเป็นดังนี้
แม้ว่า div สีแดงจะอยู่ก่อนสีเขียวในมาร์กอัป และจะได้รับการลงสีก่อนในขั้นตอนปกติ แต่พร็อพเพอร์ตี้ดัชนีลำดับ Z จะสูงกว่า จึงอยู่ด้านหน้ามากกว่าในสแต็กที่กล่องรูทเก็บไว้
แหล่งข้อมูล
สถาปัตยกรรมเบราว์เซอร์
- Grosskurth, Alan. สถาปัตยกรรมอ้างอิงสำหรับเว็บเบราว์เซอร์ (pdf)
- Gupta, Vineet. วิธีการทำงานของเบราว์เซอร์ - ตอนที่ 1 - สถาปัตยกรรม
การแยกวิเคราะห์
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (หรือที่เรียกว่า "Dragon book"), Addison-Wesley, 1986
- Rick Jelliffe The Bold and the Beautiful: two new drafts for HTML 5.
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for นักพัฒนาเว็บ (วิดีโอ Google Tech Talk)
- L. David Baron, Layout Engine ของ Mozilla
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- ภาพรวม Gecko โดย Chris Waterson
- Alexander Larsson, The life of an HTML HTTP request
WebKit
- David Hyatt, Implementing CSS(part 1)
- David Hyatt, An Overview of WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, The FOUC Problem
ข้อกำหนดเฉพาะของ W3C
วิธีการสร้างเบราว์เซอร์
คำแปล
หน้านี้ได้รับการแปลเป็นภาษาญี่ปุ่น 2 ครั้ง ดังนี้
- How Browsers Work - เบื้องหลังของเว็บเบราว์เซอร์สมัยใหม่ (ja) โดย @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 โดย @ikeike443 และ @kiyoto01
คุณสามารถดูคำแปลที่โฮสต์ภายนอกเป็นภาษาเกาหลีและตุรกีได้
ขอบคุณทุกคน