浏览器是怎么解析代码的:
HTML 的解构不算复杂,我们日常开发 90% 的“词”(指编译原理的术语 token,表示最小的有意义的单元),种类大约只有标签开始、属性、标签结束、注释、CDATA 节点几种。
麻烦主要麻烦在 HTML 跟 SGML 的千丝万缕的联系,解析代码的时候还要兼容 “<?” 和 “<%” ,报错了也不能影响页面的展现。
- token 是怎么被划分的
<!-- 代码注释 -->
<p class="myP">xhzy</p>
这个标签会被拆分成:
token | 说明 |
---|---|
< !-- 代码注释 --> | 注释 |
<p | <p 标签的开始 |
class="myP" | 属性 |
> | <p 标签的结束 |
xhzy | 文本节点 |
</p> | 结束标签 |
详细步骤:
➀ 浏览器要从 http 请求里拿到的字符流里读取字符,首先接收到 “<” ,浏览器就排除文本节点的可能。
➁ 再读一个字符为 p ,浏览器就会知道这不是注释和 CDATA,如果不是 p 便签是 span 等多字符组成的便签浏览器会一直读直到遇到空格或 “>”,就能完整的读到一个标签开始的 token 。
➂ 浏览器根据提前写好的规则读取出其他属性或文本节点的内容。
实际上,浏览器每读一个字符都要做一次决策,都和 ”当前状态“ 有关。这就要引入状态机的概念。可以参考官方文档,官方文档规定了80个状态机,根据字符的特性判断它的状态。
因为浏览器用状态机做词法分析,所以浏览器会把每个 token 的特征标识拆成独立的状态,然后再把所有词的特征标识链合并起来,形成一个连通图解构。
简单实现一个获取初识状态机的函数编写,假设我们把浏览器读到的第一个字符当作参数存进 beginState:
const beginState = content => {
if (content === '&') {
return characterReference;
}
if (content === '<') {
return tagOpen;
}
else if (content === '>') {
return tagEnd;
}
else if (.....
}
执行完 beginState 函数会获取到初始状态机,后续字符会带着当前的状态机(beginState 返回的 state)做对应的词法分析抛出相应的 token ,一层层递进下去直到标签关闭,循环重复执行这个流程直到分析到字符流最后一个字符,就会把字符流拆成 一个个 token 了。
构建 DOM 树
要把上一个步骤获取到的 token 转换成 DOM 树,这个过程是通过栈来实现的。模拟构建 DOM 树的方法:
const buildDOMTree = () => {
const stack = [];
const transformInput = token => {
/**
* 构建DOM树的算法
*/
}
const getRoot = () => {
return stack[0];
}
}
上述的 transformInput 方法 token 入参是词法分析抛出的 token,可以放到词法分析抛出 token 的时候调用,边做词法分析边构造 DOM 树。
构造 dom 树算法的流程为:
- 当前节点 push 进栈顶;
- 遇到属性,就添加到当前节点;
- 遇到文本节点,如果当前节点是文本节点,则跟文本节点合并,否则入栈成为当前节点的子节点;
- 遇到注释节点,作为当前节点的子节点;
- 遇到 tag start 就入栈一个节点,当前节点就是这个节点的父节点;
- 遇到 tag end 就出栈一个节点(还可以检查是否匹配)。
上述规则都是在代码正确的情况下工作的,当代码不正确的时候浏览器还能很好的容错,这其中的规则相当复杂,不过 W3C 已经帮我们整理好了全部规则
END......我是个有底线的家伙......END
网友评论