Hậu trường trình duyệt web hiện đại
Lời nói đầu
Thông tin 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à là kết quả của nhiều nghiên cứu của nhà phát triển người Israel Tali Gariel. Hơn một vài năm vừa qua, cô ấy xem xét tất cả dữ liệu được xuất bản về bên trong trình duyệt và dành một dành rất 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, hãy tìm hiểu các thành phần bên trong hoạt động của trình duyệt giúp bạn đưa ra quyết định tốt hơn và biết giải thích cho sự phát triển các phương pháp hay nhất. Mặc dù đây là tài liệu khá dài nhưng bạn nên mà bạn dành thời gian tìm hiểu. Bạn sẽ hài lòng với trải nghiệ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 bài viết này, tôi sẽ giải thích cách
họ làm việc trong hậu trườ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.
Những trình duyệt chúng ta sẽ đề cập
Hiện nay, có 5 trình duyệt chính được sử dụng trên máy tính: Chrome, Internet Explorer, Firefox, Safari và Opera. Trên điện thoại di động, các trình duyệt chính là Android Browser, iPhone, Opera Mini và Opera Mobile, UC Browser, các trình duyệt Nokia S40/S60 và Chrome, tất cả các trình duyệt đó, ngoại trừ trình duyệt Opera, đều dựa trên WebKit. Tôi sẽ đưa ra ví dụ qua các trình duyệt nguồn mở Firefox, Chrome và Safari (là các trình duyệt nguồn mở một phần). Theo 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à trình bày tài nguyên web bạn chọn, bằng cách yêu cầu tài nguyên web từ máy chủ và hiển thị tài nguyên đó trong cửa sổ trình duyệt. Tài nguyên thường là 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 cách sử dụng URI (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 duy trì bởi tổ chức W3C (World Wide Web Consortium), là tổ chức tiêu chuẩn cho web. 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 phần mở rộng riêng. Điều đó 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ủ các thông số kỹ thuật.
Giao diện người dùng của trình duyệt có nhiều điểm chung với nhau. Sau đây là các phần tử phổ biến của giao diện người dùng:
- Thanh địa chỉ để chèn URI
- Nút tiến và lùi
- Tuỳ chọn đánh dấu trang
- Các 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ủ
Đ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ỳ thông số kỹ thuật chính thức nào, mà chỉ xuất phát từ những phương pháp hay được hình thành qua nhiều năm kinh nghiệm và do 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à một trình duyệt cần có, nhưng liệt kê một số phần tử 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 dành riêng 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à:
- 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 hiển thị, 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: kết hợp các thao tác 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 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: cho 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 trong 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 nó sử dụng các phương thức giao diện người dùng của hệ điều hành.
- Phiên dịch JavaScript. Dùng để phân tích cú pháp và thực thi mã JavaScript.
- Bộ nhớ dữ liệu. Đây là lớp cố định. Trình duyệt có thể cần lưu tất cả các loại dữ liệu trên máy, 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](https://web.developers.google.cn/static/articles/howbrowserswork/image/browser-components-9cd8ff834cc9c.png?authuser=3&hl=vi)
Đ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.
Công cụ kết xuất
Trách nhiệm của công cụ kết xuất là rất tốt... 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ụ hiển thị có thể hiển thị các tài liệu và hình ảnh HTML và XML. Nó có thể hiển thị các loại dữ liệu khác thông qua các 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ô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à 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.
Luồng chính
Công cụ kết xuất sẽ bắt đầu tải nội dung của tài liệu được yêu cầu từ tầng mạng. Quy trình này thường được thực hiện trong các phân đ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ị](https://web.developers.google.cn/static/articles/howbrowserswork/image/rendering-engine-basic-fl-2fba02b24e871.png?authuser=3&hl=vi)
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 một cây được gọi là "cây nội dung". Công cụ sẽ phân tích cú pháp dữ liệu kiểu, cả trong tệp CSS bên ngoài và trong phần tử kiểu. Tạo kiểu thông tin 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 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. Các hình chữ nhật này sẽ xuất hiện theo đúng thứ tự trên màn hình.
Sau khi xây dựng cây kết xuất, cây đó sẽ trải qua một "layout" (bố cục) của chúng tôi. Tức là cung cấp cho mỗi nút các toạ độ chính xác ở vị trí mà nút sẽ xuất hiện trên màn hình. Giai đoạn tiếp theo là sơn – cây kết xuất sẽ được di chuyển và mỗi nút sẽ được vẽ bằng lớp phụ trợ của 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 từ từ. Để cải thiện trải nghiệm 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 sớm nhất có thể. Công cụ này 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 tạo và bố cục 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 này tiếp tục với phần nội dung còn lại từ mạng.
Ví dụ về luồng chính
![Luồng chính của WebKit.](https://web.developers.google.cn/static/articles/howbrowserswork/image/webkit-main-flow-b779d50c0cf28.png?authuser=3&hl=vi)
![Quy trình chính cho công cụ kết xuất Gecko của Mozilla.](https://web.developers.google.cn/static/articles/howbrowserswork/image/mozillas-gecko-rendering-b18e445544965.jpg?authuser=3&hl=vi)
Từ hình 3 và 4, bạn có thể thấy rằng mặc dù WebKit và Gecko sử dụng các thuật ngữ hơi khác nhau, nhưng về cơ bản thì luồng giao diện giống nhau.
Gecko gọi cây 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. WebKit sử dụng thuật ngữ "Cây hiển thị" và nó bao gồm "Đối tượng kết xuất". WebKit sử dụng thuật ngữ "bố cục" để đặt vị trí các phần tử, trong khi Gecko gọi đó là "Chỉnh lại luồng". "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 trực quan để tạo cây kết xuất. Một điểm khác biệt nhỏ về 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 lưu trữ nội dung" và là một nhà máy để tạo ra 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
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. Hãy bắt đầu với phần giới thiệu nhỏ về 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ả phân tích cú pháp thường là một cây các nút đại diện cho cấu trúc của tài liệu. Cây nà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.](https://web.developers.google.cn/static/articles/howbrowserswork/image/mathematical-expression-t-6681a2511ead2.png?authuser=3&hl=vi)
Ngữ pháp
Việc phân tích cú pháp được 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 tài liệu được viết. 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. Nó đượ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ữ đó và do đó không thể được phân tích cú pháp bằng kỹ thuật phân tích cú pháp thông thường.
Trình phân tích cú pháp - Tổ hợp Lexer
Phân tích cú pháp có thể được tách thành hai quá 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ỏ thông tin đầu vào thành mã thông báo. Mã thông báo là từ vựng 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, dữ liệu 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 của ngôn ngữ.
Các trình phân tích cú pháp thường chia công việc cho 2 thành phần: trình phân tích cú pháp (đôi khi được gọi là trình phân tích mã thông báo) chịu trách nhiệm chia nhỏ 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 cú pháp ngôn ngữ.
Trình 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à ngắt dòng.
![Từ tài liệu nguồn để phân tích cú pháp cây](https://web.developers.google.cn/static/articles/howbrowserswork/image/from-source-document-par-c9c8c59da1ef2.png?authuser=3&hl=vi)
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 đọc 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 được so 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 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 một quy tắc khớp với tất cả cá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 thì trình phân tích cú pháp sẽ đưa ra ngoại lệ. Tức là tài liệu 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 sử dụng trong bản dịch: chuyển đổi tài liệu đầu vào sang định dạng khác. Một ví dụ là biên dịch. Trước tiên, trình biên dịch biên dịch mã nguồn thành mã máy sẽ phân tích cú pháp mã này thành một 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.
![Quy trình biên dịch](https://web.developers.google.cn/static/articles/howbrowserswork/image/compilation-flow-57cfc3aa68a53.png?authuser=3&hl=vi)
Ví dụ về cách phân tích cú pháp
Trong hình 5, chúng ta đã xây dựng 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 quá trình phân tích cú pháp.
Cú pháp:
- Các thành phần tạo nên cú pháp ngôn ngữ là biểu thức, thuật ngữ và phép tính.
- Ngôn ngữ của chúng ta có thể bao gồm số lượng biểu cảm bất kỳ.
- Một biểu thức được định nghĩa là một "thuật ngữ" theo sau là "thao tác" sau đó là một cụm từ khác
- Hoạt động là mã cộng hoặc mã trừ
- 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 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, đó là một cụm từ.
Khớ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 phép toán và theo sau là một cụm từ khác.
Kết quả trùng khớp tiếp theo sẽ chỉ xuất hiện ở cuối dữ liệu nhập.
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 toán tử và 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à 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 ta sẽ được định nghĩa 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 định nghĩa ở định dạng có tên là BNF. Ngôn ngữ của chúng tôi sẽ được định nghĩa 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 cá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ữ đó không có ngữ cảnh. Định nghĩa trực quan về ngữ pháp không theo ngữ cảnh là ngữ pháp có thể được diễn đạt hoàn toàn bằng BNF. Để có định nghĩa chính thức, hãy xem Bài viết trên Wikipedia về ngữ pháp không theo bối 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à các 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 ra kết quả phù hợ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 thông tin đầu vào và dần chuyển đổi thông tin đó thành quy tắc 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
dưới dạng một biểu thức. Sau đó, phương thức 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, phù hợ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 quy tắc. Sau đó, quy tắc này sẽ thay thế dữ liệu đầu vào trùng khớp bằng quy tắc. Thao tác này sẽ diễn ra cho đến khi kết thúc quá trình 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.
Loại trình phân tích cú pháp từ dưới lên được gọi là trình phân tích cú pháp giảm dịch chuyển do 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 tiên khi bắt đầu nhập và di chuyển sang phải), sau đó giảm dần thành 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 một 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 - từ vựng và quy tắc cú pháp của ngôn ngữ - và họ sẽ tạo ra một trình phân tích cú pháp hoạt động. Để tạo một trình phân tích cú pháp, bạn phải hiểu rõ về quá trình phân tích cú pháp, đồng thời không dễ để 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 một trình phân tích cú pháp và Bison để tạo một trình phân tích cú pháp (bạn có thể gặp phải các trình tạo này với tên là Lex và Yacc). Dữ liệu đầu vào Flex là một tệp chứa định nghĩa biểu thức chính quy của các mã thông báo. Thông tin đầu vào của Bison là các quy tắc về 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 đá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 xác định chính thức bằng cách sử dụng các định dạng như BNF.
Rất tiếc, tất cả các chủ đề trình phân tích cú pháp thông thường không áp dụng cho HTML (tôi không đề cập đến chúng chỉ để cho vui - chúng sẽ được dùng để 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 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 (Định nghĩa loại tài liệu) – nhưng không phải là ngữ pháp tự do theo ngữ cảnh.
Điều này có vẻ lạ lẫm ngay từ lần đầu tiên; 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 - IME - vậy điểm khác biệt lớn là gì?
Điểm khác biệt là phương pháp HTML "dễ tha" hơn: bạn có thể bỏ qua một số thẻ nhất định (sau đó được thêm ngầm) hoặc đôi khi bỏ qua thẻ mở hoặc thẻ đóng, v.v. Nhìn chung, ứng dụng này "mềm" khá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 lại tạo nên sự khác biệt. Một mặt, đây là lý do chính khiến HTML rất phổ biến: HTML giúp bạn tha thứ sai lầm và làm cho cuộc sống trở nên dễ dàng cho tác giả trang web. Mặt khác, điều này gây khó khăn cho việc viết một ngữ pháp trang trọng. Tóm lại, các trình phân tích cú pháp thông thường không thể phân tích cú pháp HTML một cách dễ dàng vì ngữ pháp của HTML không nhất thiết phải có ngữ cảnh. Không thể phân tích cú pháp HTML bằng trình phân tích cú pháp XML.
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ả cá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 thành một 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 các thông số kỹ thuật, còn các chế độ khác có hỗ trợ đánh dấu mà các trình duyệt trong quá khứ sử dụng. Mục đích là khả năng tương thích ngược với nội dung cũ. DTD hiện tại nghiêm ngặt như sau: 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 phần tử DOM và các nút thuộc tính. DOM là viết tắt của Mô hình đối tượng tài liệu. Đó là việc 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 như JavaScript.
Gốc của cây là "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>
Đá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](https://web.developers.google.cn/static/articles/howbrowserswork/image/dom-tree-the-example-mar-70be67fe14c9a.png?authuser=3&hl=vi)
Giống như HTML, DOM được chỉ định bởi tổ chức W3C. Hãy truy cập vào 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 đị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 rằng cây chứa các nút DOM, ý tôi là cây được tạo 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 những cách 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ễ tha của ngôn ngữ.
- Thực tế là trình duyệt có khả năng chấp nhận 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 sẽ lặp 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ư các phần tử tập lệnh chứa lệnh gọi
document.write()
) có thể thêm các mã thông báo bổ sung, vì vậy, quá trình phân tích cú pháp sẽ sửa đổi dữ liệu đầu vào.
Không thể sử dụng 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: tạo mã thông báo 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 mã thông báo. Trong số các mã thông báo HTML có thẻ mở, thẻ đóng, tên thuộc tính và giá trị thuộc tính.
Trình tạo mã thông báo nhận dạng 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ứ tiếp tụ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ố HTML5)](https://web.developers.google.cn/static/articles/howbrowserswork/image/html-parsing-flow-taken-6118c51b92b56.png?authuser=3&hl=vi)
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 đượ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ự đó. Trạng thái tạo mã thông báo hiện tại và trạng thái xây dựng cây sẽ ảnh hưởng đến quyết định nà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 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 quá phức tạp để 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.
Ví dụ cơ bản – tạo mã thông báo cho 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ẽ dẫn đến việc tạo "Mã thông báo thẻ bắt đầu", trạng thái sẽ được thay đổi thành "Trạng thái tên thẻ".
Chúng ta sẽ giữ trạng thái này cho đến khi ký tự >
được sử dụng. Mỗi ký tự sẽ được thêm vào tên của mã thông báo mới. Trong trường hợp này, mã thông báo đã tạo là mã thông báo html
.
Khi bạn đạt đến thẻ >
, mã thông báo hiện tại sẽ được phát và trạng thái sẽ chuyển 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ẻ html
và body
đã được phát ra. Chúng ta hiện đã quay lại sử dụng "Trạng thái dữ liệu".
Việc sử dụng ký tự H
của Hello world
sẽ dẫn đến việc 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 đã quay lại "Trạng thái mở thẻ".
Việc sử dụng dữ liệu đầu vào tiếp theo /
sẽ dẫn đến việc tạo end tag token
và chuyển sang "Trạng thái tên thẻ". Một lần nữa, chúng ta sẽ giữ trạng thái này cho đến khi đạt >
.Sau đó, mã thông báo thẻ mới sẽ được phát và quay lại "Trạng thái dữ liệu".
Giá trị đầu vào </html>
sẽ được xử lý giống như trường hợp trước đó.
![Mã hoá dữ liệu đầu vào mẫu](https://web.developers.google.cn/static/articles/howbrowserswork/image/tokenizing-example-input-9d0cc36689681.png?authuser=3&hl=vi)
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 có Tài liệu trong gốc sẽ được sửa đổi và các thành phần sẽ được thêm vào đó. 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 cây xử lý. Đối với mỗi mã thông báo, thông số kỹ thuật 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ăn xếp các phần tử mở. Ngăn xếp này được dùng để sửa lỗi không khớp lồng nhau và thẻ chưa được đóng. Thuật toán này cũng được mô tả là máy trạng thái. Các trạng thái này được gọi là "chế độ chèn".
Hãy xem quá trình xây dựng cây để có dữ liệu đầu vào mẫu:
<html>
<body>
Hello world
</body>
</html>
Dữ liệu đầu vào cho giai đoạn xây dựng cây là một chuỗi các mã thông báo từ giai đoạn tạo mã thông báo. Chế độ đầu tiên là "chế độ ban đầu". Nhận "html" mã thông báo sẽ khiến chuyển sang chế độ "before html" và xử lý lại mã thông báo ở chế độ đó. Điều này sẽ dẫn đến việc tạo phần tử HTMLhtmlElement, sẽ được thêm vào đối tượng Tài liệu gốc.
Trạng thái sẽ đổi thành "before head". "Cơ thể" mã thông báo sẽ được nhận. HTMLHeadElement sẽ được tạo ngầm mặc dù chúng ta không có "head" mã và mã này sẽ được thêm vào cây.
Hiện tại, chúng tôi chuyển sang chế độ "in head" rồi 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, đồng thời chế độ này được chuyển sang "in body".
Mã ký tự của thông báo "Hello world" (Xin chào mọi người) hiện đã nhận được chuỗi. Thao tác đầu tiên sẽ dẫn đến việc tạo và chèn một "Văn bản" nút và các ký tự khác sẽ được thêm vào nút đó.
Việc nhận mã thông báo cuối nội dung sẽ chuyển sang chế độ "after body". Bây giờ, chúng ta sẽ nhận thẻ đóng html. Thẻ này sẽ chuyển chúng ta sang chế độ "sau phần nội dung". Khi nhận được mã thông báo kết thúc của tệp, quá trình phân tích cú pháp sẽ kết thúc.
![Xây dựng cây HTML mẫu.](https://web.developers.google.cn/static/articles/howbrowserswork/image/tree-construction-exampl-4e9757a851f96.gif?authuser=3&hl=vi)
Các thao tác khi quá trình phân tích cú pháp kết thúc
Ở 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 đang bị "trì hoãn" mode: những lệnh sẽ được thực thi sau khi phân tích cú pháp tài liệu. Sau đó, trạng thái của tài liệu sẽ được đặt thành "hoàn tất" và "tải" sự kiện sẽ được kích hoạt.
Bạn có thể xem toàn bộ các thuật toán để tạo mã thông báo và xây dựng cây trong tài liệu thông số kỹ thuật của HTML5.
Trình duyệt khả năng chịu sai số
Bạn không bao giờ nhận được thông báo "Cú pháp không hợp lệ" lỗi trên trang HTML. Trình duyệt sẽ khắc phục 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 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 khiếu nại. Vì vậy, rất nhiều mã phân tích cú pháp đang sửa các lỗi về tác giả HTML.
Quá trình xử lý lỗi trong các trình duyệt khá nhất quán, nhưng đáng kinh ngạc rằng lỗi này không thuộc quy cách của HTML. Giống như đánh dấu trang và nút tiến/lùi, nút chuyển này là tính năng đã được phát triển trong các trình duyệt trong những năm qua. Có các cấu trúc HTML không hợp lệ đã biết được lặp lại trên nhiều trang web và trình duyệt cố gắng sửa lỗi theo cách phù hợp với các trình duyệt khác.
Thông số kỹ thuật của HTML5 có xác định một vài yêu cầu trong số này. (Unity 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 dữ liệu đầu vào được mã hoá vào tài liệu, tạo ra cây tài liệu. Nếu tài liệu được định dạng đúng, việc phân tích cú pháp tài liệu phải đơn giản.
Rất tiếc, chúng tôi phải xử lý nhiều tài liệu HTML không được định dạng đúng, vì vậy trình phân tích cú pháp phải chịu được lỗi.
Chúng tôi phải quan tâm đến các điều kiện lỗi ít nhất sau đây:
- Phần tử đang được thêm vào 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 thẻ cấm phần tử và thêm thẻ sau.
- Chúng ta không được phép thêm trực tiếp phần tử này. 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). Đây có thể là trường hợp với các thẻ sau: HTML HEAD BODY TBODY TR TD LI (tôi có quên gì không?).
- Chúng ta muốn thêm một phần tử khối 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 tới 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ấ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 xử lý như <br>
.
Mã:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
Lưu ý rằng quá trình xử lý lỗi là nội bộ: người dùng sẽ không thấy lỗi này.
Một chiếc bàn thất lạc
Bảng rời là một bảng nằm trong một bảng khác, nhưng không nằm 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 lồng nhau
Trong trường hợp người dùng đặt biểu mẫu vào 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
Nhận xét tự thể hiện.
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ẻ đóng HTML hoặc phần nội dung bị đặt sai vị trí
Một lần nữa - chính nhận xét đó tự thể hiện.
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
Do đó, các tác giả trang web hãy cẩn thận - trừ khi bạn muốn xuất hiện dưới dạng một ví dụ trong đoạn mã chịu lỗi WebKit - hãy viết HTML được định dạng đú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 theo 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 cú pháp và từ vựng của CSS.
Hãy cùng xem một số ví dụ:
Ngữ pháp từ vựng (từ vựng) được xác định bởi các biểu thức chính quy cho từng 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à tên viết tắt của giá trị nhận dạng, chẳng hạn như tên lớp. "tên" 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 là cấu trúc này:
div.error, a.error {
color:red;
font-weight:bold;
}
div.error
và a.error
là các 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ằng 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à 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 dấu ngoặc nhọn và bên trong đó có một nội dung khai báo hoặc nhiều 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 đâ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. Hãy nhớ lại 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 dịch chuyển từ dưới lên trên. Firefox sử dụng trình phân tích cú pháp từ trên xuống được viết theo cách 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. Các đối tượng quy tắc CSS chứa 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.](https://web.developers.google.cn/static/articles/howbrowserswork/image/parsing-css-4531ebee58764.png?authuser=3&hl=vi)
Thứ tự xử lý cho 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ả kỳ vọng các tập lệnh sẽ được phân tích cú pháp và thực thi ngay khi trình phân tích cú pháp đạt đế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 ở bên ngoài thì trước tiên, tài nguyên phải được tìm nạp từ mạng – quá trình này cũng được thực hiện một cách đồng bộ và quá trình phân tích cú pháp sẽ tạm dừng cho đến khi tìm nạp tài nguyên.
Đâ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 dấu "trì hoãn" cho tập lệnh, trong trường hợp đó, lệ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 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ờ đó, một luồng khác sẽ phân tích cú pháp và thực thi tập lệnh này.
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, đồng thời tìm hiểu những tài nguyên khác cần được tải qua 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 thông tin tham chiếu đến các 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: công cụ này không sửa đổi cây DOM mà chỉ phân tích cú pháp theo 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ẫu khác. Về mặt lý thuyết, có vẻ như vì biểu định kiểu không thay đổi cây DOM, không có lý do gì để chờ đợi chúng và ngừng phân tích cú pháp tài liệu. Tuy nhiên, có một vấn đề xảy ra đối với 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, tập lệnh sẽ nhận được câu trả lời sai và dường như điều này gây ra rất nhiều sự cố. Đây có vẻ là một trường hợp hiếm gặp nhưng 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 các tập lệnh khi chúng cố gắng 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 tờ mẫu chưa tải.
Xây dựng cây kết xuất
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 gồm các phần tử hình ảnh theo thứ tự hiển thị. Đây là bản trình bày bằng hình ảnh của giấy tờ. 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ạ hoặc đối tượng kết xuất.
Trình kết xuất đồ hoạ biết cách sắp xếp và vẽ chính nó cũng như các phần tử con.
Lớp RenderObject của WebKit, lớp cơ sở của trình kết xuất đồ hoạ, 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 biểu thị 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. Thẻ 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 chịu ảnh hưởng của trạng thái "hiển thị" giá trị 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 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ụ: 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 một trình kết xuất đặc biệt, phần tử đó sẽ ghi đè phương thức createRenderer()
.
Trình kết xuất trỏ đến những đối tượng tạo kiểu chứa thông tin không mang tính 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à 1 với 1. 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à "head" . Ngoài ra, các phần tử có giá trị 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ị "bị ẩn" 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 trực quan. Đây thường là các phần tử có cấu trúc phức tạp không thể mô tả được bằng một hình chữ nhật đơn lẻ. Ví dụ: thao tác "chọn" phần tử có ba trình kết xuất: 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 do chiều rộng không đủ cho một dòng, các dòng mới sẽ được thêm vào làm trình kết xuất bổ sung.
Một ví dụ khác về nhiều trình kết xuất đồ hoạ là HTML bị hỏng. Theo thông số CSS, một phần tử cùng dòng chỉ được chứa các phần tử chặn 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 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 ở cùng một vị trí trong cây. Số thực nổi và các phần tử 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ạ với khung thực. Khung phần giữ chỗ là vị trí của khung đó.
![Cây kết xuất và cây DOM tương ứng.](https://web.developers.google.cn/static/articles/howbrowserswork/image/the-render-tree-the-corr-f699894ef4c75.png?authuser=3&hl=vi)
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
, còn hàm khởi tạo phân giải kiểu (xem 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 có một phần "đí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 nút mới là "attach" .
Việc xử lý các thẻ html và 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 những gì mà thông số kỹ thuật CSS gọi 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 lớp này là khung nhìn: kích thước của khu vực hiển thị cửa sổ trình duyệt.
Firefox gọi hàm này là ViewPortFrame
và WebKit gọi hàm 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 các nút DOM.
Xem thông số kỹ thuật của 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 yêu cầu tính toán các thuộc tính hình ảnh của mỗi đố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 các thuộc tính kiểu của mỗi phần tử.
Kiểu này bao gồm các biểu định kiểu có nhiều nguồn gốc khác nhau, các phần tử kiểu cùng dòng và thuộc tính hình ảnh trong HTML (như thuộc tính "bgcolor").Sau đó, kiểu được chuyển đổi thành các thuộc tính kiểu CSS phù hợp.
Nguồn gốc của các biểu định kiểu là các biểu định kiểu mặc định của trình duyệt, các biểu định kiểu do tác giả trang và các biểu định kiểu của người dùng cung cấp - đây là các 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 các 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 một biểu định kiểu trong "Cấu hình Firefox" thư mục).
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 các vấn đề về bộ nhớ.
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á. 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 công việc khó khăn. Bộ chọn có thể có cấu trúc phức tạp 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 nhưng đã được chứng minh là không hiệu quả và cần 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 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 truyền tải cây nút lên để thấy rằng chỉ có hai div và quy tắc này không áp dụng. Sau đó, bạn cần phải thử các đường dẫn khác trong cây.Việc áp dụng các quy tắc liên quan đến các quy tắc phân tầng khá phức tạp nhằm xác định hệ thống phân cấp của các quy tắc.
Hãy xem các trình duyệt gặp phải những vấn đề này như thế nào:
Dữ liệu kiểu chia sẻ
Nút WebKit tham chiếu đến các đố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 đó là anh em cùng cấp hoặc anh em họ và:
- Các phần tử phải ở cùng trạng thái chuột (ví dụ: một phần tử không được ở trạng thái :hover trong khi phần tử còn lại thì không)
- Không phần tử nào không được có mã nhận dạng
- Tên thẻ phải khớp
- Các thuộc tính 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 của tiêu điểm phải khớp
- Bộ chọn thuộc tính không ảnh hưởng đến phần tử nào. Trong trường hợp bị ảnh hưởng, bộ chọn sẽ được định nghĩa là có bất kỳ phần tử nào sử dụng bộ chọn thuộc tính ở bất kỳ vị trí nào trong bộ chọn để khớp với nhau.
- Không được có thuộc tính kiểu cùng dòng trong các phần tử
- Không được có bộ chọn đồng cấp nào đang được sử dụng. WebCore chỉ gửi 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ó. Trong đó có 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 có liên quan của nó.
![Cây ngữ cảnh kiểu Firefox.](https://web.developers.google.cn/static/articles/howbrowserswork/image/firefox-style-context-tre-f578b75b74df7.png?authuser=3&hl=vi)
Ngữ cảnh kiểu chứa giá trị kết thúc. Các giá trị được tính toán bằng cách áp dụng tất cả các quy tắc so khớp theo đúng thứ tự và thực hiện các thao tác biến chúng từ giá trị logic thành giá trị cụ thể. Ví dụ: nếu giá trị logic là tỷ lệ phần trăm 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 cây quy tắc thực sự thông minh. API 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 các giá trị đó. Việc này cũng giúp tiết kiệm không gian lưu trữ.
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 một đường dẫn có mức độ ưu tiên cao hơn. Cây chứa tất cả đường dẫn cho các kết quả trùng khớp với quy tắ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 ngay từ đầu cho mỗi nút, nhưng mỗi khi cần tính toán một kiểu nút, đường dẫn đã tính 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](https://web.developers.google.cn/static/articles/howbrowserswork/image/computed-rule-tree-f874f412bbaf.png?authuser=3&hl=vi)
Giả sử chúng ta cần phải 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 thứ tự đúng) 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ờ chúng tôi sẽ có ít việc phải làm hơn.
Hãy xem cái cây giúp chúng ta làm việc như thế nào.
Phân chia thành các cấu trúc
Ngữ 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 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. Tài sản kế thừa là những thuộc tính mà trừ phi được phần tử xác định, sẽ được kế thừa từ phần tử mẹ. Các thuộc tính không kế thừa (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 giúp chúng ta bằng cách lưu toàn bộ cấu trúc (chứa giá trị cuối đã tính) 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 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 ta 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 để lấp đầy 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à di chuyển cây lên 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 cấu trúc đầy đủ và trỏ đến nó - đó 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 sẽ lưu phép tính giá trị cuối và bộ nhớ.
Nếu tìm thấy một phần định nghĩa, chúng ta sẽ di chuyển lên cây cho đến khi cấu trúc được lấp đầy.
Nếu chúng ta 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à "được kế thừa" loại, chúng ta sẽ trỏ đến cấu trúc 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ấu trúc. Nếu đó là 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 sự thêm giá trị, thì chúng ta cần thực hiện thêm một số tính toán để biến nút đó thành giá trị thực. Sau đó, chúng ta lưu kết quả vào bộ nhớ đệm trong nút cây để con có thể sử dụng.
Trong trường hợp một phần tử có một phần tử đồng cấp hoặc một phần tử khác 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 một ví dụ: 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á mọi thứ, 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 mặt.
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à quy tắc đó trỏ đến):
![Cây quy tắc](https://web.developers.google.cn/static/articles/howbrowserswork/image/the-rule-tree-23f05b0dac33f.png?authuser=3&hl=vi)
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):
![Cây ngữ cảnh.](https://web.developers.google.cn/static/articles/howbrowserswork/image/the-context-tree-771124b7cb80d.png?authuser=3&hl=vi)
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 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à khám phá rằng các quy tắc khớp với <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à đặt bối cảnh đó vào cây bối 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 lấp đầy các cấu trúc kiểu. Chúng ta sẽ bắt đầu bằng cách điền 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 cấu trúc đã lưu trong 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 mã này trên nút B, nút trên cùng đã 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 đã lưu vào bộ nhớ đệm. Vì màu có một thuộc tính nên chúng ta không cần đi lên cây để điền các thuộc tính khác. Chúng ta 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 bộ nhớ đệm trên nút này.
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ẽ so khớp 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 trước.
Vì chúng ta có các đồ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ỉ trỏ đến ngữ 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ừ cấp độ gốc, việc lưu vào bộ nhớ đệm sẽ được thực hiện trên cây ngữ cảnh (thuộc tính màu sắc thực sự được kế thừa, nhưng Firefox sẽ 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 tôi thêm các quy tắc cho phông chữ trong một đoạn:
p {font-family: Verdana; font size: 10px; font-weight: bold}
Khi đó, 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ẹ. Trường hợp này xảy ra nếu không có quy tắc phông chữ nào được chỉ định cho đoạn.
Trong WebKit, trình duyệt không có cây quy tắc, các khai báo phù hợp sẽ được truyền tải bốn lần. Các thuộc tính có mức độ ưu tiên cao không quan trọng đầu tiên sẽ được áp dụng (những thuộc tính nên được áp dụng trước vì các thuộc tính khác phụ thuộc vào các thuộc tính đó, chẳng hạn như hiển thị), sau đó là các thuộc tính có mức độ ưu tiên cao, rồi đến các quy tắc có mức độ ưu tiên bình thường không quan trọng, sau đó là các quy tắc quan trọng có mức độ ưu tiên thông thường. Điều này có nghĩa là những tài sản xuất hiện nhiều lần sẽ được phân giải theo đúng thứ tự tầng. Người chiến thắng cuối cùng.
Vì vậy, để tóm lại: việc 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) 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ự.
Sửa đổi các quy tắc để dễ dàng so khớp
Có một số nguồn cho quy tắc kiểu:
- 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}
- Thuộc tính kiểu cùng dòng như
html <p style="color: blue" />
- Các thuộc tính hình ảnh 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 dễ dàng được khớp với phần tử vì anh ấy sở hữu các thuộc tính kiểu và thuộc tính HTML có thể được ánh xạ bằng cách sử dụng phần tử làm khóa.
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ó, các quy tắc được điều chỉnh để người dùng dễ dàng truy cập 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 các sơ đồ băm theo bộ chọn. Có các bản đồ theo id, theo tên lớp, theo tên thẻ và bản đồ chung cho bất kỳ mục nào 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, thì quy tắc sẽ được thêm vào bản đồ mã, nếu là một lớp thì quy tắc sẽ được thêm vào bản đồ lớp, v.v.
Việc chỉnh sửa này giúp việc so khớp các quy tắc trở nên dễ dàng hơn nhiều. Không cần xem trong mọi khai báo: chúng tôi có thể trích xuất các quy tắc có liên quan cho một phần tử từ tệp ánh xạ. Việc tối ưu hoá này loại bỏ hơn 95% các quy tắc, do đó chúng thậm chí không cần được xem xét trong quá trình 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. Thẻ thứ hai vào bản đồ id và thẻ 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. Bản đồ lớp sẽ chứa một "lỗi" phím chứa quy tắc cho "p.error" đã được tìm thấy. Phần tử div sẽ có các quy tắc có liên quan trong bản đồ id (khoá là id) và bản đồ thẻ. Vì vậy, việc duy nhất còn lại là tìm ra những quy tắc nào được các khoá trích xuất thực sự phù hợp.
Ví dụ: nếu quy tắc cho div là:
table div {margin: 5px}
Khoá 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 ta, 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ự tầng trong 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 tổng quát hơn). Nếu thuộc tính không được xác định theo bất kỳ quy tắc đã khớ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 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 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 được gọi là "thác nước" đơn đặt hàng. Theo thông số kỹ thuật của CSS2, thứ tự bậc là (từ thấp đến cao):
- Khai báo trình duyệt
- Khai báo thông thường của người dùng
- Nội dung khai báo thông thường của tác giả
- Ghi nhật ký các nội dung khai báo quan trọng
- Khai báo quan trọng đối với người dùng
Các khai báo trong trình duyệt là í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 quy cách và sau đó là thứ tự được chỉ định. Các thuộc tính hình ảnh HTML được chuyển đổi thành các khai báo CSS trùng khớp . Chúng được coi là quy tắc 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 của CSS2 như sau:
- đếm 1 nếu khai báo từ đó là "kiểu" thuộc tính thay vì quy tắc có bộ chọn, 0 nếu không (= 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)
Kết hợp 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ố mà bạn cần sử dụng được xác định theo số lượng cao nhất mà bạn có ở một trong các danh mục.
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 khi a=17, bạn sẽ cần cơ số số gồm 17 chữ số. Tình huống sau này 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 sẽ được sắp xếp theo quy tắc phân tầng.
WebKit sử dụng tính năng sắp xếp bong bóng cho các 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 liệu tất cả các tờ mẫu cấp cao nhất (bao gồm cả @nhập) đã được tải hay chưa. Nếu kiểu không được tải đầy đủ khi đính kèm, phần giữ chỗ sẽ được sử dụng và kiểu này được đánh dấu trong tài liệu. Chúng sẽ được tính toán lại sau khi biểu định kiểu được tải.
Bố cục
Khi được tạo và thêm vào cây, trình kết xuất 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 luồng.
HTML sử dụng mô hình bố cục dựa trên luồng, có 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 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 xuyên suốt tài liệu. Có những trường hợp ngoại lệ, ví dụ như bảng HTML có thể yêu cầu nhiều lần 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 quá trình đệ quy. Tệp này bắt đầu ở 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 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 đồ 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 đồ hoạ đều có một "bố cục" hoặc "chỉnh sửa" thì mỗi trình kết xuất sẽ gọi phương thức bố cục của phần tử con cần bố cục.
Hệ thống bit bẩn
Để không tạo ra bố cục đầy đủ cho mọi thay đổi nhỏ, trình duyệt sẽ sử dụng "một chút bẩn" hệ thống. Trình kết xuất được thay đổi hoặc thêm vào sẽ tự đánh dấu và các thành phần con tương ứng là "bẩn": cần có bố cục.
Có hai cờ: "bẩn" và "trẻ em bẩn" điều đó có nghĩa là mặc dù bản thân trình kết xuất có thể ổn, nhưng trình kết xuất đó có ít nhất một thành phần con cần có bố cục.
Bố cục chung và bố cục tăng dần
Bạn có thể kích hoạt bố cục trên toàn bộ cây hiển thị – đây là bố cục "toàn cầu" của bạn. Điều này có thể xảy ra do:
- 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 cỡ chữ.
- Do thay đổi kích thước màn hình
Bố cục có thể tăng dần, chỉ có các trình kết xuất đồ hoạ sửa đổi được bố trí (điều này có thể gây ra một số thiệt hại và đòi hỏi phải có thêm bố cục).
Bố cục tăng dần được kích hoạt (không đồng bộ) khi trình kết xuất bị bẩn. Ví dụ: khi các 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 tăng dần.](https://web.developers.google.cn/static/articles/howbrowserswork/image/incremental-layout-da3da0a148135.png?authuser=3&hl=vi)
Bố cục không đồng bộ và đồng bộ
Bố cục tăng dần được thực hiện không đồng bộ. Firefox đưa "các lệnh chỉnh lại" vào hàng đợi cho bố cục gia tăng và trình lập lịch biểu kích hoạt thực thi hàng loạt các lệnh này. WebKit cũng có một bộ tính giờ thực thi một bố cục tăng dần – cây được truyền qua và "bẩn" trình kết xuất đồ hoạ được bố 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 một cách đồ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 thao tác "đổi kích thước" kích hoạt một bố cục hoặc khi vị trí trình kết xuất thay đổi(chứ không phải kích thước), kích thước kết xuất sẽ được lấy từ mộ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 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 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:
- Trình kết xuất mẹ xác định chiều rộng riêng.
- Cha mẹ sẽ kiểm soát các phần tử 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 bố cục chung hoặc vì một số lý do khác – chức năng này tính toán chiều cao của phần tử con.
- 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. Thành phần mẹ của trình kết xuất mẹ sẽ sử dụng chiều cao này.
- Đặt bit bẩn thành false.
Firefox sử dụng "trạng thái" đối tượng(nsHTMLReflowState) làm tham số cho bố cục (gọi là "reflow"). Trong số các giá trị khác, trạng thái 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 "chỉ số" object(nsHTMLReflowMetrics). Giá trị này sẽ chứa chiều cao đã tính của trình kết xuất.
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, kiểu "chiều rộng" của trình kết xuất thuộc tính, lề và đường viền.
Ví dụ: chiều rộng của div sau:
<div style="width: 30%"/>
Sẽ được tính bằng WebKit như sau(lớp RenderBox phương thức calcWidth):
- Chiều rộng vùng chứa là chiều rộng tối đa của vùng chứa availableWidth và 0. availableWidth trong trường hợp này là contentWidth được tính như sau:
clientWidth() - paddingLeft() - paddingRight()
clientWidth và clientHeight biểu thị phần bên trong của một đối tượng ngoại trừ đường viền và thanh cuộn.
Chiều rộng của phần tử là "chiều rộng" thuộc tính kiểu. Giá trị này sẽ được tính là một giá trị tuyệt đối bằng cách tính tỷ lệ phần trăm 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 thời điểm này, đây là tính toán của "chiều rộng ưa thích". Lúc này, chiều rộng tối thiểu và chiều rộng 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 sẽ đượ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.
Dấu ngắt dòng
Khi một trình kết xuất ở giữa bố cục quyết định rằng bố cục đó cần bị hỏng, trình kết xuất sẽ dừng lại và truyền đến thành phần mẹ của bố cục mà bố cục đó cần được phá vỡ. Thành phần mẹ tạo thêm các trình kết xuất đồ hoạ và gọi bố cục trên đó.
Hội họa
Trong giai đoạn tô màu, cây kết xuất được truyền tải và hàm " Paint()" 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 tạo điểm ảnh 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ể là toàn bộ (toàn bộ cây được vẽ) hoặc tăng dần. Trong trường hợp vẽ tăng dần, 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 hình chữ nhật trên màn hình. Điều này khiến hệ điều hành xem nó là "khu vực bẩn" và tạo một "sơn" sự kiệ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, trình kết xuất 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 những sự kiện này và uỷ quyền thông báo cho thư mục gốc kết xuất. Hệ thống sẽ di chuyển cây cho đến khi truy cập được trình kết xuất phù hợp. Tệp này sẽ tự vẽ lại (và thường là các phần tử con).
Thứ tự vẽ
CSS2 xác định thứ tự của quy trình sơn. Đây thực ra là thứ tự 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 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 kiểm tra 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 đồ hoạ phù hợp với hình chữ nhật, theo thứ tự vẽ phù hợp (nền của trình kết xuất, sau đó là đường viền, v.v.).
Bằng cách đó, chỉ cần di chuyển cây một lần để sơn lại thay vì nhiều lần - vẽ tất cả nền, sau đó đến tất cả hình ảnh, rồi tất cả đường viền, v.v.
Firefox tối ưu hoá quá trình này bằng cách không thêm các phần tử sẽ bị ẩn, như các phần tử 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 tô lại, WebKit lưu hình chữ nhật cũ dưới dạng bitmap. Sau đó, phương thức này chỉ vẽ delta giữa hình chữ nhật mới và hình chữ nhật cũ.
Thay đổi linh động
Các trình duyệt sẽ cố gắng thực hiện những hành động tối thiểu có thể để phản hồi một thay đổi. Vì vậy, việc thay đổi màu của một phần tử sẽ chỉ làm cho phần tử đó vẽ lại. Các thay đổi đối với vị trí của phần tử sẽ dẫn đến bố cục và tô màu lại của phần tử, các phần tử con và có thể là các phần tử đồng cấp. Thêm một nút DOM sẽ khiến bố cục và tô màu lại của nút này. Thay đổi lớn, chẳng hạn như tăng cỡ chữ của "html" sẽ gây ra vô hiệu hoá bộ nhớ đệm, 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 có đơn luồng. Hầu hết mọi thứ, 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 cho quy trình thẻ.
Một số luồng song song có thể thực hiện các thao tác mạng. 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. Đó là một vòng lặp vô hạn giúp quá trình này tiếp tục tồn tại. Thư viện 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 đó. Dưới đây là mã của 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 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 hiển thị nội dung.
Canvas là vô hạn cho mỗi kích thước của không gian nhưng 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 sẽ trong suốt nếu nằm trong một canvas khác, và sẽ được cung cấp màu do trình duyệt xác định nếu không nằm trong khung này.
Mô hình CSS Box
Mô hình hộp CSS mô tả các hộp hình chữ nhật được tạo cho các thành phần 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 tuỳ chọn.
![Mô hình hộp CSS2](https://web.developers.google.cn/static/articles/howbrowserswork/image/css2-box-model-9c2ab852d1fb4.jpg?authuser=3&hl=vi)
Mỗi nút tạo 0...n hộp như vậy.
Tất cả các phần tử đều có màn hình "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à nội tuyến nhưng biểu định kiểu 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 "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.
Sơ đồ định vị
Có 3 giao thức:
- Thông thường: đối tượng được định vị theo vị trí của đối tượng trong tài liệu. Tức 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 cũng như kích thước của nó
- Độ chính xác đơn: ban đầu đối tượng được bố trí như luồng bình thường, sau đó di chuyển sang trái hoặc sang phải hết mức 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 cây DOM
Sơ đồ định vị được đặt theo "vị trí" và "số thực có độ chính xác đơn" .
- tạo ra luồng bình thường
- tuyệt đối và cố định gây ra vị trí tuyệt đối
Ở vị trí 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 sơ đồ 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 theo:
- Loại hộp
- Kích thước hộp
- Sơ đồ định vị
- Thông tin bên ngoài như kích thước hình ảnh và kích thước màn hình
Loại hộp
Hộp chặn: 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.](https://web.developers.google.cn/static/articles/howbrowserswork/image/block-box-b5c0bff4a44d2.png?authuser=3&hl=vi)
Hộp cùng dòng: không có khối riêng nhưng nằm bên trong khối chứa.
![Hộp cùng dòng.](https://web.developers.google.cn/static/articles/howbrowserswork/image/inline-boxes-a9ff03002e7e2.png?authuser=3&hl=vi)
Các khối được định dạng lần lượt theo chiều dọc. Nội tuyến được định dạng theo chiều ngang.
![Định dạng khối và cùng dòng.](https://web.developers.google.cn/static/articles/howbrowserswork/image/block-inline-formatting-a8450c63cf457.png?authuser=3&hl=vi)
Hộp nội tuyến được đặt bên trong các dòng hoặc "hộp dòng". Các dòng này tối thiểu phải cao 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 tiêu chí "đườ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 điểm của một hộp khác sau đó so với phần dưới cùng. Nếu chiều rộng vùng chứa không đủ, các nội tuyến sẽ được đặt trên một vài dòng. Đây thường là những gì xảy ra trong một đoạn.
![Đường.](https://web.developers.google.cn/static/articles/howbrowserswork/image/lines-68c55a378a7ff.png?authuser=3&hl=vi)
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.](https://web.developers.google.cn/static/articles/howbrowserswork/image/relative-positioning-fceed0670b8c5.png?authuser=3&hl=vi)
Nổi
Một hộp nổi được dịch chuyển sang trái hoặc phải của một đường. Điểm thú vị là các ô khác sẽ 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.](https://web.developers.google.cn/static/articles/howbrowserswork/image/float-dd9b790210284.png?authuser=3&hl=vi)
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 là gì. Phần tử này không tham gia vào luồng thông thường. Phương diện tương ứng với vùng chứa. Trong giá trị cố định, vùng chứa là khung nhìn.
![Vị trí cố định.](https://web.developers.google.cn/static/articles/howbrowserswork/image/fixed-positioning-881933fcb1a2f.png?authuser=3&hl=vi)
Biểu diễn theo lớp
Thông tin này do thuộc tính CSS z-index chỉ định. Thứ nguyên này đại diện cho chiều thứ ba của hộp: vị trí của nó dọc theo "trục z".
Các hộp 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 nhóm, các phần tử phía sau sẽ được vẽ trước và các phần tử phía trước ở trên cùng, 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 z-index. Hộp có "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:
![Vị trí cố định.](https://web.developers.google.cn/static/articles/howbrowserswork/image/fixed-positioning-84a46b366dc9a.png?authuser=3&hl=vi)
Mặc dù div màu đỏ đứng trước màu xanh lục trong thẻ đá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, do đó, thuộc tính này sẽ chuyển tiếp trong ngăn xếp do hộp gốc giữ.
Tài nguyên
Cấu trúc của trình duyệt
- Gớm ghiếc, Alan. Cấu trúc tham chiếu cho trình duyệt web (pdf)
- Tiếng 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, Người biên dịch: Nguyên tắc, Kỹ thuật và Công cụ (còn gọi là "Cuốn sách về rồng"), Addison-Wesley, 1986
- Rick Jelliffe. The Bold and the Beautiful: hai bản nháp mới cho HTML 5.
Firefox
- L. David Baron, HTML và CSS nhanh hơn: Layout Engine nội bộ cho nhà phát triển web.
- L. David Baron, HTML and CSS nhanh hơn: Layout Engine nội bộ 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ụ Layout 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
- Chris Waterson, Tổng quan về tắc kè
- Alexander Larsson, Vòng đời của yêu cầu HTTP HTML
WebKit
- David Hyatt, Triển khai CSS(phần 1)
- David Hyatt, Tổng quan về WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, Vấn đề FOUC
Thông số kỹ thuật 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 hoạt động – Hậu trường về trình duyệt web hiện đại (ja) của @kosei
- ブラウザってどうやって動いてるの?(モダンWEBブラウザシーンの裏側 của @ikeike443 và @kiyoto01.
Bạn có thể xem bản dịch được lưu trữ bên ngoài của Tiếng Hàn và Tiếng Thổ Nhĩ Kỳ.
Cảm ơn mọi người!