<p>data-id 和 data-expandable 是树节点必需属性:data-id 唯一标识节点且必须为字符串,data-expandable="true" 显式标记可展开节点;二者均需在插入 DOM 前设置,JS 通过 dataset.id 和 event.target.closest('[data-expandable]') 安全读取,结构化数据须 JSON.parse + try/catch,状态变更应操作 hidden 或 class 而非 data-* 属性。</p>

data-id 和 data-expandable 是树节点的必需属性
每个 li 节点必须带 data-id,用于唯一标识;可展开节点还需显式设置 data-expandable="true"(叶子节点不设)。浏览器不会校验这些属性是否存在,但 JS 逻辑依赖它们做精准定位和状态判断。
常见错误是把 ID 存在 id 属性里——id 仅用于 CSS 或 getElementById,而 dataset.id 读不到;或者漏掉 data-expandable,导致点击后无法识别是否该触发展开逻辑。
-
data-id值必须为字符串,数字需转成字符串:li.setAttribute('data-id', String(node.id)) - 不要用
data-node-id或dataNodeId—— 浏览器只解析全小写+连字符格式,其他命名直接忽略 - 动态创建节点时,
data-expandable必须在插入 DOM 前设置好,否则事件委托查不到
用 event.target.closest() 安全提取 data-* 值
树容器上绑定一次 click 事件,用 event.target.closest('[data-expandable]') 找到被点击的可展开节点,再取 btn.dataset.id。别直接监听每个 li,懒加载节点还没生成,监听会失效。
容易踩的坑是没加 event.stopPropagation():点击内部复选框或按钮时,事件冒泡到父 li,误触发展开/折叠。
立即学习“前端免费学习笔记(深入)”;
- 判断目标是否有效:
const btn = e.target.closest('[data-expandable]'); if (!btn || btn.disabled) return; - 读
data-id用btn.dataset.id,不是btn.id或btn.getAttribute('id') - 如果 ID 含数字开头(如
data-2024-id="123"),必须用btn.dataset["2024Id"],点号访问会报错
data-* 只传初始值,结构化数据要 JSON.parse + try/catch
树节点常需要附带额外元信息,比如 data-config='{"type":"folder","editable":true}'。但 dataset.config 拿到的是字符串,直接用会出错;且服务端模板若未转义引号,JSON 解析直接崩溃。
别把对象直接赋给 dataset.config = { type: 'folder' } —— 这只是 JS 对象,不会写入 DOM,后续读不到。
- 写入时用
li.setAttribute('data-config', JSON.stringify(config)) - 读取时必须包
try { JSON.parse(el.dataset.config || '{}') } catch (e) { console.warn('invalid data-config', e) } - 布尔值别信
if (el.dataset.active)—— 字符串"false"也是真值,应写el.dataset.active === "true"
隐藏/显示不能靠 data-* 驱动,改用 hidden 或 class
data-expanded="true" 这类属性看着方便,但改它不会让子列表显示出来。真正能触发浏览器重绘的只有 hidden 属性和 class 的增删。
常见反模式是用 setAttribute('data-expanded', 'false') 然后靠 CSS 选择器 [data-expanded="false"] ul 控制显隐 —— 这种写法在 SSR 或框架中极易失效,且无法做 CSS 过渡动画。
- 展开时:子
ul元素执行ul.hidden = false(或ul.removeAttribute('hidden')) - 折叠时:用
ul.hidden = true,比style.display = 'none'更语义、更兼容 - 多状态(如 loading / error)走
li.classList.toggle('state-loading'),配合 CSS 规则控制图标和文字
data-* 在树形菜单里只干一件事:把静态初始信息“钉”在 DOM 上,供 JS 第一时间读取。它不响应变化,不驱动视图,也不跨刷新存活。所有状态同步、类型转换、异常兜底都得手动写,少一步就可能在某个节点上静默失败。



















