JavaScript简易递归模板引擎递归子节点无法追加问题
极简递归JS模板引擎子节点挂载失败问题
实现逻辑
开发一款支持递归特性的极简JavaScript模板引擎,核心设计为:
- 定义单个
<template></template>标签作为模板基座 - 标签内设置两类特殊占位字段:
{{字段名}}用于普通文本替换,!!R 字段名!!标记递归渲染位置 - 渲染时递归标记位置会被模板自身用对应子数据递归渲染替换
初始实现代码
JavaScript 部分
class Template { constructor(template, container) { this.template = template; this.container = container; this.node = []; } render_single(template, node, data) { // 替换{{.*?}}格式的普通占位符 node.innerHTML = template.content.children[0].innerHTML.replace(/{{(.*?)}}/g, (match) => { var key = match.split(/{{|}}/).filter(Boolean)[0]; if (key in data) { return data[key]; } else { console.log("WARNING: " + key + " not found in data array"); } return ""; }); node.innerHTML = node.innerHTML.replace(/!!(.*?)!!/g, (match) => { var key = match.split(/!!|!!/).filter(Boolean)[0]; switch (key.substr(0, 1)) { case 'R': // 递归渲染标记 var rkey = key.substr(2); if (rkey in data) { var rnode = node.querySelector('.' + rkey); var rtemplate = new Template(this.template, rnode); rtemplate.render(data[rkey]); } break; } return ""; }); this.container.append(node); } render(data) { for (var j = 0; j < data.length; j++) { for (var i = 0; i < this.template.length; i++) { var node = this.template[i].content.children[0].cloneNode(true); this.render_single(this.template[i], node, data[j]); } } } } window.onload = function() { var data = [{ name: "Test1", childs: [{ name: "Test Childs 1" }, { name: "Test Childs 2" }, ] }, { name: "Test2" }, { name: "Test3" }, { name: "Test4" }, ]; var container = document.querySelector('#test'); var template = container.getElementsByTagName('template'); test = new Template(template, container); test.render(data); }
HTML 模板部分
<div id="test"> <template> <div class="test"> <p>Template {{name}}</p> <div class="childs">!!R childs!!</div> </div> </template> </div>
问题现象
- 仅顶层节点可以正常追加到容器中,递归生成的子节点无法挂载到对应目标节点
- 测试时如果将递归子节点追加到
document.body(即上述JS代码中递归逻辑里的rnode替换为document.body),子节点可以正常渲染 - 疑问:向父节点追加子节点前,是否需要执行特定操作更新父节点状态?
问题原因
不需要额外更新父节点状态,问题出在DOM操作和字符串替换的执行顺序冲突:
- 处理递归标记的逻辑写在了
String.replace()的回调函数中,此时正则替换还没执行完成,在回调里给临时DOM节点rnode插入递归生成的子节点后,replace会返回替换完成的新字符串(递归标记!!R childs!!已经被替换为空字符串) - 紧接着执行
node.innerHTML = 替换后的新字符串时,浏览器会重新解析这段HTML生成全新的DOM结构,之前在回调里操作的旧.childs节点连同插入的所有递归子节点,会被新生成的空.childs节点直接覆盖销毁,自然无法显示。 - 换成
document.body能正常渲染的原因是,document.body从来没有被重新赋值innerHTML覆盖,插入的节点会一直保留,不存在被销毁的问题。
修复方案
把递归渲染逻辑从字符串替换的回调里移出来,等普通占位符替换完成、HTML稳定渲染为DOM节点后,再查找递归容器做子节点渲染,避免后续的innerHTML赋值覆盖已经插入的递归内容。
修正后的render_single方法代码:
render_single(template, node, data) { // 1. 先替换所有普通{{}}占位符,生成替换完普通文本的HTML字符串 const plainHtml = template.content.children[0].innerHTML.replace(/{{(.*?)}}/g, (match) => { const key = match.slice(2, -2).trim(); if (key in data) return data[key]; console.log(`WARNING: ${key} 未在数据集中找到`); return ""; }); // 2. 将普通文本HTML写入节点,生成稳定DOM结构 node.innerHTML = plainHtml; // 3. 遍历所有递归标记,直接在已生成的DOM上做递归渲染,不会被后续操作覆盖 const regRecursive = /!!R\s+([a-zA-Z0-9_]+)!!/g; let matchRes; while ((matchRes = regRecursive.exec(plainHtml)) !== null) { const rkey = matchRes[1]; if (rkey in data && Array.isArray(data[rkey])) { const rnode = node.querySelector(`.${rkey}`); const rInstance = new Template(this.template, rnode); rInstance.render(data[rkey]); } } this.container.append(node); }
内容的提问来源于stack exchange,提问作者Tobias Binkowski
相关产品推荐
相关产品推荐

