首先们需要考虑到的情况有两种,一种是有多个根元素的,一种是只有一个根元素的。
我们的节点有两种类型,文本节点和正常节点,因此声明两个数据结构。
function Element(tagName) { this.tagName = tagName this.attr = {} this.childNodes = []}function Text(value) { this.value = value || ''}复制代码
目标:将元素建立起父子关系,因为真实的 DOM 结构就是父子关系,这里我一开始实践的时候,将 childNodes 属性的处理放在了 startTag token 中,还给 Element 增加了 isEnd 属性,实属愚蠢,不但复杂化了,而且还很难实现。仔细思考 DOM 结构,token 也是有顺序的,合理利用栈数据结构,这个问题就变的简单了,将 childNodes 处理放在 endTag 中处理。具体逻辑如下
如果是 startTag token,直接 push 一个新 element
如果是 endTag token,则表示当前节点处理完成,此时出栈一个节点,同时将该节点归入栈顶元素节点的 childNodes 属性,这里需要做个判断,如果出栈之后栈空了,表示整个节点处理完成,考虑到可能有平行元素,将元素 push 到 stacks。
如果是 attr token,直接写入栈顶元素的 attr 属性
如果是 text token,由于文本节点的特殊性,不存在有子节点、属性等,就认定为处理完成。这里需要做个判断,因为文本节点可能是根级别的,判断是否存在栈顶元素,如果存在直接压入栈顶元素的 childNodes 属性,不存在 push 到 stacks。
代码如下
function HTMLSyntacticalParser() { this.stack = [] this.stacks = []}HTMLSyntacticalParser.prototype.getOutPut = function() { return this.stacks}// 一开始搞复杂了,合理利用基本数据结构真是一件很酷炫的事HTMLSyntacticalParser.prototype.receiveInput = function(token) { var stack = this.stack if(token.type === 'startTag') { stack.push(new Element(token.value.substring(1))) } else if(token.type === 'attr') { var t = token.value.split('='), key = t[0], value = t[1].replace(/'|"/g, '') stack[stack.length - 1].attr[key] = value } else if(token.type === 'text') { if(stack.length) { stack[stack.length - 1].childNodes.push(new Text(token.value)) } else { this.stacks.push(new Text(token.value)) } } else if(token.type === 'endTag') { var parsedTag = stack.pop() if(stack.length) { stack[stack.length - 1].childNodes.push(parsedTag) } else { this.stacks.push(parsedTag) } }}复制代码
简单测试如下:
没啥大问题哈
解释执行
对于上述语法分析的结果,可以理解成 vdom 结构了,接下来就是映射成真实的 DOM,这里其实比较简单,用下递归即可,直接上代码吧
function vdomToDom(array) { var res = [] for(let item of array) { res.push(handleDom(item)) } return res}function handleDom(item) { if(item instanceof Element) { var element = document.createElement(item.tagName) for(let key in item.attr) { element.setAttribute(key, item.attr[key]) } if(item.childNodes.length) { for(let i = 0; i < item.childNodes.length; i++) { element.appendChild(handleDom(item.childNodes[i])) } } return element } else if(item instanceof Text) { return document.createTextNode(item.value) }}复制代码
实现函数
上面三步骤完成后,来到了最后一步,实现最开始提出的函数
function html(element, htmlString) { // parseHTML var syntacticalParser = new HTMLSyntacticalParser() var lexicalParser = new HTMLLexicalParser(htmlString, syntacticalParser.receiveInput.bind(syntacticalParser)) lexicalParser.parse() var dom = vdomToDom(syntacticalParser.getOutPut()) var fragment = document.createDocumentFragment() dom.forEach(item => { fragment.appendChild(item) }) element.appendChild(fragment)}复制代码
三个不同情况的测试用例简单测试下
html(document.getElementById('app'), '
测试并列元素的
测试并列元素的
')html(document.getElementById('app'), '测试
你好呀,我测试一下没有深层元素的
')html(document.getElementById('app'), '
测试一下嵌套很深的p的子元素
p同级别')复制代码
声明:简单测试下都没啥问题,本次实践的目的是对 DOM 这一块通过词法分析和语法分析生成 DOM Tree 有一个基本的认识,所以细节问题肯定还是存在很多的。
总结
其实在了解了原理之后,这一块代码写下来,并没有太大的难度,但却让我很兴奋,有两个成果吧
了解并初步实践了一下状态机
数据结构的魅力
本文地址:百科问答频道 https://www.neebe.cn/wenda/916276_2.html,易企推百科一个免费的知识分享平台,本站部分文章来网络分享,本着互联网分享的精神,如有涉及到您的权益,请联系我们删除,谢谢!