Behind the scenes of modern web browsers
Lời nói đầu
Tài liệu hướng dẫn toàn diện này về các hoạt động nội bộ của WebKit và Gecko là kết quả của nhiều nghiên cứu do nhà phát triển người Israel Tali Garsiel thực hiện. Trong vài năm, cô đã xem xét tất cả dữ liệu đã xuất bản về nội bộ trình duyệt và dành nhiều thời gian để đọc mã nguồn của trình duyệt web. Cô viết:
Là một nhà phát triển web, việc tìm hiểu nội dung bên trong của các hoạt động trên trình duyệt sẽ giúp bạn đưa ra quyết định sáng suốt hơn và biết được lý do đằng sau các phương pháp hay nhất để phát triển. Mặc dù tài liệu này khá dài, nhưng bạn nên dành chút thời gian để tìm hiểu kỹ. Bạn sẽ hài lòng với những gì bạn thiết lập.
Paul Irish, Quan hệ nhà phát triển Chrome
Giới thiệu
Trình duyệt web là phần mềm được sử dụng rộng rãi nhất. Trong bài viết này, tôi sẽ giải thích cách
chúng hoạt động trong thầm lặng. Chúng ta sẽ xem điều gì xảy ra khi bạn nhập google.com vào thanh địa chỉ cho đến khi bạn thấy trang Google trên màn hình trình duyệt.
Các trình duyệt mà chúng ta sẽ thảo luận
Hiện nay, có 5 trình duyệt chính được dùng trên máy tính: Chrome, Internet Explorer, Firefox, Safari và Opera. Trên thiết bị di động, các trình duyệt chính là Trình duyệt Android, iPhone, Opera Mini và Opera Mobile, Trình duyệt UC, trình duyệt Nokia S40/S60 và Chrome. Tất cả các trình duyệt này, ngoại trừ trình duyệt Opera, đều dựa trên WebKit. Tôi sẽ đưa ra ví dụ về các trình duyệt nguồn mở Firefox và Chrome, cũng như Safari (một phần là nguồn mở). Theo số liệu thống kê của StatCounter (tính đến tháng 6 năm 2013), Chrome, Firefox và Safari chiếm khoảng 71% mức sử dụng trình duyệt dành cho máy tính trên toàn cầu. Trên thiết bị di động, Trình duyệt Android, iPhone và Chrome chiếm khoảng 54% mức sử dụng.
Chức năng chính của trình duyệt
Chức năng chính của trình duyệt là trình bày tài nguyên web mà bạn chọn bằng cách yêu cầu tài nguyên đó từ máy chủ và hiển thị tài nguyên đó trong cửa sổ trình duyệt. Tài nguyên này thường là một tài liệu HTML, nhưng cũng có thể là tệp PDF, hình ảnh hoặc một số loại nội dung khác. Vị trí của tài nguyên do người dùng chỉ định bằng URI (Giá trị nhận dạng tài nguyên thống nhất).
Cách trình duyệt diễn giải và hiển thị tệp HTML được chỉ định trong quy cách của HTML và CSS. Các thông số kỹ thuật này do tổ chức W3C (World Wide Web Consortium) duy trì. Đây là tổ chức tiêu chuẩn cho web. Trong nhiều năm, các trình duyệt chỉ tuân thủ một phần thông số kỹ thuật và phát triển các tiện ích của riêng mình. Điều này gây ra các vấn đề nghiêm trọng về khả năng tương thích cho các tác giả web. Ngày nay, hầu hết các trình duyệt đều tuân thủ ít nhiều các thông số kỹ thuật.
Giao diện người dùng của các trình duyệt có nhiều điểm chung. Sau đây là một số thành phần giao diện người dùng phổ biến:
- Thanh địa chỉ để chèn URI
- Nút quay lại và tiến lên
- Các lựa chọn đánh dấu trang
- Nút làm mới và dừng để làm mới hoặc dừng tải tài liệu hiện tại
- Nút trang chủ đưa bạn đến trang chủ
Thật kỳ lạ, giao diện người dùng của trình duyệt không được chỉ định trong bất kỳ thông số kỹ thuật chính thức nào, nó chỉ xuất phát từ các phương pháp hay được định hình qua nhiều năm kinh nghiệm và bởi các trình duyệt bắt chước lẫn nhau. Thông số kỹ thuật HTML5 không xác định các phần tử trên giao diện người dùng mà trình duyệt phải có, nhưng liệt kê một số thành phần phổ biến. Trong số đó có thanh địa chỉ, thanh trạng thái và thanh công cụ. Tất nhiên, có những tính năng chỉ có trên một trình duyệt cụ thể, chẳng hạn như trình quản lý tải xuống của Firefox.
Cơ sở hạ tầng cấp cao
Các thành phần chính của trình duyệt là:
- Giao diện người dùng: bao gồm thanh địa chỉ, nút quay lại/tiến lên, trình đơn đánh dấu trang, v.v. Mọi phần của màn hình trình duyệt ngoại trừ cửa sổ nơi bạn thấy trang được yêu cầu.
- Công cụ trình duyệt: sắp xếp các hành động giữa giao diện người dùng và công cụ kết xuất.
- Công cụ kết xuất: chịu trách nhiệm hiển thị nội dung được yêu cầu. Ví dụ: nếu nội dung được yêu cầu là HTML, thì công cụ kết xuất sẽ phân tích cú pháp HTML và CSS, rồi hiển thị nội dung đã phân tích cú pháp trên màn hình.
- Kết nối mạng: đối với các lệnh gọi mạng như yêu cầu HTTP, sử dụng các phương thức triển khai khác nhau cho các nền tảng khác nhau phía sau một giao diện độc lập với nền tảng.
- Phần phụ trợ giao diện người dùng: dùng để vẽ các tiện ích cơ bản như hộp kết hợp và cửa sổ. Phần phụ trợ này hiển thị một giao diện chung không dành riêng cho nền tảng. Bên dưới, tính năng này sử dụng các phương thức giao diện người dùng của hệ điều hành.
- Trình thông dịch JavaScript. Dùng để phân tích cú pháp và thực thi mã JavaScript.
- Lưu trữ dữ liệu. Đây là một lớp lưu trữ lâu dài. Trình duyệt có thể cần lưu tất cả các loại dữ liệu trên thiết bị, chẳng hạn như cookie. Trình duyệt cũng hỗ trợ các cơ chế lưu trữ như localStorage, IndexedDB, WebSQL và FileSystem.
Điều quan trọng cần lưu ý là các trình duyệt như Chrome chạy nhiều phiên bản của công cụ kết xuất: một phiên bản cho mỗi thẻ. Mỗi thẻ chạy trong một quy trình riêng biệt.
Công cụ kết xuất
Trách nhiệm của công cụ kết xuất là… Kết xuất, tức là hiển thị nội dung được yêu cầu trên màn hình trình duyệt.
Theo mặc định, công cụ kết xuất có thể hiển thị tài liệu và hình ảnh HTML và XML. Trình duyệt có thể hiển thị các loại dữ liệu khác thông qua trình bổ trợ hoặc tiện ích; ví dụ: hiển thị tài liệu PDF bằng trình bổ trợ trình xem PDF. Tuy nhiên, trong chương này, chúng ta sẽ tập trung vào trường hợp sử dụng chính: hiển thị HTML và hình ảnh được định dạng bằng CSS.
Các trình duyệt khác nhau sử dụng các công cụ kết xuất khác nhau: Internet Explorer sử dụng Trident, Firefox sử dụng Gecko, Safari sử dụng WebKit. Chrome và Opera (từ phiên bản 15) sử dụng Blink, một nhánh của WebKit.
WebKit là một công cụ kết xuất nguồn mở, ban đầu là một công cụ cho nền tảng Linux và được Apple sửa đổi để hỗ trợ Mac và Windows.
Quy trình chính
Công cụ kết xuất sẽ bắt đầu lấy nội dung của tài liệu được yêu cầu từ tầng mạng. Quá trình này thường được thực hiện theo các đoạn 8kB.
Sau đó, đây là quy trình cơ bản của công cụ kết xuất:
Công cụ kết xuất sẽ bắt đầu phân tích cú pháp tài liệu HTML và chuyển đổi các phần tử thành nút DOM trong một cây có tên là "cây nội dung". Công cụ này sẽ phân tích cú pháp dữ liệu kiểu, cả trong tệp CSS bên ngoài và trong các phần tử kiểu. Thông tin về kiểu dáng cùng với hướng dẫn trực quan trong HTML sẽ được dùng để tạo một cây khác: cây kết xuất.
Cây kết xuất chứa các hình chữ nhật có thuộc tính hình ảnh như màu sắc và kích thước. Các hình chữ nhật được sắp xếp theo đúng thứ tự để hiển thị trên màn hình.
Sau khi xây dựng cây kết xuất, cây này sẽ trải qua một quy trình "bố cục". Điều này có nghĩa là cung cấp cho mỗi nút toạ độ chính xác nơi nút đó sẽ xuất hiện trên màn hình. Giai đoạn tiếp theo là vẽ – cây kết xuất sẽ được duyệt qua và mỗi nút sẽ được vẽ bằng lớp phụ trợ giao diện người dùng.
Bạn cần hiểu rằng đây là một quá trình diễn ra dần dần. Để mang lại trải nghiệm tốt hơn cho người dùng, công cụ kết xuất sẽ cố gắng hiển thị nội dung trên màn hình càng sớm càng tốt. Trình duyệt sẽ không đợi cho đến khi phân tích cú pháp xong tất cả HTML rồi mới bắt đầu tạo và bố trí cây hiển thị. Các phần của nội dung sẽ được phân tích cú pháp và hiển thị, trong khi quá trình này tiếp tục với phần còn lại của nội dung liên tục đến từ mạng.
Ví dụ về quy trình chính
Từ hình 3 và 4, bạn có thể thấy rằng mặc dù WebKit và Gecko sử dụng thuật ngữ hơi khác nhau, nhưng về cơ bản thì quy trình là giống nhau.
Gecko gọi cây của các phần tử được định dạng trực quan là "Cây khung". Mỗi phần tử là một khung hình. WebKit sử dụng thuật ngữ "Cây kết xuất" và cây này bao gồm "Đối tượng kết xuất". WebKit sử dụng thuật ngữ "bố cục" để đặt các phần tử, trong khi Gecko gọi là "Reflow". "Tệp đính kèm" là thuật ngữ của WebKit để kết nối các nút DOM và thông tin hình ảnh để tạo cây kết xuất. Một điểm khác biệt nhỏ không liên quan đến ngữ nghĩa là Gecko có thêm một lớp giữa HTML và cây DOM. Nó được gọi là "bồn rửa nội dung" và là một nhà máy để tạo các phần tử DOM. Chúng ta sẽ nói về từng phần của quy trình:
Phân tích cú pháp – Chung
Vì phân tích cú pháp là một quy trình rất quan trọng trong công cụ kết xuất, nên chúng ta sẽ tìm hiểu sâu hơn một chút. Hãy bắt đầu bằng phần giới thiệu ngắn về việc phân tích cú pháp.
Phân tích cú pháp một tài liệu có nghĩa là dịch tài liệu đó sang một cấu trúc mà mã có thể sử dụng. Kết quả của quá trình phân tích cú pháp thường là một cây nút đại diện cho cấu trúc của tài liệu. Đây được gọi là cây phân tích cú pháp hoặc cây cú pháp.
Ví dụ: việc phân tích cú pháp biểu thức 2 + 3 - 1 có thể trả về cây sau:
Ngữ pháp
Quá trình phân tích cú pháp dựa trên các quy tắc cú pháp mà tài liệu tuân theo: ngôn ngữ hoặc định dạng mà tài liệu được viết. Mỗi định dạng mà bạn có thể phân tích cú pháp phải có ngữ pháp tất định bao gồm từ vựng và quy tắc cú pháp. Đây được gọi là ngữ pháp phi ngữ cảnh. Ngôn ngữ của con người không phải là ngôn ngữ như vậy, do đó không thể phân tích cú pháp bằng các kỹ thuật phân tích cú pháp thông thường.
Sự kết hợp giữa trình phân tích cú pháp và trình phân tích từ vựng
Quá trình phân tích cú pháp có thể được chia thành hai quy trình phụ: phân tích từ vựng và phân tích cú pháp.
Phân tích từ vựng là quá trình chia dữ liệu đầu vào thành các mã thông báo. Mã thông báo là từ vựng ngôn ngữ: tập hợp các khối xây dựng hợp lệ. Trong ngôn ngữ của con người, từ vựng sẽ bao gồm tất cả các từ xuất hiện trong từ điển của ngôn ngữ đó.
Phân tích cú pháp là việc áp dụng các quy tắc cú pháp ngôn ngữ.
Trình phân tích cú pháp thường chia công việc giữa hai thành phần: trình phân tích từ vựng (đôi khi được gọi là trình phân tích mã thông báo) chịu trách nhiệm chia dữ liệu đầu vào thành các mã thông báo hợp lệ và trình phân tích cú pháp chịu trách nhiệm xây dựng cây phân tích cú pháp bằng cách phân tích cấu trúc tài liệu theo các quy tắc cú pháp ngôn ngữ.
Trình phân tích từ vựng biết cách loại bỏ các ký tự không liên quan như khoảng trắng và dấu ngắt dòng.
Quá trình phân tích cú pháp là một quá trình lặp lại. Trình phân tích cú pháp thường sẽ yêu cầu trình phân tích từ vựng cung cấp một mã thông báo mới và cố gắng so khớp mã thông báo đó với một trong các quy tắc cú pháp. Nếu một quy tắc khớp, thì một nút tương ứng với mã thông báo sẽ được thêm vào cây phân tích cú pháp và trình phân tích cú pháp sẽ yêu cầu một mã thông báo khác.
Nếu không có quy tắc nào khớp, trình phân tích cú pháp sẽ lưu trữ mã thông báo trong nội bộ và tiếp tục yêu cầu mã thông báo cho đến khi tìm thấy quy tắc khớp với tất cả mã thông báo được lưu trữ nội bộ. Nếu không tìm thấy quy tắc nào, trình phân tích cú pháp sẽ đưa ra một ngoại lệ. Điều này có nghĩa là tài liệu không hợp lệ và có lỗi cú pháp.
Dịch
Trong nhiều trường hợp, cây phân tích cú pháp không phải là sản phẩm cuối cùng. Phân tích cú pháp thường được dùng trong bản dịch: chuyển đổi tài liệu đầu vào sang một định dạng khác. Một ví dụ là quá trình biên dịch. Trình biên dịch biên dịch mã nguồn thành mã máy trước tiên sẽ phân tích cú pháp mã nguồn thành cây phân tích cú pháp, sau đó dịch cây này thành tài liệu mã máy.
Ví dụ về phân tích cú pháp
Trong hình 5, chúng ta đã xây dựng một cây phân tích cú pháp từ một biểu thức toán học. Hãy thử xác định một ngôn ngữ toán học đơn giản và xem quy trình phân tích cú pháp.
Cú pháp:
- Các thành phần cú pháp ngôn ngữ là biểu thức, thuật ngữ và toán tử.
- Ngôn ngữ của chúng ta có thể bao gồm số lượng biểu thức bất kỳ.
- Một biểu thức được định nghĩa là một "số hạng" theo sau là một "phép toán" theo sau là một số hạng khác
- Một phép toán là một mã thông báo cộng hoặc mã thông báo trừ
- Một số hạng là một mã thông báo số nguyên hoặc một biểu thức
Hãy phân tích dữ liệu đầu vào 2 + 3 - 1.
Chuỗi con đầu tiên khớp với một quy tắc là 2: theo quy tắc số 5, đây là một cụm từ.
Kết quả khớp thứ hai là 2 + 3: kết quả này khớp với quy tắc thứ ba: một số hạng theo sau là một phép toán, theo sau là một số hạng khác.
Kết quả trùng khớp tiếp theo sẽ chỉ được tìm thấy ở cuối dữ liệu đầu vào.
2 + 3 - 1 là một biểu thức vì chúng ta đã biết rằng 2 + 3 là một số hạng, vì vậy, chúng ta có một số hạng, theo sau là một phép toán, theo sau là một số hạng khác.
2 + + sẽ không khớp với bất kỳ quy tắc nào nên là dữ liệu đầu vào không hợp lệ.
Định nghĩa chính thức về từ vựng và cú pháp
Từ vựng thường được biểu thị bằng biểu thức chính quy.
Ví dụ: ngôn ngữ của chúng ta sẽ được xác định như sau:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
Như bạn thấy, số nguyên được xác định bằng biểu thức chính quy.
Cú pháp thường được xác định theo một định dạng gọi là BNF. Ngôn ngữ của chúng ta sẽ được định nghĩa như sau:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
Chúng tôi đã nói rằng một ngôn ngữ có thể được phân tích cú pháp bằng các trình phân tích cú pháp thông thường nếu ngữ pháp của ngôn ngữ đó là ngữ pháp phi ngữ cảnh. Định nghĩa trực quan về ngữ pháp phi ngữ cảnh là ngữ pháp có thể được thể hiện hoàn toàn bằng BNF. Để biết định nghĩa chính thức, hãy xem bài viết của Wikipedia về Ngữ pháp phi ngữ cảnh
Các loại trình phân tích cú pháp
Có hai loại trình phân tích cú pháp: trình phân tích cú pháp từ trên xuống và trình phân tích cú pháp từ dưới lên. Một cách giải thích trực quan là trình phân tích cú pháp từ trên xuống kiểm tra cấu trúc cấp cao của cú pháp và cố gắng tìm một quy tắc phù hợp. Trình phân tích cú pháp từ dưới lên bắt đầu bằng dữ liệu đầu vào và dần dần chuyển đổi dữ liệu đó thành các quy tắc cú pháp, bắt đầu từ các quy tắc cấp thấp cho đến khi đáp ứng các quy tắc cấp cao.
Hãy xem cách hai loại trình phân tích cú pháp sẽ phân tích cú pháp ví dụ của chúng ta.
Trình phân tích cú pháp từ trên xuống sẽ bắt đầu từ quy tắc cấp cao hơn: trình phân tích cú pháp này sẽ xác định 2 + 3 là một biểu thức. Sau đó, trình phân tích cú pháp sẽ xác định 2 + 3 - 1 là một biểu thức (quá trình xác định biểu thức sẽ phát triển, khớp với các quy tắc khác, nhưng điểm bắt đầu là quy tắc cấp cao nhất).
Trình phân tích cú pháp từ dưới lên sẽ quét dữ liệu đầu vào cho đến khi khớp với một quy tắc. Sau đó, hệ thống sẽ thay thế dữ liệu đầu vào khớp bằng quy tắc. Quá trình này sẽ tiếp tục cho đến khi kết thúc dữ liệu đầu vào. Biểu thức khớp một phần được đặt trên ngăn xếp của trình phân tích cú pháp.
Loại trình phân tích cú pháp từ dưới lên này được gọi là trình phân tích cú pháp dịch chuyển-giảm, vì dữ liệu đầu vào được dịch chuyển sang phải (hãy tưởng tượng một con trỏ trỏ trước vào điểm bắt đầu của dữ liệu đầu vào và di chuyển sang phải) và dần dần được giảm xuống thành các quy tắc cú pháp.
Tự động tạo trình phân tích cú pháp
Có những công cụ có thể tạo trình phân tích cú pháp. Bạn cung cấp cho chúng ngữ pháp của ngôn ngữ của bạn – từ vựng và quy tắc cú pháp – và chúng tạo ra một trình phân tích cú pháp hoạt động. Việc tạo trình phân tích cú pháp đòi hỏi bạn phải hiểu rõ về quá trình phân tích cú pháp và không dễ để tạo trình phân tích cú pháp tối ưu theo cách thủ công. Do đó, trình tạo trình phân tích cú pháp có thể rất hữu ích.
WebKit sử dụng hai trình tạo trình phân tích cú pháp nổi tiếng: Flex để tạo trình phân tích từ vựng và Bison để tạo trình phân tích cú pháp (bạn có thể gặp chúng với tên Lex và Yacc). Đầu vào Flex là một tệp chứa các định nghĩa biểu thức chính quy của mã thông báo. Đầu vào của Bison là các quy tắc cú pháp ngôn ngữ ở định dạng BNF.
Trình phân tích cú pháp HTML
Công việc của trình phân tích cú pháp HTML là phân tích cú pháp mã đánh dấu HTML thành cây phân tích cú pháp.
Ngữ pháp HTML
Từ vựng và cú pháp của HTML được xác định trong các thông số kỹ thuật do tổ chức W3C tạo ra.
Như chúng ta đã thấy trong phần giới thiệu về phân tích cú pháp, cú pháp ngữ pháp có thể được định nghĩa chính thức bằng các định dạng như BNF.
Rất tiếc, tất cả các chủ đề về trình phân tích cú pháp thông thường đều không áp dụng cho HTML (tôi không chỉ đề cập đến chúng cho vui mà chúng sẽ được dùng trong việc phân tích cú pháp CSS và JavaScript). HTML không thể dễ dàng được xác định bằng ngữ pháp không phụ thuộc ngữ cảnh mà trình phân tích cú pháp cần.
Có một định dạng chính thức để xác định HTML – DTD (Định nghĩa loại tài liệu) – nhưng đây không phải là ngữ pháp phi ngữ cảnh.
Thoạt nhìn, điều này có vẻ lạ; HTML khá gần với XML. Có rất nhiều trình phân tích cú pháp XML. Có một biến thể XML của HTML – XHTML – vậy sự khác biệt lớn là gì?
Điểm khác biệt là phương pháp HTML "dễ dãi" hơn: cho phép bạn bỏ qua một số thẻ nhất định (sau đó được thêm ngầm định) hoặc đôi khi bỏ qua thẻ mở hoặc thẻ đóng, v.v. Nhìn chung, đây là một cú pháp "mềm", trái ngược với cú pháp cứng nhắc và đòi hỏi khắt khe của XML.
Chi tiết có vẻ nhỏ này tạo ra sự khác biệt rất lớn. Một mặt, đây là lý do chính khiến HTML trở nên phổ biến: HTML bỏ qua lỗi của bạn và giúp tác giả web dễ dàng thực hiện. Mặt khác, điều này khiến việc viết ngữ pháp chính thức trở nên khó khăn. Tóm lại, các trình phân tích cú pháp thông thường không thể dễ dàng phân tích cú pháp HTML vì ngữ pháp của HTML không phải là ngữ pháp phi ngữ cảnh. Trình phân tích cú pháp XML không thể phân tích cú pháp HTML.
HTML DTD
Định nghĩa HTML ở định dạng DTD. Định dạng này được dùng để xác định ngôn ngữ của nhóm SGML. Định dạng này chứa định nghĩa cho tất cả các phần tử được phép, thuộc tính và hệ phân cấp của chúng. Như chúng ta đã thấy, DTD HTML không tạo thành ngữ pháp phi ngữ cảnh.
Có một vài biến thể của DTD. Chế độ nghiêm ngặt chỉ tuân thủ các thông số kỹ thuật, nhưng các chế độ khác hỗ trợ mã đánh dấu mà các trình duyệt đã sử dụng trước đây. Mục đích là để tương thích ngược với nội dung cũ. Bạn có thể xem DTD nghiêm ngặt hiện tại tại đây: www.w3.org/TR/html4/strict.dtd
DOM
Cây đầu ra ("cây phân tích cú pháp") là một cây gồm các nút phần tử và thuộc tính DOM. DOM là viết tắt của Document Object Model (Mô hình đối tượng tài liệu). Đây là bản trình bày đối tượng của tài liệu HTML và giao diện của các phần tử HTML với thế giới bên ngoài như JavaScript.
Gốc của cây là đối tượng "Tài liệu".
DOM có mối quan hệ gần như một với một với mã đánh dấu. Ví dụ:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
Mã đánh dấu này sẽ được dịch sang cây DOM sau:
Giống như HTML, DOM do tổ chức W3C chỉ định. Xem www.w3.org/DOM/DOMTR. Đây là quy cách chung để thao tác với tài liệu. Một mô-đun cụ thể mô tả các phần tử cụ thể của HTML. Bạn có thể tìm thấy các định nghĩa HTML tại đây: www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html.
Khi tôi nói cây chứa các nút DOM, tức là cây được tạo từ các phần tử triển khai một trong các giao diện DOM. Trình duyệt sử dụng các phương thức triển khai cụ thể có các thuộc tính khác được trình duyệt sử dụng nội bộ.
Thuật toán phân tích cú pháp
Như chúng ta đã thấy trong các phần trước, không thể phân tích cú pháp HTML bằng trình phân tích cú pháp từ trên xuống hoặc từ dưới lên thông thường.
Lý do là:
- Tính chất dễ dãi của ngôn ngữ.
- Thực tế là các trình duyệt có khả năng chịu lỗi truyền thống để hỗ trợ các trường hợp HTML không hợp lệ đã biết.
- Quá trình phân tích cú pháp có thể tái truy cập. Đối với các ngôn ngữ khác, nguồn không thay đổi trong quá trình phân tích cú pháp, nhưng trong HTML, mã động (chẳng hạn như các phần tử tập lệnh chứa lệnh gọi
document.write()) có thể thêm mã thông báo bổ sung, vì vậy, quá trình phân tích cú pháp thực sự sửa đổi dữ liệu đầu vào.
Do không thể sử dụng các kỹ thuật phân tích cú pháp thông thường, trình duyệt sẽ tạo trình phân tích cú pháp tuỳ chỉnh để phân tích cú pháp HTML.
Thuật toán phân tích cú pháp được mô tả chi tiết trong quy cách HTML5. Thuật toán này bao gồm hai giai đoạn: mã hoá và xây dựng cây.
Mã hoá là quá trình phân tích từ vựng, phân tích cú pháp dữ liệu đầu vào thành các mã thông báo. Trong số các mã thông báo HTML có thẻ bắt đầu, thẻ kết thúc, tên thuộc tính và giá trị thuộc tính.
Trình mã hoá nhận dạng mã thông báo, đưa mã thông báo đó cho hàm khởi tạo cây và sử dụng ký tự tiếp theo để nhận dạng mã thông báo tiếp theo, v.v. cho đến khi kết thúc dữ liệu đầu vào.
Thuật toán mã hoá
Kết quả của thuật toán là một mã thông báo HTML. Thuật toán được biểu thị dưới dạng máy trạng thái. Mỗi trạng thái sử dụng một hoặc nhiều ký tự của luồng đầu vào và cập nhật trạng thái tiếp theo theo các ký tự đó. Quyết định này chịu ảnh hưởng của trạng thái mã hoá hiện tại và trạng thái xây dựng cây. Điều này có nghĩa là cùng một ký tự được sử dụng sẽ mang lại kết quả khác nhau cho trạng thái tiếp theo chính xác, tuỳ thuộc vào trạng thái hiện tại. Thuật toán này quá phức tạp để có thể mô tả đầy đủ, vì vậy, hãy xem một ví dụ đơn giản để giúp chúng ta hiểu được nguyên tắc.
Ví dụ cơ bản – mã hoá HTML sau:
<html>
<body>
Hello world
</body>
</html>
Trạng thái ban đầu là "Trạng thái dữ liệu".
Khi gặp ký tự <, trạng thái sẽ thay đổi thành "Trạng thái mở thẻ".
Việc sử dụng ký tự a-z sẽ tạo ra "Mã thông báo thẻ bắt đầu", trạng thái được thay đổi thành "Trạng thái tên thẻ".
Chúng ta sẽ duy trì trạng thái này cho đến khi ký tự > được sử dụng. Mỗi ký tự được thêm vào tên mã thông báo mới. Trong trường hợp của chúng ta, mã thông báo được tạo là mã thông báo html.
Khi đạt đến thẻ >, mã thông báo hiện tại sẽ được phát ra và trạng thái sẽ thay đổi trở lại thành "Trạng thái dữ liệu".
Thẻ <body> sẽ được xử lý theo các bước tương tự.
Cho đến nay, các thẻ html và body đã được phát ra. Bây giờ, chúng ta quay lại "Trạng thái dữ liệu".
Việc sử dụng ký tự H của Hello world sẽ tạo và phát ra mã thông báo ký tự, quá trình này tiếp tục cho đến khi đạt đến < của </body>. Chúng tôi sẽ phát hành mã thông báo ký tự cho mỗi ký tự của Hello world.
Bây giờ, chúng ta sẽ quay lại "Trạng thái mở thẻ".
Việc sử dụng dữ liệu đầu vào tiếp theo / sẽ tạo ra một end tag token và chuyển sang "Trạng thái tên thẻ". Một lần nữa, chúng ta vẫn ở trạng thái này cho đến khi đạt đến >.Sau đó, mã thông báo thẻ mới sẽ được phát ra và chúng ta quay lại "Trạng thái dữ liệu".
Dữ liệu đầu vào </html> sẽ được xử lý như trường hợp trước.
Thuật toán xây dựng cây
Khi trình phân tích cú pháp được tạo, đối tượng Document cũng được tạo. Trong giai đoạn xây dựng cây, cây DOM có Tài liệu trong gốc sẽ được sửa đổi và các phần tử sẽ được thêm vào đó. Mỗi nút do trình tạo mã thông báo phát ra sẽ được trình tạo cây xử lý. Đối với mỗi mã thông báo, thông số kỹ thuật sẽ xác định phần tử DOM nào có liên quan đến mã thông báo đó và sẽ được tạo cho mã thông báo này. Phần tử này được thêm vào cây DOM và cả ngăn xếp các phần tử đang mở. Ngăn xếp này được dùng để sửa các thẻ không khớp lồng nhau và thẻ chưa đóng. Thuật toán này cũng được mô tả là một máy trạng thái. Các trạng thái này được gọi là "chế độ chèn".
Hãy xem quy trình xây dựng cây cho dữ liệu đầu vào mẫu:
<html>
<body>
Hello world
</body>
</html>
Đầu vào của giai đoạn xây dựng cây là một chuỗi mã thông báo từ giai đoạn mã hoá. Chế độ đầu tiên là "chế độ ban đầu". Việc nhận mã thông báo "html" sẽ khiến trình phân tích cú pháp chuyển sang chế độ "trước html" và xử lý lại mã thông báo ở chế độ đó. Thao tác này sẽ tạo phần tử HTMLHtmlElement và phần tử này sẽ được thêm vào đối tượng Document gốc.
Trạng thái sẽ được thay đổi thành "trước đầu". Sau đó, mã thông báo "body" sẽ được nhận. HTMLHeadElement sẽ được tạo ngầm mặc dù chúng ta không có mã thông báo "head" và mã này sẽ được thêm vào cây.
Bây giờ, chúng ta chuyển sang chế độ "trong phần đầu" rồi chuyển sang chế độ "sau phần đầu". Mã thông báo phần nội dung được xử lý lại, một HTMLBodyElement được tạo và chèn, đồng thời chế độ được chuyển sang "trong phần nội dung".
Giờ đây, các mã thông báo ký tự của chuỗi "Hello world" đã được nhận. Ký tự đầu tiên sẽ tạo và chèn một nút "Văn bản" và các ký tự khác sẽ được thêm vào nút đó.
Việc nhận mã thông báo kết thúc phần nội dung sẽ khiến quá trình chuyển sang chế độ "sau phần nội dung". Bây giờ, chúng ta sẽ nhận được thẻ kết thúc html, thẻ này sẽ chuyển chúng ta sang chế độ "after after body". Khi nhận được mã thông báo kết thúc tệp, quá trình phân tích cú pháp sẽ kết thúc.
Thao tác khi quá trình phân tích cú pháp hoàn tất
Ở giai đoạn này, trình duyệt sẽ đánh dấu tài liệu là tương tác và bắt đầu phân tích cú pháp các tập lệnh ở chế độ "trì hoãn": những tập lệnh cần được thực thi sau khi tài liệu được phân tích cú pháp. Sau đó, trạng thái tài liệu sẽ được đặt thành "hoàn tất" và sự kiện "tải" sẽ được kích hoạt.
Bạn có thể xem thuật toán đầy đủ để mã hoá và xây dựng cây trong thông số kỹ thuật HTML5.
Mức độ chấp nhận lỗi của trình duyệt
Bạn không bao giờ gặp lỗi "Cú pháp không hợp lệ" trên trang HTML. Trình duyệt sẽ sửa mọi nội dung không hợp lệ rồi tiếp tục.
Hãy lấy ví dụ về HTML sau:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
Tôi chắc hẳn đã vi phạm khoảng một triệu quy tắc ("mytag" không phải là thẻ chuẩn, lồng sai các phần tử "p" và "div" và nhiều quy tắc khác) nhưng trình duyệt vẫn hiển thị chính xác và không phàn nàn. Vì vậy, rất nhiều mã trình phân tích cú pháp đang sửa lỗi của tác giả HTML.
Xử lý lỗi khá nhất quán trong các trình duyệt, nhưng đáng ngạc nhiên là nó không nằm trong thông số kỹ thuật HTML. Giống như tính năng đánh dấu trang và các nút quay lại/tiến, đây chỉ là một tính năng được phát triển trong trình duyệt qua nhiều năm. Có một số cấu trúc HTML không hợp lệ đã biết lặp lại trên nhiều trang web và các trình duyệt cố gắng khắc phục chúng theo cách phù hợp với các trình duyệt khác.
Quy cách HTML5 xác định một số yêu cầu này. (WebKit tóm tắt điều này một cách khéo léo trong phần nhận xét ở đầu lớp trình phân tích cú pháp HTML.)
Trình phân tích cú pháp phân tích cú pháp dữ liệu đầu vào được mã hoá thành tài liệu, tạo nên cây tài liệu. Nếu tài liệu được định dạng đúng, thì việc phân tích cú pháp sẽ rất đơn giản.
Rất tiếc, chúng tôi phải xử lý nhiều tài liệu HTML không đúng định dạng, vì vậy trình phân tích cú pháp phải chấp nhận lỗi.
Chúng ta phải xử lý ít nhất các điều kiện lỗi sau:
- Phần tử đang được thêm bị cấm rõ ràng bên trong một số thẻ bên ngoài. Trong trường hợp này, chúng ta nên đóng tất cả các thẻ cho đến thẻ cấm phần tử và thêm phần tử đó sau.
- Chúng tôi không được phép thêm trực tiếp phần tử này. Có thể là người viết tài liệu đã quên một số thẻ ở giữa (hoặc thẻ ở giữa là không bắt buộc). Điều này có thể xảy ra với các thẻ sau: HTML HEAD BODY TBODY TR TD LI (tôi có quên thẻ nào không?).
- Chúng ta muốn thêm một phần tử khối vào bên trong một phần tử cùng dòng. Đóng tất cả các phần tử cùng dòng cho đến phần tử khối cao hơn tiếp theo.
- Nếu cách này không hiệu quả, hãy đóng các phần tử cho đến khi chúng tôi được phép thêm phần tử hoặc bỏ qua thẻ.
Hãy xem một số ví dụ về khả năng chịu lỗi của WebKit:
</br> thay vì <br>
Một số trang web sử dụng </br> thay vì <br>. Để tương thích với IE và Firefox, WebKit coi thuộc tính này như <br>.
Mã:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
Xin lưu ý rằng việc xử lý lỗi là nội bộ: lỗi sẽ không hiển thị cho người dùng.
Bảng không có tiêu đề
Bảng thừa là bảng nằm trong một bảng khác, nhưng không nằm trong ô của bảng.
Ví dụ:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
WebKit sẽ thay đổi hệ phân cấp thành hai bảng đồng cấp:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
Mã:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit sử dụng ngăn xếp cho nội dung phần tử hiện tại: nó sẽ bật bảng bên trong ra khỏi ngăn xếp bảng bên ngoài. Giờ đây, các bảng sẽ là thành phần đồng cấp.
Phần tử biểu mẫu lồng nhau
Trong trường hợp người dùng đặt một biểu mẫu bên trong một biểu mẫu khác, biểu mẫu thứ hai sẽ bị bỏ qua.
Mã:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
Hệ thống phân cấp thẻ quá sâu
Bình luận này đã nói lên tất cả.
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;
}
Thẻ kết thúc html hoặc body bị đặt sai vị trí
Một lần nữa, bình luận này đã nói lên tất cả.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Vì vậy, các tác giả web hãy lưu ý – trừ phi bạn muốn xuất hiện dưới dạng ví dụ trong đoạn mã dung sai lỗi WebKit – hãy viết HTML đúng định dạng.
Phân tích cú pháp CSS
Bạn có nhớ các khái niệm phân tích cú pháp trong phần giới thiệu không? Không giống như HTML, CSS là một ngữ pháp không phụ thuộc ngữ cảnh và có thể được phân tích cú pháp bằng các loại trình phân tích cú pháp được mô tả trong phần giới thiệu. Trên thực tế, quy cách CSS xác định ngữ pháp từ vựng và cú pháp CSS.
Hãy xem một số ví dụ:
Ngữ pháp từ vựng (từ vựng) được xác định bằng biểu thức chính quy cho mỗi mã thông báo:
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" là viết tắt của giá trị nhận dạng, chẳng hạn như tên lớp. "name" là mã nhận dạng phần tử (được tham chiếu bằng "#")
Ngữ pháp cú pháp được mô tả trong 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*] ')' ]
;
Giải thích:
Một bộ quy tắc có cấu trúc như sau:
div.error, a.error {
color:red;
font-weight:bold;
}
div.error và a.error là bộ chọn. Phần bên trong dấu ngoặc nhọn chứa các quy tắc được áp dụng bởi bộ quy tắc này.
Cấu trúc này được xác định chính thức trong định nghĩa sau:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
Điều này có nghĩa là một bộ quy tắc là một bộ chọn hoặc có thể là một số bộ chọn được phân tách bằng dấu phẩy và dấu cách (S là viết tắt của dấu cách). Một bộ quy tắc chứa dấu ngoặc nhọn và bên trong là một nội dung khai báo hoặc một số nội dung khai báo (không bắt buộc) được phân tách bằng dấu chấm phẩy. "declaration" và "selector" sẽ được xác định trong các định nghĩa BNF sau đây.
Trình phân tích cú pháp CSS WebKit
WebKit sử dụng trình tạo trình phân tích cú pháp Flex và Bison để tự động tạo trình phân tích cú pháp từ các tệp ngữ pháp CSS. Như bạn đã biết trong phần giới thiệu về trình phân tích cú pháp, Bison tạo ra một trình phân tích cú pháp dịch chuyển-giảm từ dưới lên. Firefox sử dụng trình phân tích cú pháp từ trên xuống được viết thủ công. Trong cả hai trường hợp, mỗi tệp CSS đều được phân tích cú pháp thành một đối tượng StyleSheet. Mỗi đối tượng chứa các quy tắc CSS. Các đối tượng quy tắc CSS chứa các đối tượng bộ chọn và khai báo cũng như các đối tượng khác tương ứng với ngữ pháp CSS.
Thứ tự xử lý cho tập lệnh và biểu định kiểu
Tập lệnh
Mô hình của web là đồng bộ. Tác giả mong muốn tập lệnh được phân tích cú pháp và thực thi ngay khi trình phân tích cú pháp đến thẻ <script>.
Quá trình phân tích cú pháp tài liệu sẽ tạm dừng cho đến khi tập lệnh được thực thi.
Nếu tập lệnh là bên ngoài, thì trước tiên, tài nguyên phải được tìm nạp từ mạng – việc này cũng được thực hiện đồng bộ và quá trình phân tích cú pháp sẽ tạm dừng cho đến khi tài nguyên được tìm nạp.
Đây là mô hình được sử dụng trong nhiều năm và cũng được chỉ định trong thông số kỹ thuật của HTML4 và 5.
Tác giả có thể thêm thuộc tính "defer" vào một tập lệnh. Trong trường hợp này, tập lệnh sẽ không dừng quá trình phân tích cú pháp tài liệu và sẽ thực thi sau khi tài liệu được phân tích cú pháp. HTML5 thêm một tuỳ chọn để đánh dấu tập lệnh là không đồng bộ, nhờ đó, tập lệnh sẽ được phân tích cú pháp và thực thi bằng một luồng khác.
Phân tích cú pháp suy đoán
Cả WebKit và Firefox đều thực hiện việc tối ưu hoá này. Trong khi thực thi tập lệnh, một luồng khác sẽ phân tích cú pháp phần còn lại của tài liệu và tìm ra những tài nguyên khác cần được tải từ mạng và tải chúng. Bằng cách này, tài nguyên có thể được tải trên các kết nối song song và tốc độ tổng thể được cải thiện. Lưu ý: Trình phân tích cú pháp suy đoán chỉ phân tích cú pháp các tệp tham chiếu đến tài nguyên bên ngoài như tập lệnh, biểu định kiểu và hình ảnh bên ngoài: trình phân tích cú pháp này không sửa đổi cây DOM – việc này được giao cho trình phân tích cú pháp chính.
Biểu định kiểu
Mặt khác, biểu định kiểu có một mô hình khác. Về mặt lý thuyết, có vẻ như vì biểu định kiểu không thay đổi cây DOM, nên không có lý do gì để đợi biểu định kiểu và dừng phân tích cú pháp tài liệu. Tuy nhiên, có một vấn đề là các tập lệnh yêu cầu thông tin về kiểu trong giai đoạn phân tích cú pháp tài liệu. Nếu kiểu chưa được tải và phân tích cú pháp, thì tập lệnh sẽ nhận được câu trả lời sai và rõ ràng là điều này gây ra nhiều vấn đề. Đây có vẻ là một trường hợp hiếm gặp nhưng thực ra lại khá phổ biến. Firefox chặn tất cả tập lệnh khi có một biểu định kiểu vẫn đang được tải và phân tích cú pháp. WebKit chỉ chặn tập lệnh khi tập lệnh cố gắng truy cập vào một số thuộc tính kiểu nhất định có thể bị ảnh hưởng bởi các biểu định kiểu chưa tải.
Xây dựng cây hiển thị
Trong khi cây DOM đang được tạo, trình duyệt sẽ tạo một cây khác, cây kết xuất. Cây này bao gồm các phần tử hình ảnh theo thứ tự hiển thị. Đây là hình ảnh đại diện của tài liệu. Mục đích của cây này là cho phép vẽ nội dung theo đúng thứ tự.
Firefox gọi các phần tử trong cây kết xuất là "khung". WebKit sử dụng thuật ngữ trình kết xuất hoặc đối tượng kết xuất.
Trình kết xuất biết cách bố trí và vẽ chính nó cũng như các phần tử con của nó.
Lớp RenderObject của WebKit, lớp cơ sở của trình kết xuất, có định nghĩa như sau:
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
}
Mỗi trình kết xuất đại diện cho một vùng hình chữ nhật thường tương ứng với hộp CSS của một nút, như được mô tả trong thông số kỹ thuật CSS2. Nó bao gồm thông tin hình học như chiều rộng, chiều cao và vị trí.
Loại hộp chịu ảnh hưởng của giá trị "display" của thuộc tính kiểu có liên quan đến nút (xem phần tính toán kiểu). Dưới đây là mã WebKit để quyết định loại trình kết xuất nào sẽ được tạo cho một nút DOM, theo thuộc tính hiển thị:
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;
}
Loại phần tử cũng được xem xét: ví dụ: các thành phần điều khiển biểu mẫu và bảng có khung đặc biệt.
Trong WebKit, nếu một phần tử muốn tạo một trình kết xuất đặc biệt, thì phần tử đó sẽ ghi đè phương thức createRenderer().
Trình kết xuất trỏ đến các đối tượng kiểu chứa thông tin không phải hình học.
Mối quan hệ giữa cây kết xuất và cây DOM
Trình kết xuất tương ứng với các phần tử DOM, nhưng mối quan hệ không phải là một với một. Các phần tử DOM không trực quan sẽ không được chèn vào cây kết xuất. Ví dụ: phần tử "head". Ngoài ra, các phần tử có giá trị hiển thị được chỉ định là "none" sẽ không xuất hiện trong cây (trong khi các phần tử có chế độ hiển thị "hidden" sẽ xuất hiện trong cây).
Có các phần tử DOM tương ứng với một số đối tượng hình ảnh. Đây thường là các phần tử có cấu trúc phức tạp, không thể mô tả bằng một hình chữ nhật. Ví dụ: phần tử "select" có ba trình kết xuất: một cho vùng hiển thị, một cho hộp danh sách thả xuống và một cho nút. Ngoài ra, khi văn bản được chia thành nhiều dòng vì chiều rộng không đủ cho một dòng, các dòng mới sẽ được thêm vào dưới dạng trình kết xuất bổ sung.
Một ví dụ khác về nhiều trình kết xuất là HTML bị hỏng. Theo thông số kỹ thuật CSS, một phần tử cùng dòng chỉ được chứa các phần tử khối hoặc chỉ chứa các phần tử cùng dòng. Trong trường hợp nội dung hỗn hợp, trình kết xuất khối ẩn danh sẽ được tạo để gói các phần tử cùng dòng.
Một số đối tượng kết xuất tương ứng với một nút DOM nhưng không nằm cùng một vị trí trong cây. Các phần tử nổi và có vị trí tuyệt đối nằm ngoài luồng, được đặt ở một phần khác của cây và được liên kết với khung thực. Khung giữ chỗ là nơi lẽ ra phải có khung hình.
Quy trình xây dựng cây
Trong Firefox, bản trình bày được đăng ký làm trình nghe cho các bản cập nhật DOM.
Bản trình bày uỷ quyền việc tạo khung cho FrameConstructor và hàm khởi tạo phân giải kiểu (xem phần tính toán kiểu) và tạo khung.
Trong WebKit, quá trình phân giải kiểu và tạo trình kết xuất được gọi là "tệp đính kèm". Mỗi nút DOM đều có một phương thức "attach". Đính kèm là đồng bộ, việc chèn nút vào cây DOM sẽ gọi phương thức "đính kèm" nút mới.
Việc xử lý các thẻ html và body sẽ tạo ra gốc của cây kết xuất.
Đối tượng kết xuất gốc tương ứng với đối tượng mà thông số kỹ thuật CSS gọi là khối chứa: khối trên cùng chứa tất cả các khối khác. Kích thước của vùng chứa là khung nhìn: kích thước vùng hiển thị của cửa sổ trình duyệt.
Firefox gọi là ViewPortFrame và WebKit gọi là RenderView.
Đây là đối tượng kết xuất mà tài liệu trỏ đến.
Phần còn lại của cây được tạo dưới dạng chèn nút DOM.
Xem thông số kỹ thuật CSS2 về mô hình xử lý.
Tính toán kiểu
Để tạo cây kết xuất, bạn cần tính toán các thuộc tính hình ảnh của từng đối tượng kết xuất. Việc này được thực hiện bằng cách tính toán các thuộc tính kiểu của từng phần tử.
Kiểu bao gồm các biểu định kiểu có nhiều nguồn gốc, các phần tử kiểu cùng dòng và các thuộc tính hình ảnh trong HTML (như thuộc tính "bgcolor").Thuộc tính sau được dịch sang các thuộc tính kiểu CSS phù hợp.
Nguồn gốc của biểu định kiểu là biểu định kiểu mặc định của trình duyệt, biểu định kiểu do tác giả trang cung cấp và biểu định kiểu của người dùng – đây là biểu định kiểu do người dùng trình duyệt cung cấp (trình duyệt cho phép bạn xác định kiểu yêu thích của mình. Ví dụ: trong Firefox, bạn có thể thực hiện việc này bằng cách đặt một biểu định kiểu trong thư mục "Hồ sơ Firefox").
Việc tính toán kiểu gây ra một số khó khăn:
- Dữ liệu kiểu là một cấu trúc rất lớn, chứa nhiều thuộc tính kiểu, điều này có thể gây ra vấn đề về bộ nhớ.
Việc tìm quy tắc phù hợp cho từng phần tử có thể gây ra vấn đề về hiệu suất nếu không được tối ưu hoá. Việc duyệt qua toàn bộ danh sách quy tắc cho từng phần tử để tìm kết quả trùng khớp là một nhiệm vụ nặng nề. Bộ chọn có thể có cấu trúc phức tạp khiến quá trình so khớp bắt đầu theo một đường dẫn có vẻ hứa hẹn nhưng lại không hiệu quả và phải thử một đường dẫn khác.
Ví dụ: bộ chọn phức hợp này:
div div div div{ ... }Có nghĩa là các quy tắc áp dụng cho một
<div>là phần tử con của 3 div. Giả sử bạn muốn kiểm tra xem quy tắc có áp dụng cho một phần tử<div>nhất định hay không. Bạn chọn một đường dẫn nhất định lên cây để kiểm tra. Bạn có thể cần phải duyệt qua cây nút chỉ để tìm hiểu xem chỉ có hai div và quy tắc không áp dụng. Sau đó, bạn cần thử các đường dẫn khác trong cây.Việc áp dụng các quy tắc này liên quan đến các quy tắc xếp tầng khá phức tạp, xác định hệ phân cấp của các quy tắc.
Hãy xem cách các trình duyệt đối mặt với những vấn đề này:
Chia sẻ dữ liệu kiểu
Các nút WebKit tham chiếu đến các đối tượng kiểu (RenderStyle). Các đối tượng này có thể được các nút chia sẻ trong một số điều kiện. Các nút là nút đồng cấp hoặc nút cùng cấp và:
- Các phần tử phải ở cùng một trạng thái chuột (ví dụ: một phần tử không thể ở trạng thái :hover trong khi phần tử kia thì không)
- Cả hai phần tử đều không được có mã nhận dạng
- Tên thẻ phải khớp
- Các thuộc tính của lớp phải khớp với nhau
- Tập hợp các thuộc tính được liên kết phải giống hệt nhau
- Trạng thái liên kết phải khớp
- Trạng thái tâm điểm phải khớp
- Cả hai phần tử đều không được chịu ảnh hưởng của bộ chọn thuộc tính, trong đó chịu ảnh hưởng được định nghĩa là có bất kỳ kết quả khớp nào với bộ chọn sử dụng bộ chọn thuộc tính ở bất kỳ vị trí nào trong bộ chọn
- Không được có thuộc tính kiểu cùng dòng trên các phần tử
- Không được sử dụng bộ chọn đồng cấp. WebCore chỉ cần tạo một nút chuyển chung khi gặp bất kỳ bộ chọn đồng cấp nào và tắt tính năng chia sẻ kiểu cho toàn bộ tài liệu khi có các bộ chọn đó. Điều này bao gồm bộ chọn + và các bộ chọn như :first-child và :last-child.
Cây quy tắc của Firefox
Firefox có thêm hai cây để tính toán kiểu dễ dàng hơn: cây quy tắc và cây ngữ cảnh kiểu. WebKit cũng có các đối tượng kiểu nhưng chúng không được lưu trữ trong cây như cây ngữ cảnh kiểu, chỉ có nút DOM trỏ đến kiểu liên quan của nó.
Bối cảnh kiểu chứa các giá trị kết thúc. Các giá trị được tính bằng cách áp dụng tất cả quy tắc phù hợp theo đúng thứ tự và thực hiện các thao tác biến đổi các giá trị đó từ giá trị logic thành giá trị cụ thể. Ví dụ: nếu giá trị logic là tỷ lệ phần trăm của màn hình, thì giá trị đó sẽ được tính toán và chuyển đổi thành đơn vị tuyệt đối. Ý tưởng về cây quy tắc thực sự rất thông minh. Thuộc tính này cho phép chia sẻ các giá trị này giữa các nút để tránh phải tính toán lại. Điều này cũng giúp tiết kiệm không gian.
Tất cả quy tắc trùng khớp đều được lưu trữ trong một cây. Các nút dưới cùng trong một nhánh có mức độ ưu tiên cao hơn. Cây này chứa tất cả đường dẫn cho các kết quả khớp quy tắc được tìm thấy. Việc lưu trữ các quy tắc được thực hiện từng phần. Cây không được tính toán ngay từ đầu cho mọi nút, nhưng bất cứ khi nào cần tính toán kiểu nút, các đường dẫn được tính toán sẽ được thêm vào cây.
Ý tưởng là xem các đường dẫn cây dưới dạng từ trong từ vựng. Giả sử chúng ta đã tính toán cây quy tắc này:
Giả sử chúng ta cần so khớp các quy tắc cho một phần tử khác trong cây nội dung và tìm ra các quy tắc phù hợp (theo đúng thứ tự) là B-E-I. Chúng ta đã có đường dẫn này trong cây vì chúng ta đã tính toán đường dẫn A-B-E-I-L. Giờ đây, chúng ta sẽ có ít việc phải làm hơn.
Hãy xem cách cây giúp chúng ta tiết kiệm công sức.
Phân chia thành các cấu trúc
Bối cảnh kiểu được chia thành các cấu trúc. Các cấu trúc đó chứa thông tin về kiểu cho một danh mục nhất định như đường viền hoặc màu sắc. Tất cả thuộc tính trong một cấu trúc đều được kế thừa hoặc không được kế thừa. Thuộc tính kế thừa là những thuộc tính được kế thừa từ phần tử mẹ, trừ phi được phần tử đó xác định. Các thuộc tính không kế thừa (gọi là thuộc tính "đặt lại") sử dụng giá trị mặc định nếu không được xác định.
Cây này giúp chúng ta bằng cách lưu toàn bộ cấu trúc (chứa các giá trị cuối được tính toán) vào bộ nhớ đệm trong cây. Ý tưởng là nếu nút dưới cùng không cung cấp định nghĩa cho một cấu trúc, thì cấu trúc được lưu vào bộ nhớ đệm trong nút trên có thể được sử dụng.
Tính toán bối cảnh kiểu bằng cây quy tắc
Khi tính toán bối cảnh kiểu cho một phần tử nhất định, trước tiên, chúng ta sẽ tính toán một đường dẫn trong cây quy tắc hoặc sử dụng một đường dẫn hiện có. Sau đó, chúng ta bắt đầu áp dụng các quy tắc trong đường dẫn để điền vào các cấu trúc trong bối cảnh kiểu mới. Chúng ta bắt đầu từ nút dưới cùng của đường dẫn – nút có mức độ ưu tiên cao nhất (thường là bộ chọn cụ thể nhất) và duyệt qua cây cho đến khi cấu trúc của chúng ta đầy. Nếu không có thông số kỹ thuật cho cấu trúc trong nút quy tắc đó, thì chúng ta có thể tối ưu hoá rất nhiều – chúng ta đi lên cây cho đến khi tìm thấy một nút chỉ định đầy đủ và trỏ đến nút đó – đó là cách tối ưu hoá tốt nhất – toàn bộ cấu trúc được chia sẻ. Việc này giúp tiết kiệm bộ nhớ và thời gian tính toán giá trị cuối.
Nếu tìm thấy định nghĩa một phần, chúng ta sẽ đi lên cây cho đến khi cấu trúc được lấp đầy.
Nếu không tìm thấy định nghĩa nào cho cấu trúc, thì trong trường hợp cấu trúc là một loại "kế thừa", chúng ta sẽ trỏ đến cấu trúc của thành phần mẹ trong cây ngữ cảnh. Trong trường hợp này, chúng ta cũng đã chia sẻ thành công các cấu trúc. Nếu đó là một cấu trúc đặt lại, thì các giá trị mặc định sẽ được sử dụng.
Nếu nút cụ thể nhất có thêm giá trị, thì chúng ta cần thực hiện thêm một số phép tính để chuyển đổi nút đó thành giá trị thực tế. Sau đó, chúng ta lưu kết quả vào bộ nhớ đệm trong nút cây để các thành phần con có thể sử dụng.
Trong trường hợp một phần tử có phần tử đồng cấp hoặc phần tử đồng cấp trỏ đến cùng một nút cây, thì toàn bộ bối cảnh kiểu có thể được chia sẻ giữa các phần tử đó.
Hãy xem ví dụ sau: Giả sử chúng ta có HTML này
<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>
Và các quy tắc sau:
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
Để đơn giản hoá, giả sử chúng ta chỉ cần điền vào hai cấu trúc: cấu trúc màu và cấu trúc lề. Cấu trúc màu chỉ chứa một thành phần: màu Cấu trúc lề chứa bốn cạnh.
Cây quy tắc thu được sẽ có dạng như sau (các nút được đánh dấu bằng tên nút: số quy tắc mà nút đó trỏ đến):
Cây ngữ cảnh sẽ có dạng như sau (tên nút: nút quy tắc mà chúng trỏ đến):
Giả sử chúng ta phân tích cú pháp HTML và chuyển đến thẻ <div> thứ hai. Chúng ta cần tạo một bối cảnh kiểu cho nút này và điền vào các cấu trúc kiểu của nút.
Chúng tôi sẽ so khớp các quy tắc và phát hiện ra rằng các quy tắc khớp cho <div> là 1, 2 và 6.
Điều này có nghĩa là đã có một đường dẫn hiện có trong cây mà phần tử của chúng ta có thể sử dụng và chúng ta chỉ cần thêm một nút khác vào đó cho quy tắc 6 (nút F trong cây quy tắc).
Chúng ta sẽ tạo một bối cảnh kiểu và đưa bối cảnh đó vào cây bối cảnh. Bối cảnh kiểu mới sẽ trỏ đến nút F trong cây quy tắc.
Bây giờ, chúng ta cần điền vào các cấu trúc kiểu. Chúng ta sẽ bắt đầu bằng cách điền vào cấu trúc lề. Vì nút quy tắc cuối cùng (F) không thêm vào cấu trúc lề, nên chúng ta có thể đi lên cây cho đến khi tìm thấy một cấu trúc được lưu vào bộ nhớ đệm được tính toán trong một lần chèn nút trước đó và sử dụng cấu trúc đó. Chúng ta sẽ tìm thấy quy tắc này trên nút B, là nút cao nhất đã chỉ định quy tắc lề.
Chúng ta có định nghĩa cho cấu trúc màu, vì vậy chúng ta không thể sử dụng cấu trúc được lưu vào bộ nhớ đệm. Vì màu sắc có một thuộc tính nên chúng ta không cần phải đi lên cây để điền các thuộc tính khác. Chúng tôi sẽ tính toán giá trị cuối (chuyển đổi chuỗi thành RGB, v.v.) và lưu cấu trúc đã tính toán vào bộ nhớ đệm trên nút này.
Việc xử lý phần tử <span> thứ hai thậm chí còn dễ dàng hơn. Chúng ta sẽ so khớp các quy tắc và đưa ra kết luận rằng quy tắc này trỏ đến quy tắc G, giống như span trước đó.
Vì có các phần tử đồng cấp trỏ đến cùng một nút, nên chúng ta có thể chia sẻ toàn bộ bối cảnh kiểu và chỉ trỏ đến bối cảnh của span trước đó.
Đối với các cấu trúc chứa quy tắc được kế thừa từ phần tử mẹ, việc lưu vào bộ nhớ đệm được thực hiện trên cây ngữ cảnh (thuộc tính màu thực sự được kế thừa, nhưng Firefox coi thuộc tính này là đặt lại và lưu vào bộ nhớ đệm trên cây quy tắc).
Ví dụ: nếu chúng ta thêm quy tắc cho phông chữ trong một đoạn văn:
p {font-family: Verdana; font size: 10px; font-weight: bold}
Sau đó, phần tử đoạn văn (là phần tử con của div trong cây ngữ cảnh) có thể dùng chung cấu trúc phông chữ với phần tử mẹ. Điều này xảy ra nếu không có quy tắc phông chữ nào được chỉ định cho đoạn văn.
Trong WebKit, không có cây quy tắc, các khai báo khớp được duyệt qua bốn lần. Trước tiên, các thuộc tính có mức độ ưu tiên cao không quan trọng sẽ được áp dụng (các thuộc tính cần được áp dụng trước vì các thuộc tính khác phụ thuộc vào chúng, chẳng hạn như hiển thị), sau đó là các quy tắc quan trọng có mức độ ưu tiên cao, sau đó là các quy tắc không quan trọng có mức độ ưu tiên bình thường, sau đó là các quy tắc quan trọng có mức độ ưu tiên bình thường. Điều này có nghĩa là các thuộc tính xuất hiện nhiều lần sẽ được phân giải theo đúng thứ tự xếp tầng. Lượt ghi sau cùng sẽ được chọn.
Tóm lại: việc chia sẻ các đối tượng kiểu (toàn bộ hoặc một số cấu trúc bên trong) sẽ giải quyết được vấn đề 1 và 3. Cây quy tắc của Firefox cũng giúp áp dụng các thuộc tính theo đúng thứ tự.
Thao túng luật chơi để dễ dàng thắng trận
Có một số nguồn cho quy tắc định kiểu:
- Quy tắc CSS, trong biểu định kiểu bên ngoài hoặc trong các phần tử kiểu.
css p {color: blue} - Các thuộc tính kiểu cùng dòng như
html <p style="color: blue" /> - Thuộc tính hình ảnh HTML (được liên kết với các quy tắc kiểu liên quan)
html <p bgcolor="blue" />Hai thuộc tính cuối cùng dễ dàng khớp với phần tử vì phần tử này sở hữu các thuộc tính kiểu và thuộc tính HTML có thể được liên kết bằng cách sử dụng phần tử làm khoá.
Như đã lưu ý trước đó trong vấn đề số 2, việc so khớp quy tắc CSS có thể phức tạp hơn. Để giải quyết khó khăn này, các quy tắc được điều chỉnh để dễ tiếp cận hơn.
Sau khi phân tích cú pháp biểu định kiểu, các quy tắc sẽ được thêm vào một trong số các bản đồ băm, theo bộ chọn. Có các bản đồ theo mã nhận dạng, theo tên lớp, theo tên thẻ và một bản đồ chung cho mọi thứ không thuộc các danh mục đó. Nếu bộ chọn là một mã nhận dạng, thì quy tắc sẽ được thêm vào bản đồ mã nhận dạng, nếu là một lớp thì quy tắc sẽ được thêm vào bản đồ lớp, v.v.
Thao tác này giúp việc so khớp quy tắc trở nên dễ dàng hơn nhiều. Bạn không cần phải xem xét mọi nội dung khai báo: chúng ta có thể trích xuất các quy tắc liên quan cho một phần tử từ bản đồ. Quá trình tối ưu hoá này loại bỏ hơn 95% quy tắc, nhờ đó, các quy tắc này thậm chí không cần được xem xét trong quá trình so khớp(4.1).
Hãy xem ví dụ về các quy tắc định kiểu sau:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
Quy tắc đầu tiên sẽ được chèn vào bản đồ lớp. Thẻ thứ hai vào sơ đồ nhận dạng và thẻ thứ ba vào sơ đồ thẻ.
Đối với đoạn mã HTML sau;
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
Trước tiên, chúng ta sẽ cố gắng tìm các quy tắc cho phần tử p. Bản đồ lớp sẽ chứa khoá "error" (lỗi) mà theo đó, quy tắc cho "p.error" được tìm thấy. Phần tử div sẽ có các quy tắc liên quan trong bản đồ mã nhận dạng (khoá là mã nhận dạng) và bản đồ thẻ. Vì vậy, việc duy nhất còn lại là tìm xem quy tắc nào trong số các quy tắc được trích xuất bằng khoá thực sự khớp.
Ví dụ: nếu quy tắc cho div là:
table div {margin: 5px}
Nó vẫn sẽ được trích xuất từ bản đồ thẻ, vì khoá là bộ chọn ở ngoài cùng bên phải, nhưng nó sẽ không khớp với phần tử div của chúng ta, phần tử không có phần tử gốc của bảng.
Cả WebKit và Firefox đều thực hiện thao tác này.
Thứ tự xếp tầng của biểu định kiểu
Đối tượng kiểu có các thuộc tính tương ứng với mọi thuộc tính hình ảnh (tất cả thuộc tính CSS nhưng chung chung hơn). Nếu thuộc tính không được xác định theo bất kỳ quy tắc nào phù hợp, thì một số thuộc tính có thể được kế thừa theo đối tượng kiểu phần tử mẹ. Các thuộc tính khác có giá trị mặc định.
Vấn đề bắt đầu khi có nhiều định nghĩa – đây là thứ tự xếp tầng để giải quyết vấn đề.
Nội dung khai báo cho một thuộc tính kiểu có thể xuất hiện trong nhiều biểu định kiểu và nhiều lần trong một biểu định kiểu. Điều này có nghĩa là thứ tự áp dụng các quy tắc là rất quan trọng. Đây được gọi là thứ tự "xếp tầng". Theo thông số kỹ thuật CSS2, thứ tự xếp tầng là (từ thấp đến cao):
- Thông tin khai báo về trình duyệt
- Khai báo thông thường của người dùng
- Khai báo thông thường của tác giả
- Tác giả của các tuyên bố quan trọng
- Bản khai báo quan trọng của người dùng
Khai báo trình duyệt là ít quan trọng nhất và người dùng chỉ ghi đè tác giả nếu khai báo được đánh dấu là quan trọng. Các nội dung khai báo có cùng thứ tự sẽ được sắp xếp theo mức độ cụ thể, sau đó là thứ tự được chỉ định. Các thuộc tính hình ảnh HTML được dịch sang các khai báo CSS phù hợp . Các quy tắc này được coi là quy tắc của tác giả có mức độ ưu tiên thấp.
Mức độ cụ thể
Tính cụ thể của bộ chọn được xác định theo quy cách CSS2 như sau:
- đếm 1 nếu nội dung khai báo là thuộc tính "style" thay vì quy tắc có bộ chọn, nếu không thì đếm 0 (= a)
- đếm số lượng thuộc tính mã nhận dạng trong bộ chọn (= b)
- đếm số lượng thuộc tính và lớp giả khác trong bộ chọn (= c)
- đếm số lượng tên phần tử và phần tử giả trong bộ chọn (= d)
Việc nối bốn số a-b-c-d (trong một hệ thống số có cơ số lớn) sẽ cho ra độ cụ thể.
Cơ số bạn cần sử dụng được xác định bằng số lượng cao nhất mà bạn có trong một trong các danh mục.
Ví dụ: nếu a=14, bạn có thể sử dụng hệ thập lục phân. Trong trường hợp hiếm hoi khi a=17, bạn sẽ cần một cơ số có 17 chữ số. Tình huống sau có thể xảy ra với một bộ chọn như sau: html body div div p… (17 thẻ trong bộ chọn của bạn… không có khả năng xảy ra).
Một số ví dụ:
* {} /* 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 */
Đang sắp xếp các quy tắc
Sau khi được so khớp, các quy tắc sẽ được sắp xếp theo quy tắc xếp tầng.
WebKit sử dụng thuật toán sắp xếp nổi bọt cho danh sách nhỏ và thuật toán sắp xếp trộn cho danh sách lớn.
WebKit triển khai việc sắp xếp bằng cách ghi đè toán tử > cho các quy tắc:
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;
}
Quy trình từng bước
WebKit sử dụng một cờ đánh dấu xem tất cả biểu định kiểu cấp cao nhất (bao gồm cả @imports) đã được tải hay chưa. Nếu kiểu không được tải đầy đủ khi đính kèm, thì phần giữ chỗ sẽ được sử dụng và được đánh dấu trong tài liệu, đồng thời chúng sẽ được tính toán lại sau khi tải biểu định kiểu.
Bố cục
Khi được tạo và thêm vào cây, trình kết xuất không có vị trí và kích thước. Việc tính toán các giá trị này được gọi là bố cục hoặc chỉnh lại dòng.
HTML sử dụng mô hình bố cục dựa trên luồng, nghĩa là hầu hết thời gian, bạn có thể tính toán hình học trong một lượt. Các phần tử sau này "trong luồng" thường không ảnh hưởng đến hình học của các phần tử trước đó "trong luồng", vì vậy, bố cục có thể tiến hành từ trái sang phải, từ trên xuống dưới trong tài liệu. Có một số trường hợp ngoại lệ: ví dụ: bảng HTML có thể yêu cầu nhiều lượt truyền.
Hệ toạ độ tương ứng với khung gốc. Toạ độ trên cùng và bên trái được sử dụng.
Bố cục là một quy trình đệ quy. Quá trình này bắt đầu tại trình kết xuất gốc, tương ứng với phần tử <html> của tài liệu HTML. Bố cục tiếp tục đệ quy thông qua một số hoặc tất cả hệ phân cấp khung, tính toán thông tin hình học cho từng trình kết xuất yêu cầu thông tin đó.
Vị trí của trình kết xuất gốc là 0,0 và kích thước của trình kết xuất này là khung nhìn – phần hiển thị của cửa sổ trình duyệt.
Tất cả trình kết xuất đều có phương thức "bố cục" hoặc "phân lại dòng", mỗi trình kết xuất gọi phương thức bố cục của các phần tử con cần bố cục.
Hệ thống bit bẩn
Để không phải thực hiện bố cục đầy đủ cho mọi thay đổi nhỏ, trình duyệt sử dụng hệ thống "bit bẩn". Trình kết xuất được thay đổi hoặc thêm vào sẽ đánh dấu chính nó và các phần tử con của nó là "dirty" (bẩn): cần bố cục.
Có hai cờ: "dirty" (bị bẩn) và "children are dirty" (phần tử con bị bẩn), nghĩa là mặc dù bản thân trình kết xuất có thể hoạt động bình thường, nhưng trình kết xuất này có ít nhất một phần tử con cần bố cục.
Bố cục chung và bố cục gia tăng
Bố cục có thể được kích hoạt trên toàn bộ cây kết xuất – đây là bố cục "chung". Điều này có thể xảy ra do:
- Thay đổi kiểu chung ảnh hưởng đến tất cả trình kết xuất, chẳng hạn như thay đổi cỡ chữ.
- Do màn hình được đổi kích thước
Bố cục có thể gia tăng, chỉ những trình kết xuất có sửa đổi mới được bố trí (điều này có thể gây ra một số hư hỏng sẽ yêu cầu thêm bố cục).
Bố cục gia tăng được kích hoạt (không đồng bộ) khi trình kết xuất bị bẩn. Ví dụ: khi trình kết xuất mới được thêm vào cây kết xuất sau khi nội dung bổ sung đến từ mạng và được thêm vào cây DOM.
Bố cục không đồng bộ và đồng bộ
Bố cục gia tăng được thực hiện không đồng bộ. Firefox đưa "lệnh chỉnh lại dòng" vào hàng đợi cho bố cục gia tăng và một trình lập lịch biểu sẽ kích hoạt việc thực thi hàng loạt các lệnh này. WebKit cũng có một bộ hẹn giờ thực thi bố cục gia tăng – cây được duyệt qua và các trình kết xuất "bẩn" được bố trí.
Các tập lệnh yêu cầu thông tin về kiểu, chẳng hạn như "offsetHeight" có thể kích hoạt bố cục gia tăng một cách đồng bộ.
Bố cục chung thường sẽ được kích hoạt đồng bộ.
Đôi khi, bố cục được kích hoạt dưới dạng lệnh gọi lại sau bố cục ban đầu vì một số thuộc tính (chẳng hạn như vị trí cuộn) đã thay đổi.
Tối ưu hoá
Khi một bố cục được kích hoạt bằng thao tác "đổi kích thước" hoặc thay đổi vị trí trình kết xuất(chứ không phải kích thước), kích thước trình kết xuất sẽ được lấy từ bộ nhớ đệm và không được tính toán lại…
Trong một số trường hợp, chỉ có một cây con được sửa đổi và bố cục không bắt đầu từ gốc. Điều này có thể xảy ra trong trường hợp thay đổi là cục bộ và không ảnh hưởng đến môi trường xung quanh – chẳng hạn như văn bản được chèn vào trường văn bản (nếu không, mỗi lần nhấn phím sẽ kích hoạt bố cục bắt đầu từ gốc).
Quy trình bố cục
Bố cục thường có mẫu sau:
- Trình kết xuất gốc xác định chiều rộng của chính nó.
- Cha mẹ xem xét các con và:
- Đặt trình kết xuất con (đặt x và y).
- Gọi bố cục con nếu cần (bố cục con bị bẩn hoặc chúng ta đang ở trong bố cục chung hoặc vì một số lý do khác) để tính chiều cao của bố cục con.
- Phần tử mẹ sử dụng chiều cao tích luỹ của phần tử con và chiều cao của lề cũng như khoảng đệm để đặt chiều cao của chính nó – chiều cao này sẽ được trình kết xuất mẹ của phần tử mẹ sử dụng.
- Đặt bit bẩn thành false.
Firefox sử dụng một đối tượng "trạng thái" (nsHTMLReflowState) làm tham số cho bố cục (được gọi là "reflow"). Trong số những trạng thái khác, trạng thái này bao gồm chiều rộng của thành phần mẹ.
Kết quả của bố cục Firefox là một đối tượng "metrics" (nsHTMLReflowMetrics). Thuộc tính này sẽ chứa chiều cao được tính toán của trình kết xuất.
Cách tính chiều rộng
Chiều rộng của trình kết xuất được tính bằng chiều rộng của khối vùng chứa, thuộc tính "width" (chiều rộng) của kiểu trình kết xuất, lề và đường viền.
Ví dụ: chiều rộng của div sau:
<div style="width: 30%"/>
Sẽ được WebKit tính như sau(phương thức calcWidth của lớp RenderBox):
- Chiều rộng vùng chứa là giá trị tối đa của availableWidth của vùng chứa và 0. Trong trường hợp này, availableWidth là contentWidth được tính như sau:
clientWidth() - paddingLeft() - paddingRight()
clientWidth và clientHeight đại diện cho phần bên trong của một đối tượng, không bao gồm đường viền và thanh cuộn.
Chiều rộng của phần tử là thuộc tính kiểu "width". Giá trị này sẽ được tính dưới dạng giá trị tuyệt đối bằng cách tính tỷ lệ phần trăm của chiều rộng vùng chứa.
Đường viền và khoảng đệm ngang hiện đã được thêm vào.
Cho đến nay, đây là cách tính "chiều rộng ưu tiên". Bây giờ, chiều rộng tối thiểu và tối đa sẽ được tính toán.
Nếu chiều rộng ưu tiên lớn hơn chiều rộng tối đa, thì chiều rộng tối đa sẽ được sử dụng. Nếu chiều rộng này nhỏ hơn chiều rộng tối thiểu (đơn vị nhỏ nhất không thể ngắt dòng) thì chiều rộng tối thiểu sẽ được sử dụng.
Các giá trị được lưu vào bộ nhớ đệm trong trường hợp cần có bố cục, nhưng chiều rộng không thay đổi.
Ngắt dòng
Khi một trình kết xuất ở giữa bố cục quyết định rằng cần phải ngắt, trình kết xuất đó sẽ dừng và thông báo cho phần tử mẹ của bố cục rằng cần phải ngắt. Phần tử mẹ tạo các trình kết xuất bổ sung và gọi bố cục trên các trình kết xuất đó.
Tranh vẽ
Trong giai đoạn vẽ, cây kết xuất được duyệt qua và phương thức "paint()" của trình kết xuất được gọi để hiển thị nội dung trên màn hình. Thao tác vẽ sử dụng thành phần cơ sở hạ tầng giao diện người dùng.
Toàn cầu và gia tăng
Giống như bố cục, việc vẽ cũng có thể áp dụng cho toàn bộ cây hoặc từng phần. Trong quá trình vẽ gia tăng, một số trình kết xuất thay đổi theo cách không ảnh hưởng đến toàn bộ cây. Trình kết xuất đã thay đổi sẽ vô hiệu hoá hình chữ nhật của nó trên màn hình. Điều này khiến hệ điều hành coi đó là "vùng bẩn" và tạo ra sự kiện "vẽ". Hệ điều hành thực hiện việc này một cách thông minh và kết hợp nhiều vùng thành một. Trong Chrome, việc này phức tạp hơn vì trình kết xuất nằm trong một quy trình khác với quy trình chính. Chrome mô phỏng hành vi của hệ điều hành ở một mức độ nhất định. Bản trình bày sẽ lắng nghe các sự kiện này và uỷ quyền thông báo cho gốc kết xuất. Cây được duyệt qua cho đến khi đạt được trình kết xuất có liên quan. Nó sẽ tự vẽ lại (và thường là cả các thành phần con của nó).
Thứ tự vẽ
CSS2 xác định thứ tự của quy trình vẽ. Đây thực ra là thứ tự xếp chồng các phần tử trong bối cảnh xếp chồng. Thứ tự này ảnh hưởng đến việc vẽ vì các ngăn xếp được vẽ từ sau ra trước. Thứ tự xếp chồng của trình kết xuất khối là:
- màu nền
- hình nền
- border
- trẻ em
- outline
Danh sách hiển thị của Firefox
Firefox xem xét cây kết xuất và tạo danh sách hiển thị cho hình chữ nhật được vẽ. Tệp này chứa các trình kết xuất phù hợp với hình chữ nhật, theo đúng thứ tự vẽ (nền của trình kết xuất, sau đó là đường viền, v.v.).
Bằng cách đó, cây chỉ cần được duyệt qua một lần để vẽ lại thay vì nhiều lần – vẽ tất cả nền, sau đó là tất cả hình ảnh, rồi đến tất cả đường viền, v.v.
Firefox tối ưu hoá quy trình bằng cách không thêm các phần tử sẽ bị ẩn, chẳng hạn như các phần tử hoàn toàn nằm dưới các phần tử mờ khác.
Bộ nhớ hình chữ nhật WebKit
Trước khi vẽ lại, WebKit sẽ lưu hình chữ nhật cũ dưới dạng bitmap. Sau đó, nó chỉ vẽ phần chênh lệch giữa hình chữ nhật mới và hình chữ nhật cũ.
Thay đổi linh động
Trình duyệt cố gắng thực hiện các thao tác tối thiểu có thể để phản hồi một thay đổi. Vì vậy, những thay đổi đối với màu của một phần tử sẽ chỉ khiến phần tử đó được vẽ lại. Những thay đổi đối với vị trí phần tử sẽ khiến bố cục và việc vẽ lại phần tử, phần tử con và có thể là phần tử đồng cấp. Việc thêm nút DOM sẽ khiến nút đó được bố trí và vẽ lại. Những thay đổi lớn, chẳng hạn như tăng cỡ chữ của phần tử "html", sẽ khiến bộ nhớ đệm mất hiệu lực, bố cục lại và vẽ lại toàn bộ cây.
Các luồng của công cụ kết xuất
Công cụ kết xuất là một luồng đơn. Hầu hết mọi thứ, ngoại trừ các hoạt động mạng, đều diễn ra trong một luồng duy nhất. Trong Firefox và Safari, đây là luồng chính của trình duyệt. Trong Chrome, đó là luồng chính của quy trình thẻ.
Các thao tác mạng có thể được thực hiện bằng nhiều luồng song song. Số lượng kết nối song song bị giới hạn (thường là 2 – 6 kết nối).
Vòng lặp sự kiện
Luồng chính của trình duyệt là một vòng lặp sự kiện. Đây là một vòng lặp vô hạn giúp duy trì quá trình. Luồng này chờ các sự kiện (như sự kiện bố cục và vẽ) rồi xử lý các sự kiện đó. Đây là mã Firefox cho vòng lặp sự kiện chính:
while (!mExiting)
NS_ProcessNextEvent(thread);
Mô hình trực quan CSS2
Canvas
Theo quy cách CSS2, thuật ngữ canvas mô tả "không gian nơi cấu trúc định dạng được kết xuất": nơi trình duyệt vẽ nội dung.
Khung vẽ là vô hạn cho mỗi chiều của không gian, nhưng trình duyệt sẽ chọn chiều rộng ban đầu dựa trên kích thước của khung nhìn.
Theo www.w3.org/TR/CSS2/zindex.html, canvas sẽ trong suốt nếu nằm trong một canvas khác và có màu do trình duyệt xác định nếu không nằm trong canvas khác.
Mô hình hộp CSS
Mô hình hộp CSS mô tả các hộp hình chữ nhật được tạo cho các phần tử trong cây tài liệu và được bố trí theo mô hình định dạng hình ảnh.
Mỗi hộp có một vùng nội dung (ví dụ: văn bản, hình ảnh, v.v.) và các vùng đệm, đường viền và lề xung quanh (không bắt buộc).
Mỗi nút tạo ra 0…n hộp như vậy.
Tất cả phần tử đều có thuộc tính "display" (hiển thị) xác định loại hộp sẽ được tạo.
Ví dụ:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
Giá trị mặc định là cùng dòng nhưng biểu định kiểu của trình duyệt có thể đặt các giá trị mặc định khác. Ví dụ: màn hình mặc định cho phần tử "div" là khối.
Bạn có thể tìm thấy ví dụ về biểu định kiểu mặc định tại đây: www.w3.org/TR/CSS2/sample.html.
Lược đồ định vị
Có 3 cơ chế:
- Bình thường: đối tượng được đặt theo vị trí của đối tượng đó trong tài liệu. Điều này có nghĩa là vị trí của nó trong cây kết xuất giống như vị trí của nó trong cây DOM và được bố trí theo loại hộp và kích thước của nó
- Float: đối tượng được bố trí trước tiên như luồng thông thường, sau đó di chuyển sang trái hoặc phải xa nhất có thể
- Tuyệt đối: đối tượng được đặt trong cây kết xuất ở một vị trí khác với vị trí trong cây DOM
Lược đồ định vị được đặt bằng thuộc tính "position" và thuộc tính "float".
- static và relative tạo ra một luồng thông thường
- absolute và fixed gây ra vị trí tuyệt đối
Trong định vị tĩnh, không có vị trí nào được xác định và định vị mặc định được sử dụng. Trong các lược đồ khác, tác giả chỉ định vị trí: trên cùng, dưới cùng, bên trái, bên phải.
Cách bố trí hộp được xác định bởi:
- Loại hộp
- Kích thước hộp
- Lược đồ định vị
- Thông tin bên ngoài, chẳng hạn như kích thước hình ảnh và kích thước màn hình
Loại hộp
Hộp khối: tạo thành một khối – có hình chữ nhật riêng trong cửa sổ trình duyệt.
Hộp cùng dòng: không có khối riêng mà nằm bên trong một khối chứa.
Các khối được định dạng theo chiều dọc, khối này nối tiếp khối kia. Quảng cáo cùng dòng được định dạng theo chiều ngang.
Hộp cùng dòng được đặt bên trong các dòng hoặc "hộp dòng". Các dòng có chiều cao ít nhất bằng hộp cao nhất nhưng có thể cao hơn, khi các hộp được căn chỉnh theo "đường cơ sở" – nghĩa là phần dưới cùng của một phần tử được căn chỉnh tại một điểm của hộp khác ngoài phần dưới cùng. Nếu chiều rộng vùng chứa không đủ, thì các phần tử cùng dòng sẽ được đặt trên nhiều dòng. Đây thường là những gì xảy ra trong một đoạn văn.
Khẳng định vị thế
Họ hàng
Định vị tương đối – được định vị như bình thường rồi di chuyển theo delta bắt buộc.
Nổi
Hộp nổi được dịch chuyển sang bên trái hoặc bên phải của một dòng. Tính năng thú vị là các hộp khác sẽ di chuyển xung quanh hộp đó. HTML:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
Sẽ có dạng như sau:
Tuyệt đối và cố định
Bố cục được xác định chính xác bất kể luồng thông thường. Phần tử này không tham gia vào luồng thông thường. Kích thước tương ứng với vùng chứa. Trong vị trí cố định, vùng chứa là khung nhìn.
Biểu diễn theo lớp
Điều này được chỉ định bằng thuộc tính CSS z-index. Nó đại diện cho chiều thứ ba của hộp: vị trí của hộp dọc theo "trục z".
Các hộp được chia thành các ngăn xếp (gọi là bối cảnh xếp chồng). Trong mỗi ngăn xếp, các phần tử phía sau sẽ được vẽ trước và các phần tử phía trước sẽ được vẽ ở trên cùng, gần người dùng hơn. Trong trường hợp chồng chéo, phần tử phía trước sẽ ẩn phần tử trước đó.
Các ngăn xếp được sắp xếp theo thuộc tính z-index. Các hộp có thuộc tính "z-index" tạo thành một ngăn xếp cục bộ. Khung nhìn có ngăn xếp bên ngoài.
Ví dụ:
<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>
Kết quả sẽ như sau:
Mặc dù div màu đỏ đứng trước div màu xanh lục trong mã đánh dấu và sẽ được vẽ trước trong luồng thông thường, nhưng thuộc tính z-index cao hơn, vì vậy, thuộc tính này nằm ở phía trước trong ngăn xếp do hộp gốc lưu giữ.
Tài nguyên
Kiến trúc trình duyệt
- Grosskurth, Alan. Kiến trúc tham chiếu cho trình duyệt web (pdf)
- Gupta, Vineet. Cách hoạt động của trình duyệt – Phần 1 – Cấu trúc
Phân tích cú pháp
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
- Rick Jelliffe. The Bold and the Beautiful: hai bản nháp mới cho HTML 5.
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers. (HTML và CSS nhanh hơn: Nội dung của công cụ bố cục dành cho nhà phát triển web)
- L. David Baron, HTML và CSS nhanh hơn: Nội dung của công cụ bố cục dành cho nhà phát triển web (video trò chuyện về công nghệ của Google)
- L. David Baron, Công cụ bố cục của Mozilla
- L. David Baron, Tài liệu về hệ thống kiểu của Mozilla
- Chris Waterson, Notes on HTML Reflow
- Tổng quan về Gecko, Chris Waterson
- Alexander Larsson, The life of an HTML HTTP request (Vòng đời của một yêu cầu HTTP HTML)
WebKit
- David Hyatt, Implementing CSS(part 1)
- David Hyatt, An Overview of WebCore (Tổng quan về WebCore)
- Kết xuất WebCore, David Hyatt
- David Hyatt, Vấn đề về FOUC
Thông số kỹ thuật của W3C
Hướng dẫn tạo trình duyệt
Bản dịch
Trang này đã được dịch sang tiếng Nhật hai lần:
- Cách trình duyệt web hoạt động - Hậu trường của các trình duyệt web hiện đại (ja) của @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 by @ikeike443 and @kiyoto01.
Bạn có thể xem bản dịch được lưu trữ bên ngoài bằng tiếng Hàn và tiếng Thổ Nhĩ Kỳ.
Cảm ơn tất cả các bạn!