各位掘金的小伙伴,大家好!上一节我们初步实现了一个扁平化结构的Tree组件,本节咱们将很好的应用计算属性,实现节点层级信息的计算与缓存,并实现垂直参照线的展示。
前一节咱们留了一个需要改进的实现:获取一个父节点下所有子节点的长度。咱们的临时做法是投机取巧的取用generateFlatTree
函数内部递归返回的children
的length
。这种弊端是:可以动态增删节点的情况下,该结果是一成不变的。为此,咱们需要实时去计算,这种“苦差事”交给vue计算属性再好不过了。
计算子节点长度
定义length计算属性
因为咱们将针对结构化的ITreeNode
类型的子节点进行计算,所以在其类型定义中扩展一个字段:
// 定义基本的树节点类型
export interface ITreeNode {
...
length?: ComputedRef<number> // 子孙节点的长度
}
注意这里的ts
类型为计算属性的结果引用类型。
相应的,咱们在tree/utils.ts
中增加一个单独的initParentTreeNode
函数来对原始结构的父节点进行计算属性的绑定:
function initParentTreeNode(node: ITreeNode) {
const nodeRef = ref(node)
node.length = computed(() => {
return nodeRef.value.children!.reduce((count, cur) => count + (cur.children ? 1 + cur.length! : 1), 0)
})
}
该初始化函数中,我们先将node
包装成响应式对象,绑定的计算属性中,咱们会获取到响应式对象中的children
属性,注意这里的!.
的访问形式,因为咱们一定是对父节点操作的,children
一定不会为空嘛。然后,咱们巧用了数组的reduce
方法,对子节点进行遍历执行count
累加,如果子节点也是父节点,则count
记为1 + cur.length!
,显然这里我们采用了计算属性的递归访问,这种思想是在多层级结构计算中为我们所惯用的哦~还要注意这里cur.length!
,因为其children
属性不为空,说明是父节点,则一定会绑定length
计算属性呢。总体来说,一行代码就巧妙的计算出了一个父节点下所有的后代节点的长度。
该函数在generateFlatTree
中判断父节点的地方调用:
if (o.children) {
initParentTreeNode(cur)
...
}
同时,我们将原先获取子节点长度的代码删掉,IFlatTreeNode
类型上临时定义的length
属性也移除掉,而采用现在ITreeNode
上计算属性的方式
使用length计算属性
再看下tree
组件tsx
文件中的调整:
-
toggleNode
参数的接收类型改为(node: IFlatTreeNode)
这样调整的意图是,不光要操作节点继承的
expanded
属性,后续为了触发某些计算属性,咱们还得同时操作节点originalNode
,也就是原始节点的expanded
属性,目前还用不上。 -
接着第一点,相应的展开/折叠的
button
点击事件中调用toggleNode
参数类型的显式指定<button onClick={() => toggleNode(treeNode as IFlatTreeNode)} ...>
-
expandedTree
计算属性中需要跳过折叠节点内部所有子节点的代码调整为:i += item.originalNode.length!
为了方便看效果,在tree
模板中输出下:
<div ... >
...
{treeNode.label}
<span class='ml-4 text-amber-600'>{treeNode.originalNode.length}</span>
</div>
效果:
计算属性重新计算的前提
- 由外部访问后首次执行过
- 所绑定的组件或vnode已渲染
- 外部操作的是从响应式对象访问的属性
- 计算属性内部操作的是从响应式对象访问的属性
- 检测到其所关心的对象属性或者计算属性发生变化
计算节点索引
对于扁平化的tree
结构,我们希望在进行一些节点上下文操作时,不用每次都计算其在列表中所处的位置。聪明的小伙伴自然想到用计算属性呗!没错!相信聪明的你很快就有思路了:
思路点拨
拽着前一个人,从排头开始报数
对IFlatTreeNode
进行扩展:
export interface IFlatTreeNode extends ITreeNode {
...
prev?: IFlatTreeNode // 前一个节点
index: ComputedRef<number> // 在列表中的索引
}
调整tree/utils.ts
...
/**
*
* ...
* @param parent 父节点
*/
export function generateFlatTree(..., parent: IFlatTreeNode | null = null)... {
...
return tree.reduce((prev, cur) => {
// 拷贝当前节点
const o = ...
// 绑定前驱节点
if (prev.length > 0) {
o.prev = prev[prev.length - 1]
} else if (parent) {
o.prev = parent
}
...
if (level > 1 && parent) {
o.parentId = parent.id
}
// 初始扁平化节点
initFlatTreeNode(o)
if (o.children) {
...
const children = generateFlatTree(o.children, level, o)
...
} ...
}, [] as IFlatTreeNode[])
}
function initFlatTreeNode(node: IFlatTreeNode) {
// 绑定index计算属性
const prevNode = ref(node.prev)
node.index = computed(() => {
return prevNode.value == null ? 0 : prevNode.value.index + 1
})
}
...
为了便于追踪父节点作为前驱节点来绑定,这里调整了generateFlatTree
最后一个参数为IFlatTreeNode
类型,函数内部对parent
获取和传参的地方都做了相应的调整,注意获取前驱节点的逻辑,如果reduce
的回调函数第一个参数为非空数组则取最后一个元素作为前驱节点,否则取parent
。在initFlatTreeNode
函数中绑定计算属性所访问的前驱节点是响应式的,因为在插入和删除节点时,前驱绑定节点会发生改变。
当我们扩展了IFlatTreeNode
定义后,在tree/index.tsx
中发现ts飘红了:
咱们干脆使用
any
类型来声明入参类型,一了百了,toggleNode(treeNode as any)
,有时候ts
语法太苛刻了,凡事考虑的太精细,也是操碎心了。不要忘了在.eslintrc.json
的rules
中关闭对any
的检查:
"rules": {
"@typescript-eslint/no-explicit-any": ["off"]
}
最后咱们为节点内容输出index
:
<div
...
>
...
{treeNode.label}
<span class='ml-4 text-amber-600'>{treeNode.originalNode.length}</span>
<span class='ml-4 text-green-600'>{treeNode.index}</span>
</div>
页面效果,看到不管是否折叠,index
都是连续的:
计算参照线高度
接下来,咱们趁热打铁,以前面的思路计算属性的递归,为tree
组件实现垂直的ztree
风格的参照线。画线的思想:从一个父节点下子节点列表的第一个节点开始,一直往下,沿着可见的节点一直画到最后一个子一代节点的位置结束。
思路点拨
这里咱们要计算两种属性:一个父节点下可见的所有子孙节点的长度,我们记为
visibleLength
;一个父节点下参照线的长度,我们记为lineLength
。lineLength
的计算规则有两种:
- 累加除最后子一代节点的其他所有子一代节点的
visibleLength
,当然还要加上自身,也就是加1,如果子一代节点是叶节点,则可见长度为0
;- 用父节点的
visibleLength
减去最后一个子一代节点的visibleLength
下面咱们来做实现,首先在ITreeNode
中定义两个属性:
export interface ITreeNode {
...
visibleLength?: ComputedRef<number> // 可见子节点的长度
lineLength?: ComputedRef<number> // 参照线长度
}
在tree/utils.ts
的initParentTreeNode
函数中增加ITreeNode
的visibleLength
计算属性,其实很简单,参照length
计算属性的实现,判断如果子节点children
有值时,在此基础上再判断是否展开,展开则递归调用子节点的visibleLength
计算属性即可。
node.visibleLength = computed(() => {
if (!nodeRef.value.expanded) return 0
return nodeRef.value.children!.reduce((count, cur) => count + (cur.children ? (cur.expanded ? 1 + cur.visibleLength! : 1) : 1), 0)
})
再来看lineLength
计算属性的实现:
node.lineLength = computed(() => {
if (!nodeRef.value.expanded) return 0
let length = 1
const children = nodeRef.value.children
// 排除最后一个子一代节点
for (let i = 0; i < children!.length - 1; i++) {
if (i < children!.length - 1) {
const child = children![i]
if (child.children) {
length += 1 + child.visibleLength!
} else {
length += 1
}
}
}
return length
})
在tree
模板中输出下lineLength
计算属性:
<div
...
>
...
{treeNode.label}
{/*<span class='ml-4 text-amber-600'>{treeNode.originalNode.length}</span>*/}
{/*<span class='ml-4 text-green-600'>{treeNode.index}</span>*/}
<span class='ml-4 text-blue-600'>{treeNode.originalNode.lineLength}</span>
</div>
最后画龙点睛的一笔,在toggleNode
点击处理函数中,操作原始节点对象的expanded
属性,触发之前定义的length
、visibleLength
、lineLength
计算属性的连锁调用更新:
const toggleNode = (node: IFlatTreeNode) => {
node.expanded = !node.expanded
// 操作原始节点,对展开状态取反
const originalNode = node.originalNode
originalNode.expanded = !originalNode.expanded
}
页面效果,ok!随着节点的展开与折叠,参照线长度也会动态的变化:
之前我们说了lineLength
计算属性还有一种简化实现方式,一起来看下代码实现:
node.lineLength = computed(() => {
if (!nodeRef.value.expanded) return 0
const children = nodeRef.value.children
// 排除最后一个子一代节点
const lastChild = children![children!.length - 1]
return nodeRef.value.visibleLength! - (lastChild.visibleLength || 0)
})
锦上添花,加上参照线:
<div
...
class='juan-tree-node relative'
...
>
{!treeNode.isLeaf && treeNode.expanded && (
<span
class='tree-node-line absolute w-px border-l-[1px] border-dashed border-l-gray-400'
style={{
height: `${24 * (treeNode.originalNode.lineLength! - 1) + 14}px`,
left: `${24 * (treeNode.level - 1) + 34}px`,
top: `${24}px`
}}
></span>
)}
...
</div>
这里,咱们用<span>
标签做绝对定位,设置了左边框作为参照线,静态样式直接指定tailwindcss
的class
值即可,非常方便,然后我们用style
属性动态绑定了之前实现的原始节点的lineLength
计算属性。最后再来看下效果,非常完美!
好了!学到这里,相信小伙伴们对于vue计算属性在实际项目中的类似实践,可以拍拍胸脯,信心满满的说,原来看似复杂的功能用计算属性来实现这么简单!后续对vue的相关知识点和特性,咱们将继续用这种思路点拨加深入实践的方式,结合有趣的例子,带小伙伴们一起来学习体验,大家加油!!