實戰中學習瀏覽器工作原理 — HTML 解析與 CSS 計算

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我是"},{"type":"text","marks":[{"type":"strong"}],"text":"三鑽"},{"type":"text","text":",一個在"},{"type":"text","marks":[{"type":"strong"}],"text":"《技術銀河》"},{"type":"text","text":"中等你們一起來終生漂泊學習。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"點贊是力量,關注是認可,評論是關愛!下期再見 👋!"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一部分我們完成了從 HTTP 發送 Request,到接收到 Response,並且把 Response 中的文本都解析出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一部分我們主要講解如何做 HTML 解析 和 CSS 計算這兩個部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/98/98bb960d57051aad896271950ca11e40.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據我們上部分列出的一個完整的瀏覽器架構的話,藍色背景的部分就是我們目前已經完成的流程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b34c995f506fd74ab4d6e4adb20b4706.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"HTML 解析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"HTML parse 模塊的文件拆分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"思路:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了方便文件管理,我們把 parser 單獨拆分到文件中"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"parser 接收 HTML 文本作爲參數,返回一棵 DOM 樹"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"加入 HTML Parser"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇文章中我們最後獲得了一個 "},{"type":"codeinline","content":[{"type":"text","text":"Response"}]},{"type":"text","text":" 對象"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們就考慮如何利用這個 "},{"type":"codeinline","content":[{"type":"text","text":"Response"}]},{"type":"text","text":" 中的 body 內容"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以我們應該從獲得 Response 之後,把 body 內容傳給 parser 中的 parseHTML 方法進行解析"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在真正的瀏覽器中,我們是應該逐段的傳給 parser 處理,然後逐段的返回"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲這裏我們的目標只是簡單實現瀏覽器工作的原理,所以我們只需要統一解析然後返回就好"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣我們更容易理解,代碼也更加清晰易懂"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:client.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 這個是 client.js\n\n // 1. 引入 parser.js\nconst parser = require('./parser.js');\n\n// ...\n//... 之前的代碼在此處忽略\n// ...\n\nlet response = await request.send();\n\n// 2. 在 `請求方法` 中,獲得 response 後加入 HTML 的解析代碼\nlet dom = parser.parseHTML(response.body);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 解析器\n * @filename parser.js\n * @author 三鑽\n * @version v1.0.0\n */\n\nmodule.exports.parseHTML = function (html) {\n console.log(html); // 這裏我們先 console.log 打印一下返回的 HTML 內容\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"用有效狀態機 (FSM) 實現 HTML的分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們用 FSM 來實現 HTML 的分析"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 HTML 標準中,已經規定了 HTML 的狀態"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的瀏覽器只挑選其中一部分狀態,完成一個最簡版本"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTML 標準裏面已經把整個狀態機中的狀態都設計好了,我們直接就看HTML標準中給我們設計好的狀態:https://html.spec.whatwg.org/multipage/,我們直接翻到 “Tokenization” 查看列出的狀態,這裏就是所有 HTML 的詞法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些同學在讀這個標準的時候會說 “我看不懂”,“我太難了”,“我看懵了”。其實我們看不懂是因爲這裏面的標準是寫給瀏覽器實現者去看的,但是用實現我們的瀏覽器的狀態機之後,我們就可以看懂了,而且發現這裏面寫的非常像我們的代碼。這個標準中寫的就是僞代碼。我們只需要把這裏面的僞代碼寫成真實代碼就可以了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 HTML 中有80個狀態,但是在我們這裏,因爲只需要走一遍瀏覽器工作的流程,我們就不一一實現了,我們在其中挑選一部分來實現即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們來初始化一下我們的 "},{"type":"codeinline","content":[{"type":"text","text":"parseHTML"}]},{"type":"text","text":" 的狀態機:(把上面的 "},{"type":"codeinline","content":[{"type":"text","text":"parser.js"}]},{"type":"text","text":" 的基礎上進行修改)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 解析器\n * @filename parser.js\n * @author 三鑽\n * @version v1.0.0\n */\n\nconst EOF = Symbol('EOF'); // EOF: end of file\n\nfunction data(char) {}\n\n/**\n * HTTP 解析\n * @param {string} html 文本\n */\nmodule.exports.parseHTML = function (html) {\n let state = data;\n for (let char of html) {\n state = state(char);\n }\n state = state(EOF);\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 上面的代碼中用了一個小技巧,因爲 HTML 最後是有一個文件終結的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 所有最後需要給他一個結束字符(重點是這裏用一個沒有特別意義的字符)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 我們這裏使用了 "},{"type":"codeinline","content":[{"type":"text","text":"Symbol"}]},{"type":"text","text":" 創建了一個 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 字符,代表 End of file (文件結束)"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"解析標籤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"HTML 有三種標籤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"開始標籤"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"結束標籤"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"自封閉標籤"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"思路:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要的標籤有:開始標籤,結束標籤和自封閉標籤"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這一步我們暫時忽略屬性"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 解析器\n * @filename parser.js\n * @author 三鑽\n * @version v1.0.0\n */\n\nconst EOF = Symbol('EOF'); // EOF: end of file\n\n/**\n * HTML 數據開始閱讀狀態\n * --------------------------------\n * 1. 如果找到 `` 就報錯\n * 3. 如果是結束符合,也是報錯\n * @param {*} char\n */\nfunction endTagOpen(char) {\n if (char.match(/^[a-zA-Z]$/)) {\n return tagName(char);\n } else if (char === '>') {\n // 報錯 —— 沒有結束標籤\n } else if (char === EOF) {\n // 報錯 —— 結束標籤不合法\n }\n}\n\n/**\n * 標籤名狀態\n * --------------------------------\n * 1. 如果 `\\t`(Tab符)、`\\n`(空格符)、`\\f`(禁止符)或者是空格,這裏就是屬性的開始\n * 2. 如果找到 `/` 就是自關閉標籤\n * 3. 如果是字母字符那還是標籤名\n * 4. 如果是 `>` 就是開始標籤結束\n * 5. 其他就是繼續尋找標籤名\n * @param {*} char\n */\nfunction tagName(char) {\n if (c.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '/') {\n return selfClosingStartTag;\n } else if (c.match(/^[a-zA-Z]$/)) {\n return tagName;\n } else if (char === '>') {\n return data;\n } else {\n return tagName;\n }\n}\n\n\n/**\n * 標籤屬性狀態\n * --------------------------------\n * 1. 如果遇到 `/` 就是自封閉標籤狀態\n * 2. 如果遇到字母就是屬性名\n * 3. 如果遇到 `>` 就是標籤結束\n * 4. 如果遇到 `=` 下來就是屬性值\n * 5. 其他情況繼續進入屬性抓取\n * @param {*} char\n */\nfunction beforeAttributeName(char) {\n if (char === '/') {\n return selfClosingStartTag;\n } else if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '>') {\n return data;\n } else if (char === '=') {\n return beforeAttributeName;\n } else {\n return beforeAttributeName;\n }\n}\n\n/**\n * 自封閉標籤狀態\n * --------------------------------\n * 1. 如果遇到 `>` 就是自封閉標籤結束\n * 2. 如果遇到 `EOF` 即使報錯\n * 3. 其他字符也是報錯\n * @param {*} char\n */\nfunction selfClosingStartTag(char) {\n if (char === '>') {\n return data;\n } else if (char === 'EOF') {\n } else {\n }\n}\n\n/**\n * HTTP 解析\n * @param {string} html 文本\n */\nmodule.exports.parseHTML = function (html) {\n let state = data;\n for (let char of html) {\n state = state(char);\n }\n state = state(EOF);\n};\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"創建元素"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在狀態機中,除了狀態遷移,我們還會加入業務邏輯"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在標籤結束狀態提交標籤 token"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"業務邏輯:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們需要建立一個 "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":" 來暫存當前的 Token(這裏我們是用於存放開始和結束標籤 token 的)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後建立一個 "},{"type":"codeinline","content":[{"type":"text","text":"emit()"}]},{"type":"text","text":" 方法來接收最後創建完畢的 Token(這裏後面會用逐個 Token 來創建 DOM 樹)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"HTML 數據開始狀態 —— data"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果找到的是 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":",那就直接 emit 一個 type: ‘EOF’ 的 Token"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果是文本內容的話,直接 emit "},{"type":"codeinline","content":[{"type":"text","text":"{type: 'text', content: char}"}]},{"type":"text","text":" 的 token"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"標籤開始狀態 —— tagOpen"},{"type":"text","text":" "}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果匹配中的是字母,那就是開始標籤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 直接記錄開始標籤 Token 對象 "},{"type":"codeinline","content":[{"type":"text","text":"{type: 'startTag, tagName: ''}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 在 "},{"type":"codeinline","content":[{"type":"text","text":"tagName()"}]},{"type":"text","text":" 狀態中我們會把整個完整的標籤名拼接好"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"標籤結束狀態 —— endTagOpen"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果匹配到字符,那就是結束標籤名"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 直接記錄結束標籤 Token 對象 "},{"type":"codeinline","content":[{"type":"text","text":"{type: 'endTag', tagName: ''}"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 雷同,後面會在 "},{"type":"codeinline","content":[{"type":"text","text":"tagName()"}]},{"type":"text","text":" 狀態中我們會把整個完整的標籤名拼接好"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"標籤名狀態 —— tagName"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏就是最核心的業務區了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 在第三種情況下,匹配到字母時,那就是需要拼接標籤名的時候"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏我們直接給 "},{"type":"codeinline","content":[{"type":"text","text":"currentTag"}]},{"type":"text","text":" 追加字母即可"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 當我們匹配到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 字符時,就是這個標籤結束的時候,這個時候我們已經擁有一個完整的標籤 Token了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 所以這裏我們直接把 "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":" emit 出去"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"標籤屬性狀態 —— beforeAttributeName"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 在匹配到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 字符的時候,這裏就是標籤結束的時候,所以可以 emit "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":" 的時候"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"自封閉標籤狀態 —— selfClosingStartTag"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏追加了一個邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 在匹配到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 字符時,就是自閉標籤結束的時候"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏我們直接給 "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":" 追加一個 "},{"type":"codeinline","content":[{"type":"text","text":"isSelfClosing = true"}]},{"type":"text","text":" 的狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 然後直接可以把 "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":" emit 出去了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 解析器\n * @filename parser.js\n * @author 三鑽\n * @version v1.0.0\n */\n\nlet currentToken = null;\n\n/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n console.log(token);\n}\n\nconst EOF = Symbol('EOF'); // EOF: end of file\n\n/**\n * HTML 數據開始閱讀狀態\n * --------------------------------\n * 1. 如果找到 `` 就報錯\n * 3. 如果是結束符合,也是報錯\n * @param {*} char\n */\nfunction endTagOpen(char) {\n if (char.match(/^[a-zA-Z]$/)) {\n currentToken = {\n type: 'endTag',\n tagName: '',\n };\n return tagName(char);\n } else if (char === '>') {\n // 報錯 —— 沒有結束標籤\n } else if (char === EOF) {\n // 報錯 —— 結束標籤不合法\n }\n}\n\n/**\n * 標籤名狀態\n * --------------------------------\n * 1. 如果 `\\t`(Tab符)、`\\n`(空格符)、`\\f`(禁止符)或者是空格,這裏就是屬性的開始\n * 2. 如果找到 `/` 就是自關閉標籤\n * 3. 如果是字母字符那還是標籤名\n * 4. 如果是 `>` 就是開始標籤結束\n * 5. 其他就是繼續尋找標籤名\n * @param {*} char\n */\nfunction tagName(char) {\n if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '/') {\n return selfClosingStartTag;\n } else if (char.match(/^[a-zA-Z]$/)) {\n currentToken.tagName += char;\n return tagName;\n } else if (char === '>') {\n emit(currentToken);\n return data;\n } else {\n return tagName;\n }\n}\n\n/**\n * 標籤屬性狀態\n * --------------------------------\n * 1. 如果遇到 `/` 就是自封閉標籤狀態\n * 2. 如果遇到字母就是屬性名\n * 3. 如果遇到 `>` 就是標籤結束\n * 4. 如果遇到 `=` 下來就是屬性值\n * 5. 其他情況繼續進入屬性抓取\n * @param {*} char\n */\nfunction beforeAttributeName(char) {\n if (char === '/') {\n return selfClosingStartTag;\n } else if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '>') {\n emit(currentToken);\n return data;\n } else if (char === '=') {\n return beforeAttributeName;\n } else {\n return beforeAttributeName;\n }\n}\n\n/**\n * 自封閉標籤狀態\n * --------------------------------\n * 1. 如果遇到 `>` 就是自封閉標籤結束\n * 2. 如果遇到 `EOF` 即使報錯\n * 3. 其他字符也是報錯\n * @param {*} char\n */\nfunction selfClosingStartTag(char) {\n if (char === '>') {\n currentToken.isSelfClosing = true;\n emit(currentToken);\n return data;\n } else if (char === 'EOF') {\n } else {\n }\n}\n\n/**\n * HTTP 解析\n * @param {string} html 文本\n */\nmodule.exports.parseHTML = function (html) {\n let state = data;\n for (let char of html) {\n state = state(char);\n }\n state = state(EOF);\n};\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"處理屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"屬性值分爲單引號、雙引號、無引號三種寫法,因此需要較多狀態處理"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"處理屬性的方式跟標籤類似"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"屬性結束時,我們把屬性加到標籤 Token 上"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"業務邏輯:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們需要定義一個 "},{"type":"codeinline","content":[{"type":"text","text":"currentAttribute"}]},{"type":"text","text":" 來存放當前找到的屬性"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後在裏面疊加屬性的名字和屬性值,都完成後再放入 "},{"type":"codeinline","content":[{"type":"text","text":"currrentToken"}]},{"type":"text","text":" 之中"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"標籤屬性名開始狀態 —— beforeAttributeName"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏如果遇到 空格,換行,回車等字符就可以再次進入標籤屬性名開始狀態,繼續等待屬性的字符"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到 "},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":"或者"},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":"就是標籤直接結束了,我們就可以進入屬性結束狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"="}]},{"type":"text","text":" 或者 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 這裏就有 HTML 語法錯誤,正常來說就會返回 "},{"type":"codeinline","content":[{"type":"text","text":"parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況的話,就是剛剛開始屬性名,這裏就可以創建新的 "},{"type":"codeinline","content":[{"type":"text","text":"currentAttribute"}]},{"type":"text","text":" 對象 "},{"type":"codeinline","content":[{"type":"text","text":"{name: '', value: ''}"}]},{"type":"text","text":",然後返回屬性名狀態"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"屬性名狀態 —— attributeName"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到空格、換行、回車、"},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 或者是 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":"等字符時,就可以判定這個屬性已經結束了,可以直接遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"afterAttributeName"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到一個 "},{"type":"codeinline","content":[{"type":"text","text":"="}]},{"type":"text","text":" 字符,證明我們的屬性名讀取完畢,下來就是屬性值了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到 "},{"type":"codeinline","content":[{"type":"text","text":"\\u0000"}]},{"type":"text","text":" 那就是解析錯誤,直接拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 最後所有其他的都是當前屬性名的字符,直接疊加到 "},{"type":"codeinline","content":[{"type":"text","text":"currentAttribute"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"name"}]},{"type":"text","text":" 值中,然後繼續進入屬性名狀態繼續讀取屬性名字符"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"屬性值開始狀態 —— beforeAttributeValue"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到空格、換行、回車、"},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 或者是 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":"等字符時,我們繼續往後尋找屬性值,所以繼續返回 "},{"type":"codeinline","content":[{"type":"text","text":"beforeAttributeValue"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"\""}]},{"type":"text","text":" 就是雙引號屬性值,進入 "},{"type":"codeinline","content":[{"type":"text","text":"doubleQuotedAttributeValue"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"'"}]},{"type":"text","text":" 就是單引號屬性值,進入 "},{"type":"codeinline","content":[{"type":"text","text":"singleQuotedAttributeValue"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況就是遇到沒有引號的屬性值,使用 "},{"type":"codeinline","content":[{"type":"text","text":"reconsume"}]},{"type":"text","text":" 的技巧進入 "},{"type":"codeinline","content":[{"type":"text","text":"unquotedAttributeValue(char)"}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"雙引號屬性值狀態 -- doubleQuotedAttributeValue"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這裏我們死等 "},{"type":"codeinline","content":[{"type":"text","text":"\""}]},{"type":"text","text":" 字符,到達這個字符證明這個屬性的名和值都讀取完畢,可以直接把這兩個值放入當前 Token 了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"\\u0000"}]},{"type":"text","text":" 或者 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 就是 HTML 語法錯誤,直接拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況就是繼續讀取屬性值,並且疊加到 "},{"type":"codeinline","content":[{"type":"text","text":"currentAttribute"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"value"}]},{"type":"text","text":" 中,然後繼續進入 "},{"type":"text","marks":[{"type":"strong"}],"text":"doubleQuotedAttributeValue"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"單引號屬性值狀態 —— singleQuotedAttributeValue"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 與雙引號雷同,這裏我們死等 "},{"type":"codeinline","content":[{"type":"text","text":"'"}]},{"type":"text","text":" 字符,到達這個字符證明這個屬性的名和值都讀取完畢,可以直接把這兩個值放入當前 Token 了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"\\u0000"}]},{"type":"text","text":" 或者 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 就是 HTML 語法錯誤,直接拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況就是繼續讀取屬性值,並且疊加到 "},{"type":"codeinline","content":[{"type":"text","text":"currentAttribute"}]},{"type":"text","text":" 的 "},{"type":"codeinline","content":[{"type":"text","text":"value"}]},{"type":"text","text":" 中,然後繼續進入 "},{"type":"text","marks":[{"type":"strong"}],"text":"singleQuotedAttributeValue"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"引號結束狀態 —— afterQuotedAttributeValue"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到空格、換行、回車等字符時,證明還有可能有屬性值,所以我們遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"beforeAttributeName"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 這個時候遇到一個 "},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":" 字符,因爲之前我們讀的是屬性,屬性都是在開始標籤中的,在開始標籤遇到 "},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":" ,那肯定是自封閉標籤了。所以這裏直接遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"selfClosingStartTag"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 字符,證明標籤要結束了,直接把當前組裝好的屬性名和值加入 "},{"type":"codeinline","content":[{"type":"text","text":"currentToken"}]},{"type":"text","text":", 然後直接 emit 出去"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 那就是 HTML 語法錯誤,拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況按照瀏覽器規範,這裏屬於屬性之間缺少空格的解析錯誤 (Parse error: missing-whitespace-between-attributes)"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"無引號屬性值狀態 —— unquotedAttributeValue"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到空格、換行、回車等字符時,證明屬性值結束,這個時候我們就可以直接把當前屬性加入 currentToken,然後還有可能有其他屬性,所以進入 "},{"type":"codeinline","content":[{"type":"text","text":"beforeAttributeName"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":" 證明標籤是一個自封閉標籤,先把當前屬性加入 currentToken 然後進入 "},{"type":"codeinline","content":[{"type":"text","text":"selfClosingStartTag"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 證明標籤正常結束了,先把當前屬性加入 currentToken 然後直接 emit token"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 遇到其他不合法字符都直接拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況就是還在讀取屬性值的字符,所以疊加當前字符到屬性值中,然後繼續回到 "},{"type":"codeinline","content":[{"type":"text","text":"unquotedAttributeValue"}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"屬性名結束狀態 —— afterAttributeName"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果我們遇到空格、換行、回車等字符時,證明還沒有找到結束字符,繼續尋找,所以重新進入 "},{"type":"codeinline","content":[{"type":"text","text":"afterAttributeName"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"/"}]},{"type":"text","text":" 證明這個標籤是自封閉標籤,直接遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"selfClosingStartTag"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"="}]},{"type":"text","text":" 字符證明下一個字符開始就是屬性值了,遷移到 "},{"type":"codeinline","content":[{"type":"text","text":"beforeAttributeValue"}]},{"type":"text","text":" 狀態"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":">"}]},{"type":"text","text":" 字符,證明標籤正常結束了,先把當前屬性加入 currentToken 然後直接 emit token"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 如果遇到 "},{"type":"codeinline","content":[{"type":"text","text":"EOF"}]},{"type":"text","text":" 證明HTML 文本異常結束了,直接拋出 "},{"type":"codeinline","content":[{"type":"text","text":"Parse error"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" + 其他情況下,屬於屬性名又開始了,所以把上一個屬性加入 currentToken 然後繼續記錄下一個屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件名:parser.js"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 解析器\n * @filename parser.js\n * @author 三鑽\n * @version v1.0.0\n */\n\nlet currentToken = null;\nlet currentAttribute = null;\n\n/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n console.log(token);\n}\n\nconst EOF = Symbol('EOF'); // EOF: end of file\n\n/**\n * HTML 數據開始閱讀狀態\n * --------------------------------\n * 1. 如果找到 `` 就報錯\n * 3. 如果是結束符合,也是報錯\n * @param {*} char\n */\nfunction endTagOpen(char) {\n if (char.match(/^[a-zA-Z]$/)) {\n currentToken = {\n type: 'endTag',\n tagName: '',\n };\n return tagName(char);\n } else if (char === '>') {\n // 報錯 —— 沒有結束標籤\n } else if (char === EOF) {\n // 報錯 —— 結束標籤不合法\n }\n}\n\n/**\n * 標籤名狀態\n * --------------------------------\n * 1. 如果 `\\t`(Tab符)、`\\n`(空格符)、`\\f`(禁止符)或者是空格,這裏就是屬性的開始\n * 2. 如果找到 `/` 就是自關閉標籤\n * 3. 如果是字母字符那還是標籤名\n * 4. 如果是 `>` 就是開始標籤結束\n * 5. 其他就是繼續尋找標籤名\n * @param {*} char\n */\nfunction tagName(char) {\n if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '/') {\n return selfClosingStartTag;\n } else if (char.match(/^[a-zA-Z]$/)) {\n currentToken.tagName += char;\n return tagName;\n } else if (char === '>') {\n emit(currentToken);\n return data;\n } else {\n return tagName;\n }\n}\n\n/**\n * 標籤屬性名開始狀態\n * --------------------------------\n * 1. 如果遇到 `/` 就是自封閉標籤狀態\n * 2. 如果遇到字母就是屬性名\n * 3. 如果遇到 `>` 就是標籤結束\n * 4. 如果遇到 `=` 下來就是屬性值\n * 5. 其他情況繼續進入屬性抓取\n * @param {*} char\n */\nfunction beforeAttributeName(char) {\n if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '/' || char === '>') {\n return afterAttributeName(char);\n } else if (char === '=' || char === EOF) {\n throw new Error('Parse error');\n } else {\n currentAttribute = {\n name: '',\n value: '',\n };\n return attributeName(char);\n }\n}\n\n/**\n * 屬性名狀態\n * @param {*} char\n */\nfunction attributeName(char) {\n if (char.match(/^[\\t\\n\\f ]$/) || char === '/' || char === '>' || char === EOF) {\n return afterAttributeName(char);\n } else if (char === '=') {\n return beforeAttributeValue;\n } else if (char === '\\u0000') {\n throw new Error('Parse error');\n } else {\n currentAttribute.name += char;\n return attributeName;\n }\n}\n\n/**\n * 屬性值開始狀態\n * @param {*} char\n */\nfunction beforeAttributeValue(char) {\n if (char.match(/^[\\t\\n\\f ]$/) || char === '/' || char === '>' || char === EOF) {\n return beforeAttributeValue;\n } else if (char === '\"') {\n return doubleQuotedAttributeValue;\n } else if (char === \"'\") {\n return singleQuotedAttributeValue;\n } else if (char === '>') {\n // return data;\n } else {\n return unquotedAttributeValue(char);\n }\n}\n\n/**\n * 雙引號屬性值狀態\n * @param {*} char\n */\nfunction doubleQuotedAttributeValue(char) {\n if (char === '\"') {\n currentToken[currentAttribute.name] = currentAttribute.value;\n return afterQuotedAttributeValue;\n } else if (char === '\\u0000') {\n throw new Error('Parse error');\n } else if (char === EOF) {\n throw new Error('Parse error');\n } else {\n currentAttribute.value += char;\n return doubleQuotedAttributeValue;\n }\n}\n\n/**\n * 單引號屬性值狀態\n * @param {*} char\n */\nfunction singleQuotedAttributeValue(char) {\n if (char === \"'\") {\n currentToken[currentAttribute.name] = currentAttribute.value;\n return afterQuotedAttributeValue;\n } else if (char === '\\u0000') {\n throw new Error('Parse error');\n } else if (char === EOF) {\n throw new Error('Parse error');\n } else {\n currentAttribute.value += char;\n return singleQuotedAttributeValue;\n }\n}\n\n/**\n * 引號結束狀態\n * @param {*} char\n */\nfunction afterQuotedAttributeValue(char) {\n if (char.match(/^[\\t\\n\\f ]$/)) {\n return beforeAttributeName;\n } else if (char === '/') {\n return selfClosingStartTag;\n } else if (char === '>') {\n currentToken[currentAttribute.name] = currentAttribute.value;\n emit(currentToken);\n return data;\n } else if (char === EOF) {\n throw new Error('Parse error: eof-in-tag');\n } else {\n throw new Error('Parse error: missing-whitespace-between-attributes');\n }\n}\n\n/**\n * 無引號屬性值狀態\n * @param {*} char\n */\nfunction unquotedAttributeValue(char) {\n if (char.match(/^[\\t\\n\\f ]$/)) {\n currentToken[currentAttribute.name] = currentAttribute.value;\n return beforeAttributeName;\n } else if (char === '/') {\n currentToken[currentAttribute.name] = currentAttribute.value;\n return selfClosingStartTag;\n } else if (char === '>') {\n currentToken[currentAttribute.name] = currentAttribute.value;\n emit(currentToken);\n return data;\n } else if (char === '\\u0000') {\n throw new Error('Parse error');\n } else if (char === '\"' || char === \"'\" || char === '') {\n currentToken[currentAttribute.name] = currentAttribute.value;\n emit(currentToken);\n return data;\n } else if (char === EOF) {\n throw new Error('Parse error');\n } else {\n currentToken[currentAttribute.name] = currentAttribute.value;\n currentAttribute = {\n name: '',\n value: '',\n };\n return attributeName(char);\n }\n}\n\n/**\n * 自封閉標籤狀態\n * --------------------------------\n * 1. 如果遇到 `>` 就是自封閉標籤結束\n * 2. 如果遇到 `EOF` 即使報錯\n * 3. 其他字符也是報錯\n * @param {*} char\n */\nfunction selfClosingStartTag(char) {\n if (char === '>') {\n currentToken.isSelfClosing = true;\n emit(currentToken);\n return data;\n } else if (char === 'EOF') {\n } else {\n }\n}\n\n/**\n * HTTP 解析\n * @param {string} html 文本\n */\nmodule.exports.parseHTML = function (html) {\n let state = data;\n for (let char of html) {\n state = state(char);\n }\n state = state(EOF);\n};\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"用 token 構建 DOM 樹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們開始語法分析,這個與複雜的 JavaScript 的語法相比就非常簡單,所以我們只需要用棧基於可以完成分析。但是如果我們要做一個完整的瀏覽器,只用棧肯定是不行的,因爲瀏覽器是有容錯性的,如果我們沒有編寫結束標籤的話,瀏覽器是會去爲我們補錯機制的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我做的這個簡單的瀏覽器就不需要對使用者做的那麼友好,而只對實現者做的更友好即可。所以我們在實現的過程中就不做那麼多特殊情況的處理了。簡單用一個棧實現瀏覽器的 HTML 語法解析,並且構建 一個 DOM 樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從標籤構建 DOM 樹的基本技巧是使用棧"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遇到開始標籤時創建元素併入棧,遇到結束標籤時出棧"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自封閉節點可視爲入棧後立刻出棧"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何元素的父元素是它入棧前的棧頂"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 中的 emit() 函數部分"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 默認給予根節點 document\nlet stack = [{ type: 'document', children: [] }];\n\n/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n if (token.type === 'text') return;\n\n // 記錄上一個元素 - 棧頂\n let top = stack[stack.length - 1];\n\n // 如果是開始標籤\n if (token.type == 'startTag') {\n let element = {\n type: 'element',\n children: [],\n attributes: [],\n };\n\n element.tagName = token.tagName;\n\n for (let prop in token) {\n if (prop !== 'type' && prop != 'tagName') {\n element.attributes.push({\n name: prop,\n value: token[prop],\n });\n }\n }\n\n // 對偶操作\n top.children.push(element);\n element.parent = top;\n\n if (!token.isSelfClosing) stack.push(element);\n\n currentTextNode = null;\n } else if (token.type == 'endTag') {\n if (top.tagName !== token.tagName) {\n throw new Error('Parse error: Tag start end not matched');\n } else {\n stack.pop();\n }\n\n currentTextNode = null;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":" 將文本節點加到 DOM 樹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏是 HTML 解析的最後一步,把文本節點合併後加入 DOM 樹裏面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文本節點與自封閉標籤處理類似"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多個文本節點需要合併"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 中的 emit() 函數部分"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"let currentToken = null;\nlet currentAttribute = null;\nlet currentTextNode = null;\n\n// 默認給予根節點 document\nlet stack = [{ type: 'document', children: [] }];\n\n/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n // 記錄上一個元素 - 棧頂\n let top = stack[stack.length - 1];\n\n // 如果是開始標籤\n if (token.type == 'startTag') {\n let element = {\n type: 'element',\n children: [],\n attributes: [],\n };\n\n element.tagName = token.tagName;\n\n for (let prop in token) {\n if (prop !== 'type' && prop != 'tagName') {\n element.attributes.push({\n name: prop,\n value: token[prop],\n });\n }\n }\n\n // 對偶操作\n top.children.push(element);\n element.parent = top;\n\n if (!token.isSelfClosing) stack.push(element);\n\n currentTextNode = null;\n } else if (token.type == 'endTag') {\n if (top.tagName !== token.tagName) {\n throw new Error('Parse error: Tag start end not matched');\n } else {\n stack.pop();\n }\n\n currentTextNode = null;\n } else if (token.type === 'text') {\n if (currentTextNode === null) {\n currentTextNode = {\n type: 'text',\n content: '',\n };\n top.children.push(currentTextNode);\n }\n\n currentTextNode.content += token.content;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b34c995f506fd74ab4d6e4adb20b4706.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"CSS 計算"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成 HTML 解析並且獲得了我們的 DOM 樹之後,我們可以通過 CSS 計算來生成帶 CSS 的 DOM 樹。CSS Computing 表示的就是我們 CSS 規則裏面所包含的那些 CSS 屬性,應用到匹配這些選擇器的元素上。 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開始這個代碼編寫之前,我們先來看看z在整個瀏覽器工作流程中,我們完成了哪些流程,到達了哪裏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/be/be7d64ed5ccbdc032ca0d337bf14a391.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的圖,我們看到 "},{"type":"codeinline","content":[{"type":"text","text":"藍色"}]},{"type":"text","text":" 部分就是已經完成的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇文章我們完成了 HTTP 請求"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後通過獲得的報文,解析出所有 HTTP 信息,裏面就包括了 HTML 內容"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後通過 HTTP 內容解析,我們構建了我們的 DOM 樹"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來就是 CSS 計算 (CSS Computing)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前的 DOM 樹只有我們的 HTML 語言裏面描述的那些語義信息,我們像完成渲染,我們需要 CSS 信息。 那有的同學就會說我們把所有的樣式寫到 style 裏面可不可以呢?如果我們這樣寫呢,我們就不需要經歷這個 CSS 計算的過程了。但是雖然我們只是做一個虛擬的瀏覽器,但是還是希望呈現一個比較完成的瀏覽器流程,所以我們還是會讓 DOM 樹參與 CSS 計算的過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以這裏我們就讓 DOM 樹掛上 CSS 信息,然後在渲染的過程中能使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在編寫這個代碼之前,我們需要準備一個環境。如果我們需要做 CSS 計算,我們就需要對 CSS 的語法與詞法進行分析。然後這個過程如果是手動來實現的話,是需要較多的編譯原理基礎知識的,但是這些編譯基礎知識的深度對我們知識想了解瀏覽器工作原理並不是重點。所以這裏我們就偷個懶,直接用 npm 上的一個"},{"type":"codeinline","content":[{"type":"text","text":"css"}]},{"type":"text","text":"現成包即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實這個 "},{"type":"codeinline","content":[{"type":"text","text":"css"}]},{"type":"text","text":" 包,就是一個 CSS parser,可以幫助我們完成 CSS 代碼轉譯成 AST 抽象語法樹。 我們所要做的就是根據這棵抽象語法樹抽出各種 CSS 規則,並且把他們運用到我們的 HTML 元素上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們第一步就是先拿到 CSS 的規則,所以叫做 “收集 CSS 規則”"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"收集 CSS 規則"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遇到 style 標籤時,我們把 CSS 規則保存起來"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"文件:parser.js 中的 emit() 函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 我們在 tagName === 'endTag' 的判斷中加入了判斷當前標籤是否 "},{"type":"codeinline","content":[{"type":"text","text":"style"}]},{"type":"text","text":" 標籤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 如果是,我們就可以獲取 "},{"type":"codeinline","content":[{"type":"text","text":"style"}]},{"type":"text","text":" 標籤裏面所有的內容進行 CSS 分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 這裏非常簡單我們加入一個 "},{"type":"codeinline","content":[{"type":"text","text":"addCSSRule(top.children[0].content)"}]},{"type":"text","text":"的函數即可"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 而,"},{"type":"codeinline","content":[{"type":"text","text":"top"}]},{"type":"text","text":" 就是當前元素,"},{"type":"codeinline","content":[{"type":"text","text":"children[0]"}]},{"type":"text","text":" 就是 text 元素,而 "},{"type":"codeinline","content":[{"type":"text","text":".content"}]},{"type":"text","text":" 就是所有的 CSS 規則文本"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 這裏我們需要注意一個點,我們忽略了在實際情況中還有 "},{"type":"codeinline","content":[{"type":"text","text":"link"}]},{"type":"text","text":" 標籤引入 CSS 文件的情況。但是這個過程涉及到多層異步請求和 HTML 解析的過程,爲了簡化我們的代碼的複雜度,這裏就不做這個實現了。當然實際的瀏覽器是會比我們做的虛擬瀏覽器複雜的多。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n // 記錄上一個元素 - 棧頂\n let top = stack[stack.length - 1];\n\n // 如果是開始標籤\n if (token.type == 'startTag') {\n // ............. 省略了這部分代碼 .....................\n } else if (token.type == 'endTag') {\n // 校驗開始標籤是否被結束\n // 不是:直接拋出錯誤,是:直接出棧\n if (top.tagName !== token.tagName) {\n throw new Error('Parse error: Tag start end not matched');\n } else {\n // 遇到 style 標籤時,執行添加 CSS 規則的操作\n if (top.tagName === 'style') {\n addCSSRule(top.children[0].content);\n }\n stack.pop();\n }\n\n currentTextNode = null;\n } else if (token.type === 'text') {\n // ............. 省略了這部分代碼 .....................\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們調用 CSS Parser 來分析 CSS 規則"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 中加入 addCSSRule() 函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 首先我們需要通過 node 引入 "},{"type":"codeinline","content":[{"type":"text","text":"css"}]},{"type":"text","text":" 包"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 然後調用 "},{"type":"codeinline","content":[{"type":"text","text":"css.parse(text)"}]},{"type":"text","text":" 獲得 AST 抽象語法樹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 最後通過使用 "},{"type":"codeinline","content":[{"type":"text","text":"..."}]},{"type":"text","text":" 的特性展開了 "},{"type":"codeinline","content":[{"type":"text","text":"ast.stylesheet.rules"}]},{"type":"text","text":" 中的所有對象,並且加入到 "},{"type":"codeinline","content":[{"type":"text","text":"rules"}]},{"type":"text","text":" 裏面"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"const css = require('css');\n\nlet rules = [];\n/**\n * 把 CSS 規則暫存到一個數字裏\n * @param {*} text\n */\nfunction addCSSRule(text) {\n var ast = css.parse(text);\n console.log(JSON.stringify(ast, null, ' '));\n rules.push(...ast.stylesheet.rules);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們必須要仔細研究此庫分析 CSS 規則的格式"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終 AST 輸出的結果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"type"}]},{"type":"text","text":" 類型是 "},{"type":"codeinline","content":[{"type":"text","text":"stylesheet"}]},{"type":"text","text":" 樣式表"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後在 "},{"type":"codeinline","content":[{"type":"text","text":"stylesheet"}]},{"type":"text","text":" 中有 "},{"type":"codeinline","content":[{"type":"text","text":"rules"}]},{"type":"text","text":" 的 CSS 規則數組"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"rules"}]},{"type":"text","text":" 數組中就有一個 "},{"type":"codeinline","content":[{"type":"text","text":"declarations"}]},{"type":"text","text":" 數組,這裏面就是我們 CSS 樣式的信息了"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拿第一個 delarations 來說明,他的屬性爲 "},{"type":"codeinline","content":[{"type":"text","text":"width"}]},{"type":"text","text":", 屬性值爲 "},{"type":"codeinline","content":[{"type":"text","text":"100px"}]},{"type":"text","text":",這些就是我們需要的 CSS 規則了"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n type: \"stylesheet\",\n stylesheet: {\n source: undefined,\n rules: [\n {\n type: \"rule\",\n selectors: [\n \"body div #myId\",\n ],\n declarations: [\n {\n type: \"declaration\",\n property: \"width\",\n value: \"100px\",\n position: {\n start: {\n line: 3,\n column: 9,\n },\n end: {\n line: 3,\n column: 21,\n },\n source: undefined,\n },\n },\n {\n type: \"declaration\",\n property: \"background-color\",\n value: \"#ff5000\",\n position: {\n start: {\n line: 4,\n column: 9,\n },\n end: {\n line: 4,\n column: 34,\n },\n source: undefined,\n },\n },\n ],\n position: {\n start: {\n line: 2,\n column: 7,\n },\n end: {\n line: 5,\n column: 8,\n },\n source: undefined,\n },\n },\n ],\n parsingErrors: [\n ],\n },\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏還有一個問題需要我們注意的,像 "},{"type":"codeinline","content":[{"type":"text","text":"body div #myId"}]},{"type":"text","text":" 這種帶有空格的標籤選擇器,是不會逐個給我們單獨分析出來的,所以這種我們是需要在後面自己逐個分解分析。除非是 "},{"type":"codeinline","content":[{"type":"text","text":","}]},{"type":"text","text":" 逗號分隔的選擇器纔會被拆解成多個 "},{"type":"codeinline","content":[{"type":"text","text":"delarations"}]},{"type":"text","text":"。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"添加調用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一步我們收集好了 CSS 規則,這一步我們就是要找一個合適的時機把這些規則應用上。應用的時機肯定是越早越好,CSS 設計裏面有一個潛規則,就是 CSS 設計會盡量保證所有的選擇器都能夠在 "},{"type":"codeinline","content":[{"type":"text","text":"startTag"}]},{"type":"text","text":" 進入的時候就能被判斷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,我們後面又加了一些高級的選擇器之後,這個規則有了一定的鬆動,但是大部分的規則仍然是去遵循這個規則的,當我們 DOM 樹構建到元素的 startTag 的步驟,就已經可以判斷出來它能匹配那些 CSS 規則了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們創建一個元素後,立即計算CSS"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們假設:理論上,當我們分析一個元素時,所有的 CSS 規則已經被收集完畢"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在真實瀏覽器中,可能遇到寫在 body 的 style 標籤,需要重新 CSS 計算的情況,這裏我們忽略"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"文件:parser.js 的 emit() 函數加入 computeCSS() 函數調用"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 輸出 HTML token\n * @param {*} token\n */\nfunction emit(token) {\n // 記錄上一個元素 - 棧頂\n let top = stack[stack.length - 1];\n\n // 如果是開始標籤\n if (token.type == 'startTag') {\n let element = {\n type: 'element',\n children: [],\n attributes: [],\n };\n\n element.tagName = token.tagName;\n\n // 疊加標籤屬性\n for (let prop in token) {\n if (prop !== 'type' && prop != 'tagName') {\n element.attributes.push({\n name: prop,\n value: token[prop],\n });\n }\n }\n\n // 元素構建好之後直接開始 CSS 計算\n computeCSS(element);\n\n // 對偶操作\n top.children.push(element);\n element.parent = top;\n // 自封閉標籤之外,其他都入棧\n if (!token.isSelfClosing) stack.push(element);\n\n currentTextNode = null;\n } else if (token.type == 'endTag') {\n // ............. 省略了這部分代碼 .....................\n } else if (token.type === 'text') {\n // ............. 省略了這部分代碼 .....................\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"文件:parser.js 中加入 computeCSS() 函數"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 對元素進行 CSS 計算\n * @param {*} element\n */\nfunction computeCSS(element) {\n console.log(rules);\n console.log('compute CSS for Element', element);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"獲取父元素序列"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼需要獲取父元素序列呢?因爲我們今天的選擇器大多數都是跟元素的父元素相關的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 computeCSS 函數中,我們必須知道元素的所有父級元素才能判斷元素與規則是否匹配"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們從上一步驟的 stack,可以獲取本元素的父元素"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲我們首先獲取的是 “當前元素”,所以我們獲得和計算父元素匹配的順序是從內向外"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"文件:parser.js 中的 computeCSS() 函數"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 因爲棧裏面的元素是會不斷的變化的,所以後期元素會在棧中發生變化,就會可能被污染。所以這裏我們用了一個"},{"type":"codeinline","content":[{"type":"text","text":"slice"}]},{"type":"text","text":"來複制這個元素。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ 然後我們用了 "},{"type":"codeinline","content":[{"type":"text","text":"reverse()"}]},{"type":"text","text":" 把元素的順序倒過來,爲什麼我們需要顛倒元素的順序呢?是因爲我們的標籤匹配是會從當前元素開始逐級的往外匹配(也就是一級一級往父級元素去匹配的) "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 對元素進行 CSS 計算\n * @param {*} element\n */\nfunction computeCSS(element) {\n var elements = stack.slice().reverse();\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"選擇器與元素的匹配"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們來了解一下選擇器的機構,其實選擇器其實是有一個層級結構的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最外層叫選擇器列表,這個我們的 CSS parser 已經幫我們做了拆分"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選擇器列表裏面的,叫做複雜選擇器,這個是由空格分隔了我們的複合選擇器"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複雜選擇器是根據親代關係,去選擇元素的"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複合選擇器,是針對一個元素的本身的屬性和特徵的判斷"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而複合原則性選擇器,它又是由緊連着的一對選擇器而構成的"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的模擬瀏覽器中,我們可以假設一個複雜選擇器中只包含簡單選擇器"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們就把這種情況當成而外有精力的同學自行去實現了哈"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"思路:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選擇器也要從當前元素向外排列"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"複雜選擇器拆成對單個元素的選擇器,用循環匹配父級元素隊列"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 匹配函數下一節會重點實現\n * @param {*} element\n * @param {*} selector\n */\nfunction match(element, selector) {}\n\n/**\n * 對元素進行 CSS 計算\n * @param {*} element\n */\nfunction computeCSS(element) {\n var elements = stack.slice().reverse();\n\n if (!elements.computedStyle) element.computedStyle = {};\n // 這裏循環 CSS 規則,讓規則與元素匹配\n // 1. 如果當前選擇器匹配不中當前元素直接 continue\n // 2. 當前元素匹配中了,就一直往外尋找父級元素找到能匹配上選擇器的元素\n // 3. 最後檢驗匹配中的元素是否等於選擇器的總數,是就是全部匹配了,不是就是不匹配\n for (let rule of rules) {\n let selectorParts = rule.selectors[0].split(' ').reverse();\n\n if (!match(element, selectorParts[0])) continue;\n\n let matched = false;\n\n let j = 1;\n for (let i = 0; i < elements.length; i++) {\n if (match(elements[i], selectorParts[j])) j++;\n }\n\n if (j >= selectorParts.length) matched = true;\n\n if (matched) console.log('Element', element, 'matched rule', rule);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"計算選擇器與元素"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一節我們沒有完成 "},{"type":"codeinline","content":[{"type":"text","text":"match"}]},{"type":"text","text":" 匹配函數的實現,那這一部分我們來一起實現元素與選擇器的匹配邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據選擇器的類型和元素屬性,計算是否與當前元素匹配"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏僅僅實現了三種基本選擇器,實際的瀏覽器中要處理複合選擇器 "}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同學們可以自己嘗試一下實現複合選擇器,實現支持空格的 Class 選擇器"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 匹配元素和選擇器\n * @param {Object} element 當前元素\n * @param {String} selector CSS 選擇器\n */\nfunction match(element, selector) {\n if (!selector || !element.attributes) return false;\n\n if (selector.charAt(0) === '#') {\n let attr = element.attributes.filter(attr => attr.name === 'id')[0];\n if (attr && attr.value === selector.replace('#', '')) return true;\n } else if (selector.charAt(0) === '.') {\n let attr = element.attributes.filter(attr => attr.name === 'class')[0];\n if (attr && attr.value === selector.replace('.', '')) return true;\n } else {\n if (element.tagName === selector) return true;\n }\n\n return false;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":" 生成 computed 屬性"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一部分我們生成 computed 屬性,這裏我們只需要把 "},{"type":"codeinline","content":[{"type":"text","text":"delarations"}]},{"type":"text","text":" 裏面聲明的屬性給他加到我們的元素的 "},{"type":"codeinline","content":[{"type":"text","text":"computed"}]},{"type":"text","text":" 上就可以了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一旦選擇器匹配中了,就把選擇器中的屬性應用到元素上"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後形成 computedStyle"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 對元素進行 CSS 計算\n * @param {*} element\n */\nfunction computeCSS(element) {\n var elements = stack.slice().reverse();\n\n if (!elements.computedStyle) element.computedStyle = {};\n // 這裏循環 CSS 規則,讓規則與元素匹配\n // 1. 如果當前選擇器匹配不中當前元素直接 continue\n // 2. 當前元素匹配中了,就一直往外尋找父級元素找到能匹配上選擇器的元素\n // 3. 最後檢驗匹配中的元素是否等於選擇器的總數,是就是全部匹配了,不是就是不匹配\n for (let rule of rules) {\n let selectorParts = rule.selectors[0].split(' ').reverse();\n\n if (!match(element, selectorParts[0])) continue;\n\n let matched = false;\n\n let j = 1;\n for (let i = 0; i < elements.length; i++) {\n if (match(elements[i], selectorParts[j])) j++;\n }\n\n if (j >= selectorParts.length) matched = true;\n\n if (matched) {\n let computedStyle = element.computedStyle;\n for (let declaration of rule.declarations) {\n if (!computedStyle[declaration.property]) computedStyle[declaration.property] = {};\n computedStyle[declaration.property].value = declaration.value;\n }\n console.log(computedStyle);\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看完代碼的同學,或者自己去實現這個代碼時候的同學,應該會發現這個代碼中有一個問題。如果我們回去看看我們的 HTML 代碼中的 style 樣式表,我們發現 HTML 中的 "},{"type":"codeinline","content":[{"type":"text","text":"img"}]},{"type":"text","text":" 標籤會被兩個 CSS 選擇器匹配中,分別是 "},{"type":"codeinline","content":[{"type":"text","text":"body div #myId"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"body div img"}]},{"type":"text","text":"。這樣就會導致前面匹配中後加入 "},{"type":"codeinline","content":[{"type":"text","text":"computedStyle"}]},{"type":"text","text":" 的屬性值會被後面匹配中的屬性值所覆蓋。但是根據 CSS 中的權重規則,ID選擇器是高於標籤選擇器的。這個問題我們下一部分會和同學們一起解決掉哦。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":" Specificity 的計算邏輯"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一節的代碼中,我們只是把匹配中的選擇器中的屬性直接覆蓋上一個,但是其實在 CSS 裏面是有一個 "},{"type":"codeinline","content":[{"type":"text","text":"specification"}]},{"type":"text","text":" 的規定。"},{"type":"codeinline","content":[{"type":"text","text":"specification"}]},{"type":"text","text":" 翻譯成中文,很多時候都會被翻譯成 "},{"type":"codeinline","content":[{"type":"text","text":"優先級"}]},{"type":"text","text":",當然在理論上是對的,但是在英文中呢,優先級是 "},{"type":"codeinline","content":[{"type":"text","text":"priority"}]},{"type":"text","text":",所以 "},{"type":"codeinline","content":[{"type":"text","text":"specificity"}]},{"type":"text","text":" 是 "},{"type":"codeinline","content":[{"type":"text","text":"專指程度"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"放在 CSS 中理解就是,ID 選擇器中的專指度是會比 CLASS 選擇器的高,所以 CSS 中的 "},{"type":"text","marks":[{"type":"strong"}],"text":"ID 的屬性會覆蓋 CLASS 的屬性"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好我們先來理解一下 "},{"type":"codeinline","content":[{"type":"text","text":"specification"}]},{"type":"text","text":" 是什麼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先 "},{"type":"codeinline","content":[{"type":"text","text":"specifity"}]},{"type":"text","text":" 會有四個元素"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"按照 CSS 中優先級的順序來說就是 inline style > id > class > tag"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以把這個生成爲 "},{"type":"codeinline","content":[{"type":"text","text":"specificity"}]},{"type":"text","text":" 就是 "},{"type":"codeinline","content":[{"type":"text","text":"[0, 0, 0, 0]"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數組裏面每一個數字都是代表在樣式表中出現的次數"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們用一些例子來分析一下,我們應該如何用 "},{"type":"codeinline","content":[{"type":"text","text":"specificity"}]},{"type":"text","text":" 來分辨優先級的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"A組選擇器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"A 選擇器:"},{"type":"codeinline","content":[{"type":"text","text":"div div #id"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"A 的 "},{"type":"codeinline","content":[{"type":"text","text":"specification"}]},{"type":"text","text":" :[0, 1, 0, 2]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ id 出現了一次,所以第二位數字是 "},{"type":"codeinline","content":[{"type":"text","text":"1"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ div tag 出現了兩次,所以第四位數是 "},{"type":"codeinline","content":[{"type":"text","text":"2"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"B組選擇器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"B 選擇器:"},{"type":"codeinline","content":[{"type":"text","text":"div #my #id"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"B 的 "},{"type":"codeinline","content":[{"type":"text","text":"specification"}]},{"type":"text","text":":[0, 2, 0, 1]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ id 出現了兩次,所以第二位數字是 "},{"type":"codeinline","content":[{"type":"text","text":"2"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"+ div tag 出現了一次,所以第四位數是 "},{"type":"codeinline","content":[{"type":"text","text":"1"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好,那麼我們怎麼去比較上面的兩種選擇器,那個更大呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要從左到右開始比對;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"遇到同位置的數值一樣的,就可以直接跳過;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"直到我們找到一對數值是有不一樣的,這個時候就看是哪個選擇器中的數值更大,那個選擇器的優先級就更高;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"只要有一對比對出大小後,後面的就不需要再比對了。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用上面 A 和 B 兩種選擇器來做對比的話,第一對兩個都是 "},{"type":"codeinline","content":[{"type":"text","text":"0"}]},{"type":"text","text":",所以可以直接跳過。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後第二位數值對,A選擇器是 "},{"type":"codeinline","content":[{"type":"text","text":"1"}]},{"type":"text","text":",B選擇器是 "},{"type":"codeinline","content":[{"type":"text","text":"2"}]},{"type":"text","text":",很明顯 B 要比 A 大,所以 B 選擇器中的屬性就要覆蓋 A 的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到這裏同學們應該都明白 CSS 中 "},{"type":"codeinline","content":[{"type":"text","text":"specificity"}]},{"type":"text","text":" 的規則和對比原理了,下來我們一起來看看如何實現這個代碼邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CSS 規則根據 specificity 和後來優先規則覆蓋"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"specificity 是個四元組,越左邊權重越高"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 CSS 規則的 specificity 根據包含的簡單選擇器相加而成"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 中添加一個 "},{"type":"codeinline","content":[{"type":"text","text":"specificity"}]},{"type":"text","text":" 函數,來計算一個選擇器的 specificity"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 計算選擇器的 specificity\n * @param {*} selector\n */\nfunction specificity(selector) {\n let p = [0, 0, 0, 0];\n let selectorParts = selector.split(' ');\n for (let part of selectorParts) {\n if (part.charAt(0) === '#') {\n p[1] += 1;\n } else if (part.charAt(0) === '.') {\n p[2] += 1;\n } else {\n p[3] += 1;\n }\n }\n return p;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 添加一個 "},{"type":"codeinline","content":[{"type":"text","text":"compare"}]},{"type":"text","text":" 函數,來對比兩個選擇器的 specificity"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 對比兩個選擇器的 specificity\n * @param {*} sp1\n * @param {*} sp2\n */\nfunction compare(sp1, sp2) {\n for (let i = 0; i <= 3; i++) {\n if (i === 3) return sp1[3] - sp2[3];\n if (sp1[i] - sp2[i]) return sp1[i] - sp2[i];\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件:parser.js 的 "},{"type":"codeinline","content":[{"type":"text","text":"computeCSS"}]},{"type":"text","text":" 中修改匹配中元素後的屬性賦值邏輯"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * 對元素進行 CSS 計算\n * @param {*} element\n */\nfunction computeCSS(element) {\n var elements = stack.slice().reverse();\n\n if (!elements.computedStyle) element.computedStyle = {};\n // 這裏循環 CSS 規則,讓規則與元素匹配\n // 1. 如果當前選擇器匹配不中當前元素直接 continue\n // 2. 當前元素匹配中了,就一直往外尋找父級元素找到能匹配上選擇器的元素\n // 3. 最後檢驗匹配中的元素是否等於選擇器的總數,是就是全部匹配了,不是就是不匹配\n for (let rule of rules) {\n let selectorParts = rule.selectors[0].split(' ').reverse();\n\n if (!match(element, selectorParts[0])) continue;\n\n let matched = false;\n\n let j = 1;\n for (let i = 0; i < elements.length; i++) {\n if (match(elements[i], selectorParts[j])) j++;\n }\n\n if (j >= selectorParts.length) matched = true;\n\n if (matched) {\n let sp = specificity(rule.selectors[0]);\n let computedStyle = element.computedStyle;\n for (let declaration of rule.declarations) {\n if (!computedStyle[declaration.property]) computedStyle[declaration.property] = {};\n\n if (!computedStyle[declaration.property].specificity) {\n computedStyle[declaration.property].value = declaration.value;\n computedStyle[declaration.property].specificity = sp;\n } else if (compare(computedStyle[declaration.property].specificity, sp) < 0) {\n computedStyle[declaration.property].value = declaration.value;\n computedStyle[declaration.property].specificity = sp;\n }\n }\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b34c995f506fd74ab4d6e4adb20b4706.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"最後"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們這裏就完成了瀏覽器工作原理中的 HTML 解析和 CSS 計算。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一篇文章我們來一起完成排版和渲染兩個瀏覽器過程。敬請期待!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b34c995f506fd74ab4d6e4adb20b4706.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/32/325465f9600893ac4bac99e6b756f096.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章