记一道算法题解题思路,字符串解析成DOM树

745 阅读5分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

写作背景

一个小伙伴给的一道题, 把下列字符串解析成一个类似DOM树的存在;

应该是字符串的,但是为了获得高亮方便阅读,我就直接用标签了


<el-tabs v-model="activeName" type="card">
   	<el-tab-pane label="图" name="view">
     	<div class="code">
     		<input />
     	</div>
   	</el-tab-pane>
   	<el-tab-pane label="JSON" name="sourceCode">
     	<div class="code">
       		<json-viewer :value="jsonValue" copyable boxed sort class="json-editor" ></json-viewer>
     	</div>
   	</el-tab-pane>
 </el-tabs>
 
 <!-- 解析结果 -->
<script>
{
  name: 'el-tabs', 
  children: [
    {
      name: 'el-tab-pane', 
      children: []
    }
  ]
}
</script>

解题思路

这一段文字可能会有点点枯燥,但是我最真实的想法, 如果你暂时不想看没关系,可以看完全篇之后,想深入理解如果找到思路,可以回来看看我的脑回路。


在我看完这道题之后,我转手就艾特了小群里另一位小伙伴, 问他有没有什么思路, 他上来就是一句:“这不是虚拟DOM吗? 我以前用堆栈实现过。”

本来是想直接看看他的代码的, 但是并没有,他找不着也忘记怎么写了。(不然也没这篇文章了)

然后我就带着这道题是上班了,经过下午的汗撒工位之后,啥也没做,因为上班忙啊。

不过在下班回家的路上, 我就在思考这道题了,小伙伴提到过用堆栈,这不失为一个解题方法,但是我对这种解法来解这类题不是很擅长。

想挑战下的小伙伴可以自己试试用堆栈实现


之后我又想到了正则匹配,正则匹配之后,用正则的match方法可以获取所有匹配到的字符串,然后结合堆栈来实现。

但是这方面的练习做的也少,脑子有点短路想不通。


最后的最后我想到了我之前写的一篇关于字符串解析成标题树的题,也是他提的,好像可以借助那道题的思路。

最后嘛,也是不了了之,因为还是有点差距的。


所以在最后,我想到了一个很黄很暴力的解法,遍历+正则+堆栈,来实现,不过最终代码中,堆栈没用上,就删掉了。

开始写了。

大概的思路有了以后,就可以了,毕竟我还做不到纯靠脑补就完成所有的逻辑。

需要实践来验错。

一、创建基础结构

这一块主要是参考了下之前标题树那道题的结构,正好差不多就拿来用了。

相比起文章开头的结果结构要多了一些属性,后续我会把他删除,但是如果需要,也可以保留。

class Node {
	/**
	 * @param {String} aTag 标签名
	 * @param {Object} props 标签所含有的属性
	 * @param {String} value 可选 标签内容 待完善
	 * @param {Array} parent 当前标签的父级
	 */
  constructor({ aTag, props, parent }) {
    this.aTag = aTag;
    this.props = props;
    // this.value = value;
    this.parent = parent || [];
    this.children = [];
  }
}

二、创建主函数

function parseDOM(str){
    let reg = /<([^<>]*)>/g; // 正则匹配标签 <div props="value">
    let res, temp, tag,
    parent = new Node({aTag: '', props: ''}); // 初始化父级
    
    temp = str.match(reg);
    
    return parent.children;
}

这个就是主函数一个基础架构了,先创建好需要的变量,以及匹配所有的标签。

最终代码肯定是需要完善主函数的, 先把其他工具函数写好,回头来完善这一部分的代码。

temp结果如下

image.png

三、解析标签函数

这个函数的主要作用在于将上图中的标签进行解析,拆分为标签名、标签属性、以及确认是否是单标签

function splitTag(tag){
    let cut = tag.replace(/<|>|"|'/g, ''); // 将尖括号<> 单双引号"'都去除,免得影响美观
    let res = {},
        props = {},
        temp = [];
    res['tagName'] = cut.shift(); // 记录tagName, 用shift()可以不用计算
    
    if(cut.length){  // 如果还有剩余值,表明还有属性需要处理
        for(let item of cut){
            if(temp[0] == '/'){ // 假设不会有用户用“/” 作为属性名
                props['singleTag'] = true; // 标记为单标签
            } else {
                // Vue中有:value 的写法,去除冒号, 如果是单属性, 设置默认值为true
                props[temp.shift().replace(':','')] = temp.shift() || true;
            }
        }
        res.props = props;
    }
    return res; // {tagName: 'div', props: {name: "value"}}
}

写完这个工具函数,就完成了一大半了,这时候打印解析过的标签可以看到如下图

image.png

四、完善主函数,完成解析

能打印出如上图的解析结果,基本完成三分之一了。

剩下的三分之二就是用正确的父子顺序插入到对象中了。

function parseDOM(str){
	let reg = /<([^<>]*)>/g; // 匹配所有的标签  <div  props="value">
	let res, temp, tag,
	parent = new Node({aTag: '', props: ''}); // 初始化父级

	temp = str.match(reg);

	temp.map(item => {
		tag = splitTag(item); // 调用解析函数
                
		if(tag.tagName.indexOf('/') == 0 ){ // 如果有斜杠开头,说明是结束标签,向上一级
			parent = parent.parent; // 重新指向父级
		} else if(tag.props?.singleTag == true){ // 如果为单标签 存入当前子级,父级不变.
                // 这里注意判断是要使用可选链,否则会出现singleTag未定义的情况
			res = new Node({aTag: tag.tagName, props: tag.props, parent: parent});
			parent.children.push(res)
		} else { // 双标签的开始抱歉,存入当前子级,并将子级设置为新的父级
			res = new Node({aTag: tag.tagName, props: tag.props, parent: parent});
			parent.children.push(res)
			parent = parent.children[parent.children.length - 1]; 
		}
	})

	return parent.children;
}

完善了主函数之后,基本就大功告成了。

image.png

五、去除父级和属性

就在自我感觉良好的时候, 小伙伴来了一句有点乱,希望保留标签名和子级。

本来今天高高兴兴的。

看着我的代码,和打印结果,我陷入了沉思。

要不要打他一顿呢, 套麻袋还是套垃圾袋呢?

突然,我脑子一动,这个结构想什么? 因为最多只有两个子节点,像不像一颗二叉树。

但是考虑到字节点是会变的, 那最少也是一棵树。既然是书,那就可以先序、中序、后序遍历啊。

只见我电光火石见有了如下函数

function deleteParentProps(data){
    if(+data) return // 如果是空数组 就return,不在向下
    
    data.children.map(item => deleteParentProps(item))
    delete data.parent
    delete data.props // 想要保留解析的属性的话,可以不删
}

加上这个函数之后, 在主函数的return之前 加入下面的代码

deleteParentProps(parent);

return parent.children;

看最终效果

image.png

相对干净整洁了很多, 但是没有标签属性,没有灵魂。

六、文本节点

这个时候又要扩充一下了, 标签内怎么会没有文本呢? 所以我对原字符串进行了扩充

<el-tabs v-model="activeName" type="card">
   	<el-tab-pane label="图" name="view">
     	<div class="code">
            这是一段文字
            <input />
     	</div>
   	</el-tab-pane>
        这里也是一段文字
   	<el-tab-pane label="JSON" name="sourceCode">
     	<div class="code">
       		<json-viewer :value="jsonValue" copyable boxed sort class="json-editor" ></json-viewer>
     	</div>
   	</el-tab-pane>
 </el-tabs>

既然对文字进行扩充了,那么首当其冲的,就是主函数内的正则了

// 原正则为
let reg = /<([^<>]*)>/g;
// 修改为
let reg = /<([^<>]*)>|(\S.*)/g;

之后在主函数中添加对文本节点的处理

// 主函数中的代码不全部写出来了
temp.map(item => {
    // 修改1
    if(item.indexOf('<') == 0) tag.splitTag(item)
    // 模拟splitTag的结果,添加文本标志,将文本作为tagName
    else tag = {props: {'text': true}, tagName: item}
    
    // 修改2
    	if(tag.tagName?.indexOf('/') === 0 ){
	} else if(tag.props?.singleTag === true){	
	} else if(tag.props?.text === true){ // 增加了对文本的判断及处理
		res = new Node({aTag: tag.tagName, props: null, type: "Text", parent: parent});
		parent.children.push(res);
	} else {
	}
})

删除了props和parent,方便查看,最终效果图如下,

image.png

总结

想来应该是可以优化的, 甚至我这种写法也不算最优解吧。 只是实在太晚了,没时间优化了。

明天还得早起搬砖呢, 今天的砖格外烫手啊。

愿心情美丽,愿我爱之人、爱我之人一切安好。