Cách hoạt động của trình duyệt

Vén màn hậu trường của các trình duyệt web hiện đại

Tali Garsiel
Tali Garsiel

Lời nói đầu

Khoá học sơ lược toàn diện này về hoạt động nội bộ của WebKit và Gecko là kết quả của nhiều nghiên cứu của nhà phát triển Tali Garsiel người Israel. Trong vài năm, cô ấy xem xét tất cả dữ liệu được công bố về bên trong 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 hoạt động của trình duyệt giúp bạn đưa ra quyết định sáng suốt hơn và biết lý do đằng sau các phương pháp phát triển hay nhất. Mặc dù tài liệu này khá dài, nhưng bạn nên dành thời gian tìm hiểu. Bạn sẽ hài lòng với những gì bạn đã làm.

Paul Ireland, Nhóm 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 hướng dẫn cơ bản này, tôi sẽ giải thích cách hoạt động của các thiết bị này trong hậu trường. Chúng tôi sẽ xem điều gì sẽ 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.

Trình duyệt chúng tôi sẽ nói đến

Hiện nay, có năm trình duyệt chính được sử dụng trên máy tính để bàn: Chrome, Internet Explorer, Firefox, Safari và Opera. Trên điện thoại 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, các trình duyệt Nokia S40/S60 và Chrome, tất cả đều dựa trên WebKit. Tôi sẽ đưa ra ví dụ từ các trình duyệt nguồn mở Firefox, Chrome và 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 trên 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à hiển thị 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à một 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 (Uniform Resource Identifier – mã 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 thông số kỹ thuật HTML và CSS. Những thông số kỹ thuật này được tổ chức W3C (World Wide Web Consortium), một tổ chức tiêu chuẩn cho web duy trì. Trong nhiều năm, các trình duyệt chỉ tuân theo 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 vấn đề nghiêm trọng về khả năng tương thích cho các tác giả trang web. Ngày nay, hầu hết các trình duyệt đều tuân thủ ít nhiều thông số kỹ thuật.

Các giao diện người dùng của trình duyệt có rất nhiều điểm tương đồng với nhau. Sau đây là một số phần tử phổ biến trên giao diện người dùng:

  1. Thanh địa chỉ để chèn URI
  2. Nút tiến và lùi
  3. Tuỳ chọn đánh dấu trang
  4. 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
  5. Nút Trang chủ đưa bạn đến trang chủ

Điều kỳ lạ là giao diện người dùng của trình duyệt không được chỉ định trong bất kỳ đặc tả chính thức nào, nó chỉ đến từ các phương pháp hay được hình thành qua nhiều năm kinh nghiệm và bởi các trình duyệt bắt chước 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ố phần tử phổ biến. Bao gồm cả thanh địa chỉ, thanh trạng thái và thanh công cụ. Tất nhiên, có các tính năng chỉ dành cho một trình duyệt cụ thể 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à:

  1. Giao diện người dùng: bao gồm thanh địa chỉ, nút tiến/lùi, trình đơn đánh dấu trang, v.v. Mọi phần của trình duyệt đều hiển thị, ngoại trừ cửa sổ nơi bạn thấy trang được yêu cầu.
  2. 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.
  3. Công cụ kết xuất hình ảnh: chịu trách nhiệm hiển thị nội dung yêu cầu. Ví dụ: nếu nội dung được yêu cầu là HTML, công cụ kết xuất sẽ phân tích cú pháp HTML và CSS, đồng thời hiển thị nội dung đã phân tích cú pháp trên màn hình.
  4. Kết nối mạng: đối với các lệnh gọi mạng (chẳng hạn như yêu cầu HTTP), sử dụng nhiều cách triển khai cho nhiều nền tảng sau một giao diện độc lập với nền tảng.
  5. 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 cho thấy một giao diện chung không dành riêng cho nền tảng. Bên dưới lớp 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.
  6. Trình phiên dịch JavaScript. Được dùng để phân tích cú pháp và thực thi mã JavaScript.
  7. Bộ nhớ dữ liệu. Đây là tầng cố định. Trình duyệt có thể cần phải lưu tất cả các loại dữ liệu cục bộ, chẳng hạn như cookie. Các trình duyệt cũng hỗ trợ cơ chế lưu trữ như localStorage, IndexedDB, WebSQL và FileSystem.
Các thành phần của trình duyệt
Hình 1: Các thành phần của trình duyệt

Đ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ỗi phiên bản cho một 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 hình ảnh có thể hiển thị các tài liệu và hình ảnh HTML và XML. Trình duyệt này 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ị các 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ụ hiển thị 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à công cụ hiển thị nguồn mở bắt đầu như một công cụ dành cho nền tảng Linux và đã được Apple sửa đổi để hỗ trợ Mac và Windows.

Luồng chính

Công cụ kết xuất hình ảnh sẽ bắt đầu nhận nội dung của tài liệu được yêu cầu từ lớp mạng. Việc này thường được thực hiện trong các đoạn 8kB.

Sau đó, đây là quy trình cơ bản của công cụ kết xuất:

Quy trình cơ bản của công cụ hiển thị
Hình 2: Quy trình cơ bản của công cụ hiển thị

Công cụ hiển thị 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 cây được gọi 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. Việc định kiểu thông tin cùng với các 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 hiển thị chứa các hình chữ nhật có các thuộc tính trực quan như màu sắc và kích thước. Hình chữ nhật sẽ xuất hiện theo đúng thứ tự xuất hiện trên màn hình.

Sau khi tạo cây hiển thị, cây sẽ trải qua quá trình "bố cục". Tức là cung cấp cho mỗi nút toạ độ chính xác mà nó sẽ xuất hiện trên màn hình. Giai đoạn tiếp theo là sơ đồ – cây hiển thị sẽ được truyền tải và mỗi nút sẽ được vẽ bằng cách sử dụ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 từng bước. Để cải thiện trải nghiệm người dùng, công cụ kết xuất hình ảnh sẽ cố gắng cho thấy nội dung trên màn hình sớm nhất có thể. Sẽ không đợi cho đến khi tất cả HTML được phân tích cú pháp trước khi bắt đầu xây dựng và bố trí cây kết xuất. 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 tiếp tục với các nội dung còn lại từ mạng.

Ví dụ về quy trình chính

Luồng chính WebKit.
Hình 3: Luồng chính WebKit
Luồng chính của công cụ kết xuất đồ hoạ Gecko của Mozilla.
Hình 4: Luồng chính của công cụ hiển thị Gecko của Mozilla

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 luồng về cơ bản là giống nhau.

Gecko gọi cây phần tử được định dạng trực quan là "Cây khung". Mỗi phần tử là một khung. WebKit sử dụng thuật ngữ "Cây kết xuất" và 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 nó là "Reflow". "Đính kèm" là thuật ngữ của WebKit để kết nối các nút DOM và thông tin trực quan để tạo cây hiển thị. Một điểm khác biệt nhỏ về mặt ngữ nghĩa là Gecko có thêm một lớp giữa HTML và cây DOM. Đây được gọi là "bồn lưu trữ nội dung" và là nhà máy để tạo các phần tử DOM. Chúng ta sẽ thảo luận về từng phần của quy trình này:

Phân tích cú pháp – chung

Vì phân tích cú pháp là một quá trình rất quan trọng trong công cụ kết xuất, nên chúng ta sẽ đi sâu hơn một chút. Trước tiên, tôi sẽ giới thiệu đôi chút về quá trình phân tích cú pháp.

Phân tích cú pháp một tài liệu 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 việc 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 này:

Nút cây biểu thức toán học.
Hình 5: nút cây biểu thức toán học

Ngữ pháp

Việc phân tích cú pháp dựa trên các quy tắc về cú pháp mà tài liệu tuân theo: ngôn ngữ hoặc định dạng của văn bản. Mọi định dạng bạn có thể phân tích cú pháp phải có ngữ pháp tất định bao gồm các quy tắc về từ vựng và cú pháp. Đây được gọi là ngữ pháp tự do theo ngữ cảnh. Ngôn ngữ của con người không phải là ngôn ngữ như vậy và do đó không thể phân tích cú pháp bằng kỹ thuật phân tích cú pháp thông thường.

Kết hợp trình phân tích cú pháp – Lexer

Quá trình phân tích cú pháp có thể được tách 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 nhỏ dữ liệu đầu vào thành các mã thông báo. Mã thông báo là từ vựng cho ngôn ngữ: tập hợp các thành phần hợp lệ. Trong ngôn ngữ của con người, từ này sẽ bao gồm tất cả các từ xuất hiện trong từ điển cho 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 cho 2 thành phần: lexer (đôi khi còn gọi là trình tạo 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 quy tắc về cú pháp ngôn ngữ.

Kỹ thuật viên 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.

Từ tài liệu nguồn để phân tích cú pháp cây
Hình 6: từ tài liệu nguồn đến cây phân tích cú pháp

Quá trình phân tích cú pháp có tính lặp lại. Trình phân tích cú pháp thường sẽ yêu cầu trình 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 về cú pháp. Nếu quy tắc khớp, 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 phù hợp, trình phân tích cú pháp sẽ lưu trữ mã thông báo 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 đã lưu trữ nội bộ. Nếu không tìm thấy quy tắc nào thì 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à giấy tờ không hợp lệ và có lỗi cú pháp.

Dịch thuật

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 quá trình 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à tính năng 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ã đó thành một cây phân tích cú pháp, sau đó dịch cây này thành một tài liệu mã máy.

Quy trình biên dịch
Hình 7: quy trình biên dịch

Ví dụ về phân tích cú pháp

Trong hình 5, chúng ta đã tạo cây phân tích cú pháp từ một biểu thức toán học. Hãy cố gắng định nghĩa một ngôn ngữ toán học đơn giản và xem quá trình phân tích cú pháp.

Cú pháp:

  1. Các thành phần của cú pháp ngôn ngữ là biểu thức, thuật ngữ và phép tính.
  2. Ngôn ngữ của chúng tôi có thể bao gồm vô số cách diễn đạt.
  3. Một biểu thức được định nghĩa là một "thuật ngữ", theo sau là một "phép toán", theo sau là một thuật ngữ khác
  4. Toán tử là mã cộng hoặc mã trừ
  5. Cụm từ 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 đầu vào 2 + 3 - 1.

Chuỗi con đầu tiên phù hợp với quy tắc là 2: theo quy tắc #5, đó là một thuật ngữ. Kết quả phù hợp thứ hai là 2 + 3: khớp với quy tắc thứ ba: một cụm từ theo sau là một toán tử, theo sau là một cụm từ khác. Kết quả phù hợp tiếp theo sẽ chỉ được nhấn ở cuối nội dung nhập. 2 + 3 - 1 là một biểu thức vì chúng ta đã biết 2 + 3 là một thuật ngữ, vậy nên chúng ta có một thuật ngữ, theo sau là một toán tử, theo sau là một thuật ngữ khác. 2 + + sẽ không phù hợp với bất kỳ quy tắc nào nên là giá trị nhập 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 tôi sẽ được xác định là:

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

Như bạn thấy, số nguyên được xác định bằng một biểu thức chính quy.

Cú pháp thường được xác định ở định dạng BNF. Ngôn ngữ của chúng tôi sẽ được xác định là:

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 trình phân tích cú pháp thông thường phân tích cú pháp nếu ngữ pháp của ngôn ngữ đó là ngữ pháp không có ngữ cảnh. Định nghĩa trực quan của ngữ pháp không có ngữ cảnh là ngữ pháp có thể được diễn đạt 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 không theo 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 sẽ kiểm tra cấu trúc cấp cao của cú pháp và cố gắng tìm kết quả trùng khớp với quy tắc. 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à chuyển đổi dần thành quy tắc về cú pháp, bắt đầu từ 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 này 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: quy tắc này sẽ xác định 2 + 3 là một biểu thức. Sau đó, hàm này 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 nội dung nhập cho đến khi tìm được một quy tắc. Sau đó, thao tác này sẽ thay thế giá trị nhập vào bằng quy tắc. Quá trình này sẽ tiếp tục cho đến khi kết thúc nội dung nhập. 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.

Xếp chồng Đầu vào
2 + 3 - 1
term + 3 – 1
phép toán cụm từ 3 – 1
biểu thức – 1
toán tử biểu thức 1
biểu thức -

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 shift-rút vì đầu vào được dịch chuyển sang phải (hãy tưởng tượng một con trỏ trỏ đến đầu vào bắt đầu và di chuyển sang phải) và được giảm dần theo 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 họ ngữ pháp của ngôn ngữ của bạn - các quy tắc về từ vựng và cú pháp của ngôn ngữ đó - và chúng sẽ tạo 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 yêu cầu bạn phải hiểu rõ về phân tích cú pháp và cũng không dễ dàng để tạo một trình phân tích cú pháp được tối ưu hoá 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 phổ biến: Flex để tạo lexer và Bison để tạo trình phân tích cú pháp (bạn có thể gặp các trình tạo này có tên là Lex và Yacc). Dữ liệu đầu vào linh hoạt là một tệp chứa định nghĩa biểu thức chính quy của mã thông báo. Dữ liệu đầ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.

Như chúng ta đã thấy trong phần giới thiệu phân tích cú pháp, cú pháp ngữ pháp có thể được xác định chính thức bằng các định dạng như BNF.

Rất tiếc, tất cả các chủ đề của 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 đưa ra các chủ đề này chỉ để giải trí - chúng sẽ được sử dụng trong việc phân tích cú pháp CSS và JavaScript). Không thể dễ dàng xác định HTML bằng ngữ pháp không 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 để định nghĩa HTML - DTD (Document Type Definition) - nhưng đó không phải là ngữ pháp miễn phí theo ngữ cảnh.

Thoạt nhìn, điều này có vẻ lạ; HTML khá gần với XML. Hiện 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 – OpenGL – vậy có gì khác biệt lớn không?

Sự khác biệt là phương pháp HTML "tha thứ" hơn: nó cho phép bạn bỏ qua một số thẻ nhất định (sau đó được thêm vào ngầm ẩn) hoặc đôi khi bỏ qua thẻ mở hoặc thẻ đóng, v.v. Nhìn chung, đó là cú pháp "mềm", trái ngược với cú pháp cứng và đòi hỏi cao của XML.

Chi tiết tưởng chừng như rất nhỏ này nhưng lại tạo nên một thế giới khác biệt. Một mặt, đây là lý do chính khiến HTML trở nên thông dụng như vậy: nó xoá bỏ những sai lầm của bạn và giúp tác giả trang web dễ dàng sống. Mặt khác, nó gây khó khăn cho việc viết một ngữ pháp chính thức. Tóm lại, HTML không thể được phân tích cú pháp dễ dàng bằng trình phân tích cú pháp thông thường, vì ngữ pháp của nó không có ngữ cảnh. Trình phân tích cú pháp XML không thể phân tích cú pháp HTML.

DTD HTML

Định nghĩa HTML ở định dạng DTD. Định dạng này dùng để xác định các ngôn ngữ của nhóm SGML. Định dạng này chứa định nghĩa cho tất cả phần tử được phép, thuộc tính và hệ phân cấp của các phần tử đó. Như chúng ta đã thấy trước đó, HTML DTD không tạo ra ngữ pháp tự do theo ngữ cảnh.

Có một vài biến thể của DTD. Chế độ nghiêm ngặt chỉ tuân theo thông số kỹ thuật nhưng các chế độ khác có hỗ trợ đánh dấu mà các trình duyệt sử dụng trước đây. Mục đích của tính năng này là khả năng tương thích ngược với nội dung cũ. DTD nghiêm ngặt hiện tại có tại đây: www.w3.org/TR/html4/strict.dtd

Mô hình đối tượng tài liệu (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 thuộc tính và phần tử DOM. DOM là viết tắt của Mô hình đối tượng tài liệu. Đó 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 ra thế giới bên ngoài, chẳng hạn 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 chuyển đổi sang cây DOM sau:

Cây DOM của mã đánh dấu mẫu
Hình 8: Cây DOM của mã đánh dấu mẫu

Giống như HTML, DOM được tổ chức W3C chỉ định. Hãy truy cập vào www.w3.org/DOM/DOMTR. Đây là thông số kỹ thuật chung cho các thao tác thao tác với tài liệu. Mô-đun cụ thể mô tả các phần tử HTML cụ thể. Bạn có thể xem các định nghĩa về 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 nút DOM, ý tôi là cây được xây dựng 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 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à:

  1. Bản chất dễ tha thứ của ngôn từ.
  2. Thực tế là các trình duyệt có khả năng chống lỗi truyền thống để hỗ trợ các trường hợp HTML không hợp lệ đã biết.
  3. Quá trình phân tích cú pháp vẫn còn lại. Đố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ư 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 sẽ thực sự sửa đổi dữ liệu đầu vào.

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 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 theo quy cách HTML5. Thuật toán bao gồm hai giai đoạn: quá trình mã hoá và tạo cây.

Mã hoá mã hoá là việc phân tích từ vựng, phân tích cú pháp thông tin đầu vào thành mã thông báo. Mã thông báo HTML gồm có thẻ mở, thẻ kết thúc, tên thuộc tính và giá trị thuộc tính.

Trình tạo mã thông báo nhận ra mã thông báo, cung cấp mã đó 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, cứ như vậy cho đến khi kết thúc hoạt động đầu vào.

Quy trình phân tích cú pháp HTML (lấy từ thông số kỹ thuật HTML5)
Hình 9: Quy trình phân tích cú pháp HTML (lấy từ thông số kỹ thuật HTML5)

Thuật toán mã hoá

Dữ liệu đầu ra của thuật toán là một mã thông báo HTML. Thuật toán này được biểu thị dưới dạng một 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 các kết quả khác nhau cho đúng trạng thái tiếp theo, tuỳ thuộc vào trạng thái hiện tại. Thuật toán quá phức tạp nên không thể mô tả đầy đủ, vì vậy, hãy xem một ví dụ đơn giản giúp chúng ta hiểu nguyên tắc này.

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 sẽ thay đổi thành "Trạng thái tên thẻ". Chúng ta sẽ ở trạng thái này cho đến khi sử dụng hết ký tự >. Mỗi ký tự sẽ được nối thêm vào tên mã thông báo mới. Trong trường hợp này, 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 và trạng thái sẽ thay đổi về "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ẻ htmlbody đã được phát. Chúng ta hiện đã 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 mã thông báo ký tự. Quá trình này sẽ tiếp tục cho đến khi đạt đến < của </body>. Chúng tôi sẽ phát ra một mã thông báo ký tự cho mỗi ký tự của Hello world.

Giờ đây, chúng ta sẽ trở lại ở "Trạng thái mở thẻ". Việc sử dụng giá trị đầu vào / tiếp theo sẽ tạo ra 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 và chúng ta quay lại "Trạng thái dữ liệu". Giá trị nhập vào </html> sẽ được xử lý như trường hợp trước.

Mã hoá thông tin đầu vào mẫu
Hình 10: Mã hoá dữ liệu đầu vào mẫu

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 Tài liệu sẽ được tạo. Trong giai đoạn xây dựng cây, cây DOM với Tài liệu trong gốc sẽ được sửa đổi và các phần tử sẽ được thêm vào nó. Mỗi nút do trình tạo mã thông báo tạo ra sẽ được hàm khởi tạo dạng 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ã đó 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 như ngăn xếp các phần tử mở. Ngăn xếp này dùng để sửa các thẻ không khớp và thẻ chưa đóng. Thuật toán này cũng được mô tả như một máy trạng thái. Các trạng thái đượ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>

Dữ liệu đầu vào cho giai đoạn tạo 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 được mã thông báo "html" sẽ dẫn đến việc chuyển sang chế độ "trước html" và tái xử lý mã thông báo ở chế độ đó. Điều này sẽ tạo ra việc tạo phần tử HTMLHTMLElement, phần tử này sẽ được nối thêm vào đối tượng Tài liệu gốc.

Trạng thái sẽ được thay đổi thành "trước đầu". Sau đó, hệ thống sẽ nhận được mã thông báo "body". 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ế độ "inhead" và sau đó sang "after head". Mã thông báo nội dung được xử lý lại, một HTMLBodyElement được tạo và chèn vào, đồng thời chế độ được chuyển sang "in body".

Hiện đã nhận được mã thông báo ký tự của chuỗi "Hello world". Ký tự đầu tiên sẽ dẫn đến việc tạo và chèn nút "Văn bản", đồng thời 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 nội dung sẽ dẫn đến quá trình chuyển sang chế độ "after body". Bây giờ, chúng ta sẽ nhận được thẻ đóng html để chuyển chúng ta sang chế độ "sau nội dung". Khi nhận được kết thúc mã thông báo tệp, quá trình phân tích cú pháp sẽ kết thúc.

Xây dựng cây của HTML mẫu.
Hình 11: cấu trúc cây của html mẫu

Các hành động 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à có tính 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 các thuật toán đầy đủ để mã hoá và tạo cây trong tài liệu thông số kỹ thuật HTML5.

Khả năng 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. Các trình duyệt sẽ sửa mọi nội dung không hợp lệ và tiếp tục.

Hãy lấy HTML này làm ví dụ:

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

Tôi chắc hẳn đã vi phạm khoảng 1 triệu quy tắc ("mytag" không phải là thẻ chuẩn, việc lồng sai các phần tử "p" và "div" và các phần tử 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ã phân tích cú pháp đang sửa lỗi của tác giả HTML.

Việc xử lý lỗi tương đối nhất quán trong các trình duyệt, nhưng thật đáng ngạc nhiên là lỗi này chưa được đưa vào thông số kỹ thuật của HTML. Giống như việc đánh dấu trang và nút tiến/lùi, đây chỉ là thứ được phát triển trong các trình duyệt qua nhiều năm. Có những 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 sẽ cố gắng sửa chúng theo cách phù hợp với các trình duyệt khác.

Thông số HTML5 xác định một số yêu cầu trong số này. (WebKit tóm tắt nội dung này 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 thông tin nhập được mã hoá vào tài liệu, từ đó tạo dựng cây tài liệu. Nếu tài liệu có định dạng tốt, việc phân tích cú pháp phải rõ ràng.

Thật không may, chúng tôi phải xử lý nhiều tài liệu HTML không được định dạng tốt, do đó trình phân tích cú pháp phải chịu được lỗi.

Chúng ta phải chú ý đến ít nhất các điều kiện lỗi sau:

  1. Phần tử đang được thêm bị cấm một cách 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ẻ lên tới thẻ cấm phần tử đó rồi thêm vào sau đó.
  2. Chúng tôi không được phép thêm phần tử trực tiếp. Có thể 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 gì không?).
  3. Chúng ta muốn thêm một phần tử khối bên trong phần tử cùng dòng. Đóng tất cả các phần tử cùng dòng lên đến phần tử khối cao hơn tiếp theo.
  4. 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ấp nhận lỗi 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 sẽ xem 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ộ: người dùng sẽ không thấy.

Một chiếc bàn nhỏ

Bảng đi lạc là bảng nằm bên trong một bảng khác, nhưng không phải bên trong một ô 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ẽ đẩy bảng bên trong ra khỏi ngăn xếp bảng bên ngoài. Các bảng giờ đây sẽ là các bảng đồng cấp.

Phần tử biểu mẫu được lồng

Trong trường hợp người dùng đặ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ệ phân cấp thẻ quá sâu

Bình luận 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 phần nội dung bị đặt sai vị trí

Xin nhắc lại một lần nữa, bình luận này thể hiện điều đó.

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

Vì vậy, tác giả trang web hãy cảnh giác - trừ khi bạn muốn xuất hiện như một ví dụ trong đoạn mã dung sai lỗi WebKit - hãy viết HTML có định dạng tốt.

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? Chà, không giống như HTML, CSS là một ngữ pháp không có ngữ cảnh và có thể được phân tích cú pháp bằng cách sử dụng các loại trình phân tích cú pháp được mô tả trong phần giới thiệu. Trong thực tế, thông số kỹ thuật CSS định nghĩa 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 theo 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à id phần tử (được tham chiếu bởi "#" )

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:

Bộ quy tắc có cấu trúc như sau:

div.error, a.error {
  color:red;
  font-weight:bold;
}

div.errora.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 định nghĩa 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à bộ quy tắc là một bộ chọn hoặc một số bộ chọn (không bắt buộc) được phân tách bằng dấu phẩy và dấu cách (S là viết tắt của khoảng trắng). Một bộ quy tắc chứa các dấu ngoặc nhọn và bên trong đó là một phần 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. "khai báo" và "bộ chọn" sẽ được định nghĩa trong các định nghĩa BNF sau.

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ừ tệp ngữ pháp CSS. Như bạn còn nhớ trong phần giới thiệu về trình phân tích cú pháp, Bison sẽ tạo một trình phân tích cú pháp shift-rút 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 đều chứa các quy tắc CSS. Đối tượng quy tắc CSS chứa đối tượng bộ chọn và đối tượng khai báo cũng như các đối tượng khác tương ứng với ngữ pháp CSS.

Phân tích cú pháp CSS.
Hình 12: Phân tích cú pháp CSS

Đơn đặt hàng đang xử lý tập lệnh và biểu định kiểu

Tập lệnh

Mô hình web có tính đồng bộ. Tác giả mong muốn các 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 tiếp cậ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à phân tích cú pháp các lần tạm dừng cho đến khi tài nguyên được tìm nạp. Đây là mô hình trong nhiều năm và cũng được chỉ định trong thông số kỹ thuật HTML4 và 5. Tác giả có thể thêm thuộc tính "trì hoãn" vào tập lệnh. Trong trường hợp đó, thuộc tính này sẽ không tạm dừng phân tích cú pháp tài liệu và sẽ thực thi sau khi phân tích cú pháp tài liệu. HTML5 thêm tuỳ chọn đánh dấu tập lệnh là không đồng bộ để tập lệnh đó được phân tích cú pháp và thực thi bởi một luồng khác.

Phân tích cú pháp theo suy đoán

Cả WebKit và Firefox đều thực hiện 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 tải từ mạng cũng như tải các tài nguyên đó. 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 tham chiếu đến tài nguyên bên ngoài như tập lệnh bên ngoài, biểu định kiểu và hình ảnh: công cụ này không sửa đổi cây DOM - phần còn lại sẽ do trình phân tích cú pháp chính.

Biểu định kiểu

Trong khi đó, các biểu định kiểu có một mô hình khác. Về mặt lý thuyết, có vẻ như vì các biểu định kiểu không thay đổi cây DOM, không có lý do gì để đợi chúng và dừng phân tích cú pháp tài liệu. Tuy nhiên, có một vấn đề về tập lệnh yêu cầu thông tin 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à có vẻ như đ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 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 các tập lệnh khi các tập lệnh này cố truy cập vào các 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 kết xuất hình ảnh

Trong khi cây DOM đang được xây dựng, trình duyệt sẽ xây dựng một cây khác, cây hiển thị. Cây này bao gồm các phần tử thị giác theo thứ tự được hiển thị. Đây là bản trình bày giấy tờ bằng hình ảnh. Mục đích của cây này là cho phép tô màu nội dung theo đúng thứ tự.

Firefox gọi các phần tử trong cây kết xuất là "frames". 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 đồ hoạ biết cách bố trí và vẽ các thành phần con.

Lớp RenderObject của WebKit, là lớp cơ sở của trình kết xuất, có định nghĩa 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 khu vực hình chữ nhật, thường tương ứng với hộp CSS của nút, như được mô tả trong thông số kỹ thuật CSS2. Trình kết xuất này 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 bị ảnh hưởng bởi 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 nên được tạo cho nút DOM, theo thuộc tính display (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, chẳng hạn như các thành phần điều khiển biểu mẫu và bảng có các khung đặc biệt.

Trong WebKit, nếu một phần tử muốn tạo 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 tạo kiểu có chứa thông tin không phải hình học.

Mối quan hệ của cây hiển thị với 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 hiển thị. Một ví dụ là phần tử "head". Ngoài ra, các phần tử có chế độ hiển thị được gán là "none" sẽ không xuất hiện trong cây (trong khi các phần tử có chế độ hiển thị "ẩn" sẽ xuất hiện trong cây).

Có các phần tử DOM tương ứng với nhiều đối tượng trực quan. Đây thường là những 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 duy nhất. Ví dụ: phần tử "select" có ba trình kết xuất đồ hoạ: một cho khu vực 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 dưới dạng trình kết xuất bổ sung.

Một ví dụ khác về việc nhiều trình kết xuất đồ hoạ là HTML bị hỏng. Theo thông số CSS, phần tử cùng dòng chỉ được chứa các phần tử khối hoặc chỉ 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 đồ hoạ khối ẩn danh sẽ được tạo để bao bọc các phần tử cùng dòng.

Một số đối tượng kết xuất tương ứng với nút DOM nhưng không ở cùng một vị trí trong cây. Các độ nổi và các thành phần 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 ánh xạ tới khung thực. Khung phần giữ chỗ là nơi đáng lẽ nên xuất hiện.

Cây hiển thị và cây DOM tương ứng.
Hình 13: Cây hiển thị và cây DOM tương ứng. "Khung nhìn" là khối chứa ban đầu. Trong WebKit, đó sẽ là đối tượng "RenderView"

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 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 sẽ phân giải kiểu (xem phần tính toán kiểu) và tạo một 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ó phương thức "đính kèm". Tệp đính kèm có tính đồng bộ, việc chèn nút vào cây DOM sẽ gọi phương thức "đính kèm" mới.

Việc xử lý thẻ html và thẻ nội dung sẽ dẫn đến việc xây dựng gốc cây hiển thị. Đối tượng kết xuất gốc tương ứng với khối được chứa trong thông số kỹ thuật CSS: khối chứa cao nhất có chứa tất cả các khối khác. Kích thước của phần này là khung nhìn: kích thước của khu vực hiển thị trong cửa sổ trình duyệt. Firefox gọi giao thức này là ViewPortFrame và WebKit gọi giao diện này 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 xây dựng 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

Việc xây dựng cây kết xuất đồ hoạ yêu cầu tính toán các thuộc tính trực quan của từng đối tượng kết xuất. Bạn có thể thực hiện việc này 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 này bao gồm các biểu định kiểu có nhiều nguồn gốc, phần tử kiểu cùng dòng và thuộc tính trực quan trong HTML (như thuộc tính "bgcolor").Sau này đượ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 và biểu định kiểu người dùng cung cấp – đây là những 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, việc này được thực hiện bằng cách đặt biểu định kiểu trong thư mục "Cấu hình Firefox").

Việc tính toán kiểu gây ra một số khó khăn:

  1. 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ớ.
  2. Việc tìm các quy tắc so khớ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 truyền tải 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ụ khó khăn. Bộ chọn có thể có cấu trúc phức tạp, điều này có thể khiến quá trình so khớp bắt đầu trên một đường dẫn có vẻ hứa hẹn được chứng minh là không hiệu quả và người dùng 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 <div> là thành phần con của 3 div. Giả sử bạn muốn kiểm tra xem quy tắc này 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 trên cây để kiểm tra. Có thể bạn cần phải dịch chuyển cây nút lên để biết rằng chỉ có hai div và quy tắc không được áp dụng. Sau đó, bạn cần thử các đường dẫn khác trong cây.

  3. Việc áp dụng quy tắc liên quan đến các quy tắc phân tầng khá phức tạp giúp xác định hệ phân cấp của quy tắc.

Hãy xem cách trình duyệt đối mặt với những vấn đề này:

Chia sẻ dữ liệu kiểu

Nút WebKit tham chiếu đến đối tượng kiểu (RenderStyle). Các nút có thể chia sẻ các đối tượng này trong một số điều kiện. Các nút này là anh em hoặc anh em họ và:

  1. Các thành phần phải ở cùng trạng thái chuột (ví dụ: một thành phần không được ở trong :di chuột trong khi thành phần còn lại thì không)
  2. Không phần tử nào phải có id
  3. Tên thẻ phải khớp
  4. Các thuộc tính lớp phải khớp
  5. Tập hợp các thuộc tính được liên kết phải giống nhau
  6. Trạng thái liên kết phải khớp
  7. Trạng thái lấy nét phải khớp
  8. Không có phần tử nào chịu ảnh hưởng của bộ chọn thuộc tính, trong đó bộ chọn thuộc tính bị ảnh hưởng được xác định là có bất kỳ phần tử trùng khớp nào sử dụng bộ chọn thuộc tính ở bất kỳ vị trí nào trong bộ chọn đó
  9. Không được có thuộc tính kiểu cùng dòng trên phần tử
  10. Bạn không được sử dụng bộ chọn đồng cấp nào. WebCore chỉ cần gửi 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ó bất kỳ bộ chọn đồng cấp nào. Trong đó có bộ chọn dấu + và các bộ chọn như :first-child và :last-child.

Cây quy tắc của Firefox

Firefox có hai cây bổ sung để tính toán định kiểu dễ dàng hơn: cây quy tắc và cây ngữ cảnh kiểu. WebKit cũng 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ỉ nút DOM trỏ đến kiểu có liên quan.

Cây ngữ cảnh kiểu Firefox.
Hình 14: Cây ngữ cảnh kiểu Firefox.

Ngữ cảnh kiểu chứa giá trị kết thúc. Các giá trị được tính bằng cách áp dụng tất cả các quy tắc phù hợp theo đúng thứ tự và thực hiện các thao tác để chuyển đổi chúng từ các giá trị logic sang các giá trị cụ thể. Ví dụ: nếu giá trị logic là một 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ự thông minh. Thao tác này cho phép chia sẻ các giá trị này giữa các nút để tránh tính toán lại chúng. Thao tác này cũng giúp tiết kiệm dung lượng.

Tất cả quy tắc phù hợp được lưu trữ trong một cây. Các nút dưới cùng trong đường dẫn có mức độ ưu tiên cao hơn. Cây này chứa tất cả các đường dẫn cho quy tắc khớp với quy tắc đã được tìm thấy. Việc lưu trữ quy tắc được thực hiện từng phần. Cây không được tính toán từ đầu cho mọi nút, nhưng mỗi khi cần tính toán một kiểu nút, các đường dẫn đã tính toán sẽ được thêm vào cây.

Ý tưởng là xem đường dẫn cây dưới dạng các từ trong từ vựng. Giả sử chúng ta đã tính toán cây quy tắc này:

Cây quy tắc đã tính toán
Hình 15: Cây quy tắc đã tính toán.

Giả sử chúng ta cần khớp 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ờ thì chúng ta không cần làm gì nhiều.

Hãy xem cây giúp chúng ta cứu nguy như thế nào.

Chia thành các cấu trúc

Ngữ cảnh kiểu được chia thành các cấu trúc. Những cấu trúc đó chứa thông tin 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 mà phần tử không xác định được sẽ được kế thừa từ phần tử mẹ. Các thuộc tính không kế thừa (còn gọi là thuộc tính "đặt lại") sử dụng các giá trị mặc định nếu không được xác định.

Cây này hỗ trợ chúng ta bằng cách lưu toàn bộ cấu trúc vào bộ nhớ đệm (chứa các giá trị cuối đã được tính toán) 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ì bạn có thể sử dụng cấu trúc đã lưu vào bộ nhớ đệm ở nút trên.

Tính toán ngữ cảnh kiểu bằng cách sử dụng cây quy tắc

Khi tính toán ngữ cảnh kiểu cho một phần tử nhất định, trước tiên chúng tôi sẽ tính toán đường dẫn trong cây quy tắc hoặc sử dụng đườ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 các cấu trúc trong ngữ cảnh kiểu mới. Chúng ta bắt đầu ở 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à chuyển hướng cây lên cho đến khi cấu trúc đầy đủ. Nếu không có đặc tả cho cấu trúc trong nút quy tắc đó, thì chúng ta có thể tối ưu hoá đáng kể bằng cách di chuyển lên cây cho đến khi tìm thấy nút chỉ định cấu trúc đầy đủ và trỏ đến cấu trúc đó – đó là cách tối ưu hoá tốt nhất – toàn bộ cấu trúc được chia sẻ. Thao tác này giúp tiết kiệm việc tính toán các giá trị cuối và bộ nhớ.

Nếu tìm thấy định nghĩa một phần, chúng ta sẽ di chuyển 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 của mình, thì trong trường hợp cấu trúc là loại "được 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 thành công trong việc chia sẻ 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 thực hiện 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 để con có thể dùng kết quả đó.

Trong trường hợp một phần tử có mục đồng cấp hoặc anh em trỏ đến cùng một nút cây, thì bạn có thể chia sẻ toàn bộ ngữ cảnh kiểu giữa các phần tử đó.

Hãy xem ví dụ: Giả sử chúng ta có đoạn mã 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 2 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 kết quả sẽ trông giống như sau (các nút được đánh dấu bằng tên nút: số của quy tắc mà chúng trỏ vào):

Cây quy tắc
Hình 16: Cây quy tắc

Cây ngữ cảnh sẽ trông giống như sau (tên nút: nút quy tắc mà chúng trỏ đến):

Cây ngữ cảnh.
Hình 17: Cây ngữ cảnh

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 ngữ 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 ta sẽ so khớp các quy tắc và phát hiện ra rằng các quy tắc phù hợp cho <div> là 1, 2 và 6. Điều này có nghĩa là đã có sẵn một đường dẫn 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 ngữ cảnh kiểu và đặt ngữ cảnh đó vào cây ngữ cảnh. Ngữ 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 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ể di chuyển lên cây cho đến khi tìm thấy một cấu trúc đã lưu vào bộ nhớ đệm được tính toán trong lần chèn nút trước đó và sử dụng cấu trúc đó. Chúng ta sẽ tìm thấy nó trên nút B, là nút trên cùng đã chỉ định các 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 di chuyển lên cây để tô các thuộc tính khác. Chúng tôi sẽ tính toán giá trị cuối cùng (chuyển đổi chuỗi thành RGB, v.v.) và lưu cấu trúc đã tính vào nút này vào bộ nhớ đệm.

Công việc trên phần tử <span> thứ hai thậm chí còn dễ dàng hơn. Chúng tôi sẽ đối sánh các quy tắc và đi đến kết luận rằng quy tắc này trỏ đến quy tắc G, giống như khoảng thời gian trước đó. Vì có các thành phần đồng cấp trỏ đến cùng một nút, nên chúng ta có thể chia sẻ toàn bộ ngữ cảnh kiểu và chỉ cần trỏ đến ngữ cảnh của span trước đó.

Đối với các cấu trúc có chứa quy tắc kế thừa từ đơn vị gốc, 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 sẽ coi thuộc tính đó 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 về phông chữ trong một đoạn:

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

Sau đó, phần tử đoạn, là phần tử con của div trong cây ngữ cảnh, có thể chia sẻ cùng một cấu trúc phông chữ với phần tử mẹ. Đó là khi chưa có quy tắc phông chữ nào được chỉ định cho đoạn văn bản.

Trong WebKit, không có cây quy tắc, các khai báo phù hợp được truyền qua bốn lần. Đầu tiên là các thuộc tính có mức độ ưu tiên cao không quan trọng (nê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 thuộc tính có mức độ ưu tiên cao, tiếp đến là các quy tắc có mức độ ưu tiên bình thường không quan trọng, tiếp đến 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 giải quyết theo thứ tự phân tầng chính xác. Những chiến thắng cuối cùng.

Vì vậy, tóm tắt: chia sẻ các đối tượng kiểu (hoàn toàn hoặc một số cấu trúc bên trong chúng) giải quyết cá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ự.

Sửa đổi các quy tắc để so khớp dễ dàng

Có một số nguồn cho quy tắc kiểu:

  1. Các quy tắc CSS, trong biểu định kiểu bên ngoài hoặc trong phần tử kiểu. css p {color: blue}
  2. Các thuộc tính kiểu cùng dòng như html <p style="color: blue" />
  3. Các thuộc tính trực quan HTML (được liên kết với các quy tắc kiểu có 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ử đó sở hữu các thuộc tính kiểu và các 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 đề 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, hệ thống sẽ thay đổi các quy tắc để người dùng truy cập dễ dàng hơn.

Sau khi phân tích cú pháp biểu định kiểu, các quy tắc đượ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 id, theo tên lớp, theo tên thẻ và một bản đồ chung cho bất cứ thứ gì không phù hợp với các danh mục đó. Nếu bộ chọn là một mã nhận dạng, quy tắc sẽ được thêm vào bản đồ mã, nếu đó là một lớp, 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. Không cần phải xem xét trong mọi khai báo: chúng ta có thể trích xuất các quy tắc có liên quan cho một phần tử từ bản đồ. Sự tối ưu hoá này giúp loại bỏ hơn 95% quy tắc, do đó chúng 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 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. Cái thứ hai vào bản đồ id và cái thứ ba vào bản đồ thẻ.

Đối với phân đoạn 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. Sơ đồ lớp sẽ chứa khoá "error" mà theo đó quy tắc "p.error" được tìm thấy. Phần tử div sẽ có các quy tắc liên quan trong bản đồ id (khoá là id) và bản đồ thẻ. Vì vậy, công việc duy nhất còn lại là tìm ra quy tắc nào trong số những quy tắc đã được các khoá trích xuất thực sự khớp.

Ví dụ: nếu quy tắc cho div là:

table div {margin: 5px}

Nó sẽ vẫn đượ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 sẽ không khớp với phần tử div của chúng tôi, phần tử không có đối tượng cấp trên 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ả các thuộc tính CSS nhưng chung hơn). Nếu thuộc tính không được xác định bởi bất kỳ quy tắc phù hợp nào, thì một số thuộc tính có thể được đối tượng kiểu phần tử mẹ kế thừa. 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 – sau đây là thứ tự phân tầng để giải quyết vấn đề.

Phần khai báo cho một thuộc tính kiểu có thể xuất hiện trong một vài biểu định kiểu và nhiều lần bê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 gọi là thứ tự "thác". Theo thông số kỹ thuật CSS2, thứ tự tầng là (từ thấp đến cao):

  1. Nội dung khai báo cho trình duyệt
  2. Nội dung khai báo thông thường của người dùng
  3. Tác giả nội dung khai báo thông thường
  4. Nội dung khai báo quan trọng về tác giả
  5. Nội dung khai báo quan trọng của người dùng

Nội dung khai báo của trình duyệt ít quan trọng nhất và người dùng chỉ ghi đè tác giả nếu nội dung 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 tính cụ thể, sau đó sắp xếp theo thứ tự mà các nội dung khai báo đó được chỉ định. Các thuộc tính trực quan HTML được chuyển đổi sang khai báo CSS trùng khớp . Các quy tắc này được coi là quy tắc tác giả có mức độ ưu tiên thấp.

Mức độ cụ thể

Tính đặc hiệu của bộ chọn được xác định theo quy cách CSS2 như sau:

  1. đếm 1 nếu nội dung khai báo là thuộc tính "style" (kiểu) thay vì quy tắc có bộ chọn, 0 nếu không (= a)
  2. đếm số thuộc tính ID trong bộ chọn (= b)
  3. đếm số thuộc tính và lớp giả khác trong bộ chọn (= c)
  4. đế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ẽ tạo ra tính cụ thể.

Cơ sở số bạn cần sử dụng được xác định theo số lượng cao nhất bạn có ở một trong các danh mục này.

Ví dụ: nếu a=14, bạn có thể sử dụng cơ số thập lục phân. Trong trường hợp hiếm gặp là a=17, bạn sẽ cần cơ sở số gồm 17 chữ số. Tình huống sau đó có thể xảy ra với một công cụ chọn như sau: html body div div p... (17 thẻ trong bộ chọn của bạn... không có nhiều khả năng).

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 */

Sắp xếp quy tắc

Sau khi các quy tắc được khớp, chúng được sắp xếp theo quy tắc xếp tầng. WebKit sử dụng sắp xếp bong bóng cho danh sách nhỏ và sắp xếp hợp nhất cho các danh sách lớn. WebKit triển khai tính năng 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 cờ đánh dấu nếu tất cả biểu định kiểu cấp cao nhất (bao gồm @nhập) đã được tải. Nếu kiểu không được tải đầy đủ khi đính kèm, 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 biểu định kiểu được tải.

Bố cục

Khi trình kết xuất được tạo và thêm vào cây, trình kết xuất sẽ 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à layout (bố cục) hoặc chỉnh lại luồng (reflow).

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 có thể tính toán hình học trong một lần truyền. Các phần tử sau đó "trong luồng" thường không ảnh hưởng đến hình dạng của các phần tử trước đó "trong luồng", vì vậy bố cục có thể tiếp tục từ trái sang phải, từ trên xuống dưới thông qua tài liệu. Có những ngoại lệ: chẳng hạn như các bảng HTML có thể yêu cầu nhiều thẻ và vé.

Hệ toạ độ tương ứng với khung gốc. Sử dụng toạ độ trên cùng và bên trái.

Bố cục là một quá 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 mỗi trình kết xuất yêu cầu.

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 đồ hoạ 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 "chỉnh lại luồng", mỗi trình kết xuất sẽ gọi phương thức bố cục của các trình kết xuất con cần bố cục.

Hệ thống bit bẩn

Để không 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". Một 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 là "bẩn": cần có bố cục.

Có hai cờ: "dơ" và "trẻ em bị bẩn", nghĩa là mặc dù trình kết xuất có thể vẫn ổn, nhưng có ít nhất một trình kết xuất con cần có bố cục.

Bố cục chung và gia tăng

Bạn có thể kích hoạt bố cục trên toàn bộ cây kết xuất – đây là bố cục "toàn cầu". Điều này có thể xảy ra do:

  1. Thay đổi về kiểu chung ảnh hưởng đến tất cả trình kết xuất, chẳng hạn như thay đổi kích thước phông chữ.
  2. Do màn hình được đổi kích thước

Bố cục có thể tăng dần, chỉ các trình kết xuất đồ hoạ bẩn mới được bố trí (điều này có thể gây ra một số thiệt hại và sẽ cần 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ị sửa đổi. Ví dụ: khi các trình kết xuất mới được thêm vào cây hiển thị sau khi nội dung bổ sung đến từ mạng và được thêm vào cây DOM.

Bố cục gia tăng.
Hình 18: Bố cục tăng dần – chỉ trình xuất đồ hoạ bẩn và các trình kết xuất con của các trình kết xuất đó được bố trí

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 xếp hàng "các lệnh chỉnh lại luồng" cho bố cục tăng dần và trình lập lịch biểu sẽ kích hoạt quá trình thực thi hàng loạt các lệnh này. WebKit cũng có bộ tính giờ thực thi bố cục tăng dần - cây được duyệt qua và trình kết xuất "bẩn" được bố trí.

Những tập lệnh yêu cầu thông tin kiểu, chẳng hạn như "offsetHeight" có thể kích hoạt bố cục tăng dần 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ởi việc "đổi kích thước" hoặc thay đổi vị trí kết xuất đồ hoạ(chứ không phải kích thước), kích thước 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ó 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 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 các trường văn bản (nếu không mỗi thao tác nhấn phím sẽ kích hoạt mộ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:

  1. Trình kết xuất gốc xác định chiều rộng của riêng nó.
  2. Cha mẹ quản lý các phần tử con và:
    1. Đặt trình kết xuất đồ hoạ con (thiết lập x và y).
    2. Gọi bố cục con nếu cần – bố cục con bẩn hoặc chúng ta đang ở trong bố cục toàn cục hay vì một số lý do khác – tính toán chiều cao của bố cục con.
  3. Phần tử mẹ sử dụng chiều cao tích luỹ của phần tử con cũng như chiều cao của lề và khoảng đệm để đặt chiều cao riêng. Giá trị này sẽ được phần tử mẹ của trình kết xuất mẹ sử dụng.
  4. Đặt bit sửa đổi thành false.

Firefox sử dụng đối tượng "state" (nsHTMLReflowState) làm tham số cho bố cục (gọi là "reflow"). Trong số các trạng thái khác, trạng thái bao gồm chiều rộng của thành phần mẹ.

Đầu ra của bố cục Firefox là một đối tượng "metric" (nsHTMLReflowMetrics). Tệp này sẽ chứa chiều cao do trình kết xuất tính toán.

Tính toán chiều rộng

Chiều rộng của trình kết xuất được tính bằng cách sử dụng chiều rộng của khối vùng chứa, thuộc tính "width" (chiều rộng) kiểu của 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 RenderBox của lớp calcWidth):

  • Chiều rộng vùng chứa là tối đa của vùng chứa availableWidth 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 đố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 "chiều rộng". Chiều rộng vùng chứa 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 ngang và khoảng đệm 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.

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 nhỏ hơn chiều rộng tối thiểu (đơn vị không thể phá vỡ nhỏ nhất), thì chiều rộng tối thiểu được sử dụng.

Các giá trị được lưu vào bộ nhớ đệm trong trường hợp cần bố cục nhưng chiều rộng không thay đổi.

Ngắt đường kẻ

Khi một trình kết xuất ở giữa một bố cục quyết định rằng cần phải hỏng, trình kết xuất này sẽ dừng lại và truyền đến thành phần mẹ của bố cục rằng cần phải phá vỡ. Thành phần 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 hiển thị được truyền tải và phương thức "Painter()" của trình kết xuất được gọi để hiển thị nội dung trên màn hình. Tính năng 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

Tương tự như bố cục, việc vẽ tranh cũng có thể áp dụng trên toàn cầu, chẳng hạn như vẽ một lần hoặc tăng dần số lượng cây. Trong quá trình vẽ tăng dần, một số trình kết xuất đồ hoạ 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ẽ làm mất hiệu lực hình chữ nhật trên màn hình. Việc này khiến hệ điều hành xem đó là "khu vực bẩn" và tạo ra sự kiện "sơn". Hệ điều hành thực hiện việc này một cách khéo léo và kết hợp nhiều vùng thành một. Trong Chrome, quy trình 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 độ nào đó. Bản trình bày sẽ theo dõi các sự kiện này và uỷ quyền thông báo cho thư mục gốc kết xuất. Cây được di chuyển cho đến khi tiếp cận được trình kết xuất có liên quan. Tài sản này sẽ tự vẽ lại (và thường là con của nó).

Đơn đặt hàng

CSS2 xác định thứ tự của quy trình vẽ. Đây thực sự là thứ tự mà các phần tử được xếp chồng trong ngữ cảnh xếp chồng. Thứ tự này ảnh hưởng đến tình trạng 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à:

  1. màu nền
  2. hình nền
  3. border
  4. trẻ em
  5. outline

Danh sách hiển thị của Firefox

Firefox chuyển qua cây kết xuất và tạo một 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 thứ tự vẽ bên phải (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 một lần để sơn lại thay vì nhiều lần – vẽ tất cả nền, sau đó là tất cả hình ảnh, sau đó tất cả đường viền, v.v.

Firefox tối ưu hoá quy trình này 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ử nằm hoàn toàn bên 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 đó, hàm này chỉ vẽ vùng delta giữa các hình chữ nhật mới và cũ.

Thay đổi động

Trình duyệt sẽ cố gắng thực hiện các hành động tối thiểu có thể để phản hồi một thay đổi. Vì vậy, khi thay đổi màu của một phần tử, thì phần tử đó sẽ chỉ được vẽ lại. Các thay đổi đối với vị trí của phần tử sẽ dẫn đến việc bố cục và vẽ lại phần tử, phần tử con và có thể là các phần tử đồng cấp. Thêm nút DOM sẽ tạo ra bố cục và vẽ lại nút. Những thay đổi chính, như tăng kích thước phông chữ của phần tử "html", sẽ khiến bộ nhớ đệm không hợp lệ, bố cục lại và vẽ lại toàn bộ cây.

Luồng của công cụ kết xuất

Công cụ kết xuất hình ảnh có dạng đơn luồng. Hầu hết mọi việc (ngoại trừ 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ẻ.

Một số luồng song song có thể thực hiện hoạt động mạng. Số lượng các mối nối song song bị hạn chế (thường là 2 – 6 mối 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. Một vòng lặp vô hạn giúp quy trình này diễn ra. Nó 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ã của Firefox cho vòng lặp sự kiện chính:

while (!mExiting)
    NS_ProcessNextEvent(thread);

Mô hình hình ảnh CSS2

Canvas

Theo thông số kỹ thuật của CSS2, thuật ngữ canvas mô tả "không gian hiển thị cấu trúc định dạng": nơi trình duyệt vẽ nội dung.

Canvas là vô hạn đối với mỗi kích thước của không gian nhưng các trình duyệt 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 trong suốt nếu nằm trong một canvas khác và được cung cấp màu do trình duyệt xác định nếu không được chứa.

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 trực quan.

Mỗi hộp có một vùng nội dung (ví dụ: văn bản, hình ảnh, v.v.) cũng như các vùng đệm, đường viền và lề tuỳ chọn xung quanh.

Mô hình hộp CSS2
Hình 19: Mô hình hộp CSS2

Mỗi nút tạo ra 0...n hộp như vậy.

Tất cả các phần tử đều có thuộc tính "display" giúp 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 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ể xem 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 lược đồ:

  1. Thông thường: đối tượng được định vị theo vị trí của nó trong tài liệu. Điều này có nghĩa là vị trí của nó trong cây hiển thị 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.
  2. Nổi: trước tiên, đối tượng được bố trí như dòng chảy bình thường, sau đó di chuyển ra xa sang trái hoặc sang phải nhất có thể
  3. Tuyệt đối: đối tượng được đặt trong cây hiển thị ở một vị trí khác với trong cây DOM

Lược đồ định vị được đặt bởi thuộc tính "position" và thuộc tính "float".

  • tĩnh và tương đối gây ra dòng chảy bình thường
  • vị trí tuyệt đối và nguyên nhân cố định

Trong định vị tĩnh, không có vị trí nào được xác định và vị trí mặc định được sử dụng. Trong các lược đồ khác, tác giả sẽ 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 như kích thước hình ảnh và kích thước của 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 chặn.
Hình 20: Hộp khối

Hộp nội tuyến: không có khối riêng nhưng nằm trong một khối chứa.

Hộp nội tuyến.
Hình 21: Hộp cùng dòng

Các khối được định dạng lần lượt theo chiều dọc. Dòng nội tuyến được định dạng theo chiều ngang.

Định dạng chặn và cùng dòng.
Hình 22: Định dạng khối và cùng dòng

Hộp nội tuyến được đặt bên trong các dòng hoặc "hộp dòng". Các đường ít nhất sẽ cao bằng hình hộp cao nhất nhưng cũng có thể cao hơn, khi các hộp được căn chỉnh thành "đường cơ sở" – có nghĩa là phần dưới cùng của một phần tử được căn chỉnh tại điểm của một hình khác hộp khác rồi đến đáy. Nếu chiều rộng vùng chứa không đủ, các dòng nội tuyến sẽ được đặt trên một số dòng. Đây thường là điều sẽ xảy ra trong một đoạn văn.

Đường kẻ.
Hình 23: Đường

Xác lập vị thế

Họ hàng

Vị trí tương đối – được định vị như bình thường, sau đó được di chuyển theo delta bắt buộc.

Vị trí tương đối.
Hình 24: Vị trí tương đối

Nổi

Một hộp nổi được dịch chuyển sang trái hoặc bên phải của một đường kẻ. Điều thú vị là các ô khác chạy xung quanh. 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:

Số thực có độ chính xác đơn.
Hình 25: Số thực

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 dữ liệu thông thường. Các phương diện có liên quan đến vùng chứa. Ở trạng thái cố định, vùng chứa là khung nhìn.

Vị trí cố định.
Hình 26: Vị trí cố định

Biểu diễn theo lớp

Điều này được chỉ định bởi thuộc tính CSS z-index. Thuộc tính này đạ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 ô này được chia thành các ngăn xếp (được gọi là ngữ cảnh xếp chồng). Trong mỗi ngăn xếp, các thành phần phía sau sẽ được vẽ trước và các thành phần phía trước sẽ được vẽ gần với người dùng hơn. Trong trường hợp chồng chéo, phần tử trên cùng sẽ ẩn phần tử cũ.

Các ngăn xếp được sắp xếp theo thuộc tính chỉ mục z. 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ẽ là:

Vị trí cố định.
Hình 27: Vị trí cố định

Mặc dù div màu đỏ đứng trước div màu xanh lục trong phần đánh dấu và có thể đượ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 nó nằm phía trước nhiều hơn trong ngăn xếp do hộp gốc giữ.

Tài nguyên

  1. Cấu trúc trình duyệt

    1. [Tên người], Alan. Cấu trúc tham chiếu cho các trình duyệt web (pdf)
    2. Gupta, Vineet. Cách trình duyệt hoạt động – Phần 1 – Cấu trúc
  2. Phân tích cú pháp

    1. Aho, Sethi, Ullman, Compilers: Guidelines, Kỹ thuật và Tools (còn gọi là "Cuốn sách về rồng"), Addison-Wesley, 1986
    2. Rick Jelliffe. The Bold and the Beautiful: hai bản nháp mới cho HTML 5.
  3. Firefox

    1. L. David Baron, HTML và CSS nhanh hơn: Layout Engine nội bộ dành cho nhà phát triển web.
    2. L. David Baron, HTML và CSS nhanh hơn: Layout Engine nội bộ dành cho nhà phát triển web (video nói chuyện về công nghệ của Google)
    3. L. David Baron, Công cụ bố cục của Mozilla
    4. L. David Baron, Tài liệu về hệ thống kiểu Mozilla
    5. Chris Waterson, Notes on HTML Reflow (Lưu ý về tính năng chỉnh lại luồng)
    6. Chris Waterson, Tổng quan về tắc kè
    7. Alexander Larsson, Cuộc đời của một yêu cầu HTML HTTP
  4. WebKit

    1. David Hyatt, Triển khai CSS(phần 1)
    2. David Hyatt, Tổng quan về WebCore
    3. David Hyatt, Kết xuất webCore
    4. David Hyatt, Vấn đề FOUC
  5. Thông số kỹ thuật W3C

    1. Thông số kỹ thuật HTML 4.01
    2. Quy cách HTML5 W3C
    3. Thông số kỹ thuật của Biểu định kiểu xếp tầng cấp 2 Bản sửa đổi 1 (CSS 2.1)
  6. Hướng dẫn tạo trình duyệt

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

Bản dịch

Trang này đã được dịch sang tiếng Nhật, hai lần:

Bạn có thể xem các bản dịch được lưu trữ bên ngoài của tiếng Hàntiếng Thổ Nhĩ Kỳ.

Cảm ơn mọi người!