Element UI el-tree 展开指定节点,需要遍历逐级展开
...大约 4 分钟
Element UI el-tree 展开指定节点,需要遍历逐级展开
背景
迭代一个老项目,需要让 el-tree 在数据加载完成后默认展开某个节点。参照网上博客的写法(通过 store.nodesMap 直接设置 expanded)时,直接报错 TypeError: Cannot set properties of undefined (setting 'expanded'),而且只展开子节点时,父节点处于收起状态,目标节点依然看不见。这篇文章记录了这个问题的排查过程和最终方案。
一、问题现象
1、报错信息
在 treeData 的 watcher 中设置默认展开节点时,控制台报错:
Error in callback for watcher "treeData": "TypeError: Cannot set properties of undefined (setting 'expanded')"2、效果异常
- 即使不报错,只把目标子节点展开,其父节点仍是收起状态,目标节点在界面上依然看不到
- 树层级越深,这个现象越明显,需要把从根到目标节点的整条路径都展开
二、问题代码
最初的写法(参照网上博客,针对单个子级的场景):
treeData: {
handler (val) {
if (val && val.length > 0) {
this.defaultExpandId = val[0].children[0].id
this.$refs.tissueTree.store.nodesMap[this.defaultExpandId].expanded = true
}
}
}三、问题原因分析
1、el-tree 展开指定节点的原理
el-tree 内部有一个 store,其中 store.nodesMap 是以 node-key(如 id)为 key 的节点映射表。让某个节点展开,本质上就是把该节点对象上的 expanded 置为 true:
this.$refs.tree.store.nodesMap[nodeId].expanded = true2、为什么会报 undefined?
| 原因 | 说明 |
|---|---|
| 时序问题 | treeData 通常是异步加载的,watcher 触发时 el-tree 还没用新数据重建内部的 store.nodesMap,此时映射表里还没有目标节点,取出来就是 undefined |
| 数据缺失 | val[0] 可能没有 children,或 children[0] 不存在,导致 defaultExpandId 本身就是 undefined |
3、为什么只展开一个节点还不够?
网上的博客写法一般只展开目标节点本身。但树的显示逻辑是:父节点收起时,子节点是隐藏的。所以只把目标子节点 expanded = true,父节点不展开,界面上照样看不到目标节点。
尤其当目标节点层级较深时,必须从根节点到目标节点逐级展开整条路径,这才是多个子级/多层级的通用解法。
四、解决方案:遍历逐级展开
核心思路三步:
- 在
$nextTick中操作,确保树渲染、store.nodesMap重建完成; - 递归收集从根到目标节点的整条路径 id;
- 逐级把路径上每个节点的
expanded置为true,并做防御性判空。
treeData: {
handler (val) {
if (val && val.length > 0) {
// 默认展开第一个一级节点的第一个子节点
const first = val[0]
const target = first && first.children && first.children[0]
if (target && target.id != null) {
this.defaultExpandId = target.id
// 等树渲染完成、store 更新后再展开
this.$nextTick(() => {
const tree = this.$refs.tissueTree
if (!tree || !tree.store || !tree.store.nodesMap) return
// 收集目标节点及其所有祖先 id,逐级展开,确保目标节点可见
const pathIds = []
const collectPath = (nodes) => {
for (const item of nodes) {
if (item.id === this.defaultExpandId) {
pathIds.push(item.id)
return true
}
if (item.children && collectPath(item.children)) {
pathIds.push(item.id)
return true
}
}
return false
}
collectPath(val)
pathIds.forEach(id => {
const node = tree.store.nodesMap[id]
if (node) node.expanded = true
})
})
}
}
}
}关键点说明:
$nextTick:treeData异步更新后,store.nodesMap尚未重建,必须等 DOM/store 更新完成后再取节点,否则必然是undefined。- 递归收集路径:
collectPath从根开始查找目标节点,命中后回溯收集整条路径上的所有 id,逐级展开,保证目标节点真正可见。 - 防御性判空:每个节点都判断存在再操作,避免再次抛出
Cannot set properties of undefined。
五、总结
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接设置 nodesMap[id].expanded | 单层、数据已就绪 | 代码简单 | 有时序问题,层级深时父节点不展开看不到 |
| 遍历逐级展开(本文方案) | 多层/多子级 | 时序稳定,目标节点必然可见 | 需要递归收集路径,代码略多 |
建议
展开树的指定节点时,不要只展开目标节点本身,一定要从根到目标节点逐级展开整条路径,并配合 $nextTick 和判空处理,才能在异步加载场景下稳定生效。