都知道浏览器和服务端是通过 HTTP 协议进行数据传输的,而 HTTP 协议又是纯文本协议,那么浏览器在得到服务端传输过来的 HTML 字符串,是如何解析成真实的 DOM 元素的呢,也就是我们常说的生成 DOM Tree,最近了解到状态机这样一个概念,于是就萌生一个想法,实现一个 innerHTML 功能的函数,也算是小小的实践一下。
函数原型
我们实现一个如下的函数,参数是 DOM 元素和 HTML 字符串,将 HTML 字符串转换成真实的 DOM 元素且 append 在参数一传入的 DOM 元素中。
function html(element, htmlString) { // 1. 词法分析 // 2. 语法分析 // 3. 解释执行 }
在上面的注释我已经注明,这个步骤我们分成三个部分,分别是词法分析、语法分析和解释执行。
词法分析
词法分析是特别重要且核心的一部分,具体任务就是:把字符流变成 token 流。
词法分析通常有两种方案,一种是状态机,一种是正则表达式,它们是等效的,选择你喜欢的就好。我们这里选择状态机。
首先我们需要确定 token 种类,我们这里不考虑太复杂的情况,因为我们只对原理进行学习,不可能像浏览器那样有强大的容错能力。除了不考虑容错之外,对于自闭合节点、注释、CDATA 节点暂时均不做考虑。
接下来步入主题,假设我们有如下节点信息,我们会分出哪些 token 来呢。
<p data="js">测试元素</p>
对于上述节点信息,我们可以拆分出如下 token
开始标签:<p
属性标签:class="a"
文本节点:测试元素
结束标签:</p>
状态机的原理,将整个 HTML 字符串进行遍历,每次读取一个字符,都要进行一次决策(下一个字符处于哪个状态),而且这个决策是和当前状态有关的,这样一来,读取的过程就会得到一个又一个完整的 token,记录到我们最终需要的 tokens 中。
万事开头难,我们首先要确定起初可能处于哪种状态,也就是确定一个 start 函数,在这之前,对词法分析类进行简单的封装,具体如下
function HTMLLexicalParser(htmlString, tokenHandler) { this.token = []; this.tokens = []; this.htmlString = htmlString this.tokenHandler = tokenHandler }
简单解释下上面的每个属性
token:token 的每个字符
tokens:存储一个个已经得到的 token
htmlString:待处理字符串
tokenHandler:token 处理函数,我们每得到一个 token 时,就已经可以进行流式解析
我们可以很容易的知道,字符串要么以普通文本开头,要么以 < 开头,因此 start 代码如下
HTMLLexicalParser.prototype.start = function(c) { if(c === '<') { this.token.push(c) return this.tagState } else { return this.textState(c) } }
start 处理的比较简单,如果是 < 字符,表示开始标签或结束标签,因此我们需要下一个字符信息才能确定到底是哪一类 token,所以返回 tagState 函数去进行再判断,否则我们就认为是文本节点,返回文本状态函数。
接下来分别展开 tagState 和 textState 函数。 tagState 根据下一个字符,判断进入开始标签状态还是结束标签状态,如果是 / 表示是结束标签,否则是开始标签, textState 用来处理每一个文本节点字符,遇到 < 表示得到一个完整的文本节点 token,代码如下
HTMLLexicalParser.prototype.tagState = function(c) { this.token.push(c) if(c === 'https://www.jb51.net/') { return this.endTagState } else { return this.startTagState } } HTMLLexicalParser.prototype.textState = function(c) { if(c === '<') { this.emitToken('text', this.token.join('')) this.token = [] return this.start(c) } else { this.token.push(c) return this.textState } }
这里初次见面的函数是 emitToken 、 startTagState 和 endTagState 。
emitToken 用来将产生的完整 token 存储在 tokens 中,参数是 token 类型和值。
startTagState 用来处理开始标签,这里有三种情形
如果接下来的字符是字母,则认定依旧处于开始标签态
遇到空格,则认定开始标签态结束,接下来是处理属性了
遇到>,同样认定为开始标签态结束,但接下来是处理新的节点信息
endTagState用来处理结束标签,结束标签不存在属性,因此只有两种情形
如果接下来的字符是字母,则认定依旧处于结束标签态
遇到>,同样认定为结束标签态结束,但接下来是处理新的节点信息
逻辑上面说的比较清楚了,代码也比较简单,看看就好啦
HTMLLexicalParser.prototype.emitToken = function(type, value) { var res = { type, value } this.tokens.push(res) // 流式处理 this.tokenHandler && this.tokenHandler(res) }