Vue3非遞歸渲染Tree組件的初步實現(xiàn)代碼
本節(jié)開始,咱們一起來開發(fā)一個tree組件,通過逐步迭代,使其從光禿禿、毫無生機(jī)變?yōu)樯鷻C(jī)盎然、裝飾漂亮。
樹結(jié)構(gòu)的定義
首先對于樹結(jié)構(gòu),我們自然想到下面的展現(xiàn)形式:
[
{
label: '前端',
id: 'frontend'
},
{
label: 'java',
id: 'java',
expanded: true,
children: [
{ label: '11', id: '11' },
{ label: '22', id: '22' },
{ label: '33', id: '33' },
{
label: '44',
id: '44',
expanded: true,
children: [
{
label: 'aaa',
id: 'aaa',
expanded: true,
children: [
{ label: 'a11', id: 'a11' },
{ label: 'a22', id: 'a22' }
]
},
{
label: 'bbb',
id: 'bbb',
expanded: false,
children: [
{ label: 'b11', id: 'b11' },
{ label: 'b22', id: 'b22' }
]
}
]
},
{ label: '55', id: '55' },
{ label: '66', id: '66' },
{ label: '77', id: '77' },
{
label: 'Java基礎(chǔ)',
id: 'javaBasic'
},
{
label: 'Java Web',
id: 'javaWeb',
children: [
{
label: 'ssm',
id: 'ssm'
}
]
}
]
},
{
label: '數(shù)據(jù)庫',
id: 'db',
expanded: false,
children: [
{
label: '關(guān)系型數(shù)據(jù)庫',
id: 'relationShipDB'
},
{
label: '非關(guān)系型數(shù)據(jù)庫',
id: 'nonRelationShipDB'
}
]
}
]
這里,小卷隨便造了些數(shù)據(jù),這是一個帶有children子節(jié)點數(shù)組的嵌套結(jié)構(gòu),此外還具有的屬性:label(標(biāo)簽)、id(節(jié)點標(biāo)識id)和expanded(是否展開)。很自然,我們可以為這種嵌套結(jié)構(gòu)的節(jié)點抽取出類型定義,創(chuàng)建一個維護(hù)樹節(jié)點類型定義的types.ts文件: src/components/tree/types.ts
// 定義基本的樹節(jié)點類型
export interface ITreeNode {
label: string // 節(jié)點標(biāo)簽名
id?: string // 節(jié)點id,可為空
children?: ITreeNode[] // 子節(jié)點列表,可為空
expanded?: boolean //是否展開,空則表示默認(rèn)折疊
}
這里咱們創(chuàng)建并導(dǎo)出一個ITreeNode類型,其中label字段是非空的,children是自身類型的一個數(shù)組。 有了前面的tree數(shù)據(jù)結(jié)構(gòu)和節(jié)點類型的定義,是不是咱們就可以編寫組件模板來渲染這棵樹了呢?答案是肯定的!我們可以定義一個TreeNode的組件,對其子節(jié)點列表的渲染采用遍歷的方式繼續(xù)遞歸渲染子組件TreeNode,這種思路是開發(fā)具有嵌套的層級結(jié)構(gòu)組件大伙兒所能想到的常規(guī)思路。
咱們這里要介紹的是對嵌套數(shù)據(jù)結(jié)構(gòu)進(jìn)行扁平化處理后,采用列表的形式來渲染,只不過樹的層級結(jié)構(gòu)是由節(jié)點元素按照層級進(jìn)行一定的paddingLeft來實現(xiàn)的。隨著后續(xù)的學(xué)習(xí),小伙伴會發(fā)現(xiàn),盡管tree組件的渲染不是遞歸的,但是對鋪平節(jié)點列表之前的拍平處理以及后續(xù)子節(jié)點的計算處理卻依然采用的遞歸的思想呢。
注意
拍平結(jié)構(gòu)避免了樹組件內(nèi)部的遞歸渲染,但帶來的麻煩是,需要開發(fā)者對于子節(jié)點范圍劃定做更多的編程處理。后續(xù)開發(fā)中,小卷會提供思路,如何來簡化這種處理方式。
樹結(jié)構(gòu)拍平處理
現(xiàn)在咱們寫一個工具函數(shù)對之前的嵌套結(jié)構(gòu)進(jìn)行拍平處理。小伙伴們跟著小卷的思路,咱們先來實現(xiàn)一個最簡單的數(shù)組結(jié)果處理的需求:把一個數(shù)組中的元素復(fù)制到一個新數(shù)組中。創(chuàng)建一個utils.ts的工具函數(shù)編寫文件:
src/components/tree/utils.ts
function copyArr(arr: number[]) {
return arr.reduce((result, cur) => {
return result.concat(cur)
}, [] as number[])
}
const newArr = copyArr([1, 2, 3])
console.log(newArr)
這里我們巧用了Array的reduce方法,在遍歷每個元素時進(jìn)行轉(zhuǎn)存的處理,把結(jié)果存入箭頭回調(diào)函數(shù)的第一個參數(shù)result中,而在reduce的第2個參數(shù)中對這個存放轉(zhuǎn)存元素的數(shù)組進(jìn)行初始化。 現(xiàn)在我們測試這個copyArr函數(shù),可以全局安裝一個ts-node工具來執(zhí)行.ts文件:
npm i -g ts-node@10.9.2
同時,在工程tsconfig.json中加入ts-node配置,啟用對es module的編譯支持:
{
...
"ts-node": {
"esm": true
},
...
}
測試下,ok!

核心處理函數(shù)
發(fā)散思維
對于嵌套的樹結(jié)構(gòu),我們只需要寫一個函數(shù),接收一個代表當(dāng)前層級的數(shù)組,也就是通過數(shù)組的
reduce方法,將當(dāng)前層級的每個節(jié)點放到一個新的數(shù)組中,而對于父節(jié)點的情況,我們遞歸調(diào)用該函數(shù),對其子節(jié)點列表做相同的處理,將得到的拍平的數(shù)組,插入到當(dāng)前父節(jié)點之后即可。
ok!現(xiàn)在我們可以輕易寫出拍平核心處理函數(shù):
src/components/tree/utils.ts
import { ITreeNode } from './types'
export function generateFlatTree(tree: ITreeNode[]): ITreeNode[] {
return tree.reduce((prev, cur) => {
if (cur.children) {
// 遞歸,得到子節(jié)點拍平的數(shù)組
const children = generateFlatTree(cur.children)
return prev.concat(cur, children)
} else {
return prev.concat(cur)
}
}, [] as ITreeNode[])
}
測試一下,ok!

扁平化數(shù)據(jù)結(jié)構(gòu)
現(xiàn)在我們系統(tǒng)拍平后的結(jié)構(gòu)能夠展示父節(jié)點id、所處的層級,并且把嵌套的children屬性移除掉,用一個是否葉子節(jié)點的標(biāo)記屬性代替,也就是說,咱們要定義一個新的IFlattreeNode結(jié)構(gòu)來替換掉原始的ITreeNode,我們將在原來節(jié)點類型基礎(chǔ)上進(jìn)行擴(kuò)展: src/components/tree/types.ts
...
// 擴(kuò)展層級關(guān)系,用于拍平結(jié)構(gòu)的樹節(jié)點
export interface IFlatTreeNode extends ITreeNode {
parentId?: string // 父節(jié)點id,若是一級節(jié)點則為空
level: number // 節(jié)點層級,數(shù)值從1開始
isLeaf: boolean //是否為葉子節(jié)點
originalNode: ITreeNode // 指向?qū)?yīng)的原始節(jié)點
}
這里,我們對ITreeNode擴(kuò)展了4個字段,其中parentId可為空,originalNode的引用有助于對其子節(jié)點做遞歸計算處理。
對generateFlatTree函數(shù)進(jìn)一步完善后導(dǎo)出,方法參數(shù)擴(kuò)展level和pid參數(shù),使得遞歸時能綁定節(jié)點上下級關(guān)系,對拍平的節(jié)點這里我們從原節(jié)點拷貝出一個對象作為IFlatTreeNode類型,后續(xù)會通過邏輯處理為其擴(kuò)展屬性賦值,并最終把children屬性移除掉,完善后的核心代碼:
import { IFlatTreeNode, ITreeNode } from './types'
/**
*
* @param tree 當(dāng)前層級的節(jié)點列表
* @param level 表示當(dāng)前節(jié)點所處的層級
* @param pid 父節(jié)點id
*/
export function generateFlatTree(tree: ITreeNode[], level = 0, pid = ''): IFlatTreeNode[] {
level++ // 層級加1
// 巧用數(shù)組的reduce方法對嵌套的樹形結(jié)構(gòu)進(jìn)行拍平處理,prev為當(dāng)前層級要處理的拍平結(jié)構(gòu)結(jié)果,cur為當(dāng)前遍歷的節(jié)點
return tree.reduce((prev, cur) => {
// 拷貝當(dāng)前節(jié)點
const o = { ...cur } as IFlatTreeNode
// 綁定關(guān)系
o.originalNode = cur
o.level = level
// 為內(nèi)層節(jié)點設(shè)置父id
if (level > 1 && pid) {
o.parentId = pid
}
// 判斷當(dāng)前節(jié)點是否存在children,如果存在則遞歸處理
if (o.children) {
// 以當(dāng)前節(jié)點作為父節(jié)點,對子節(jié)點列表做遞歸處理,得到內(nèi)部拍平的內(nèi)容
const children = generateFlatTree(o.children, level, o.id!)
// 移除嵌套結(jié)構(gòu)
delete o.children
// 在已經(jīng)拍平的結(jié)構(gòu)基礎(chǔ)上再拼接當(dāng)前節(jié)點和內(nèi)部拍平節(jié)點
o.isLeaf = false
return prev.concat(o, children)
} else {
// 葉子節(jié)點,處理會更簡單
o.isLeaf = true
return prev.concat(o)
}
}, [] as IFlatTreeNode[])
}
通過ts-node進(jìn)行測試,ok!

Tree組件開發(fā)
有了前面樹結(jié)構(gòu)轉(zhuǎn)換的鋪墊,樹組件的開發(fā)會變得非常簡單!在types.ts中定義組件的data屬性:
import { ExtractPropTypes, PropType } from 'vue'
// 樹數(shù)據(jù)的屬性定義
export const props = {
data: {
// 類型為一個元素為ITreeNode類型的數(shù)組
type: Object as PropType<Array<IFlatTreeNode>>,
required: true
}
} as const // 設(shè)置為常量,外部無法修改
// 提取tree組件的屬性定義類型
export type Props = ExtractPropTypes<typeof props>
...
注意,這里節(jié)點的定義類型為IFlatTreeNode,這樣我們可以在外部先完成拍平操作后再給tree組件傳入data屬性即可。
src/components/tree/index.tsx
import { defineComponent } from 'vue'
import { props, Props } from './types'
export default defineComponent({
name: 'JuanTree',
props,
setup(props: Props) {
// 解構(gòu)出傳入的tree數(shù)據(jù)
const { data } = props
return () => {
return (
<div class='juan-tree'>
{/* 相當(dāng)于v-for */}
{data.map((treeNode) => (
<div
key={treeNode.id}
class='juan-tree-node'
style={{
/* 樹的層級縮進(jìn) */
paddingLeft: `${24 * (treeNode.level - 1)}px`
}}
>
{treeNode.label}
</div>
))}
</div>
)
}
}
})
值得注意的是,這里tsx的元素遍歷的寫法,需要為元素綁定key,以實現(xiàn)節(jié)點變化后的局部dom渲染。這里我們對節(jié)點元素<div>采用了動態(tài)綁定style屬性的方式依據(jù)level來決定其左邊留白的距離。
在App組件中應(yīng)用JuanTree組件:
import { defineComponent } from 'vue'
import JuanTree from './components/tree'
import { generateFlatTree } from './components/tree/utils'
import { ITreeNode } from './components/tree/types'
export default defineComponent({
setup() {
// 這里數(shù)據(jù)省略
const treeData = [...] as ITreeNode[]
return () => {
// 樹的扁平化處理
const flatTree = generateFlatTree(treeData)
return (
<div class='m-4'>
<JuanTree class='bg-gray-200' data={flatTree}></JuanTree>
</div>
)
}
}
})
看下頁面效果,粗略的展示出一棵樹:

實現(xiàn)樹節(jié)點的展開與折疊
思路點撥
實際要渲染的樹結(jié)構(gòu),咱們應(yīng)該排除掉所有
expanded不為true的節(jié)點的后代節(jié)點。也就是應(yīng)該有一個方法來計算一個IFlatTreeNode下所有后代節(jié)點的長度,在從上到下對傳入的data進(jìn)行遍歷時,跳過這些長度的節(jié)點即可得到最終要渲染的樹的列表結(jié)構(gòu)。
這里要計算的后代節(jié)點長度,聰明的小伙伴想到在generateFlatTree工具函數(shù)中,其實基于遞歸調(diào)用得到的結(jié)果,咱們是直接可以利用的。我們不妨打印到控制臺看看:
if (o.children) {
const children = generateFlatTree(o.children, level, o.id!)
console.log(o.id + '子節(jié)點長度:' + children.length)
...
}

這樣還要啥自行車,咱直接獲取就行了嘛。自然我們可以在IFlatTreeNode類型中擴(kuò)展一個length屬性:
export interface IFlatTreeNode extends ITreeNode {
...
length: number // 所有子孫節(jié)點的長度
}
在generateFlatTree函數(shù)中判斷是父節(jié)點的邏輯中設(shè)置下該屬性:
if (o.children) {
const children = generateFlatTree(...)
// 記錄當(dāng)前節(jié)點子代的長度
o.length = children.length
...
}
接著,咱們需要借助于計算屬性對響應(yīng)式的tree數(shù)據(jù)進(jìn)行計算,得到真正要展示的數(shù)據(jù),自然我們想到在tree組件的setup方法中進(jìn)行這樣的處理:
// 讓其變?yōu)轫憫?yīng)式數(shù)據(jù)以加入計算屬性的計算
const flatData = ref(data)
// 獲取那些展開的節(jié)點列表
const expandedTree = computed(() => {
const result = []
// 循環(huán)列表,跳過那些非expanded
for (let i = 0; i < flatData.value.length; i++) {
const item = flatData.value[i]
// 當(dāng)當(dāng)前節(jié)點處于折疊狀態(tài),它的子節(jié)點應(yīng)該被排除
if (!item.isLeaf && item.expanded !== true) {
// 跳過內(nèi)部所有的節(jié)點
i += item.length
}
result.push(item)
}
// 得到折疊后的新節(jié)點列表
return result
})
此時,模板使用的是計算屬性返回的數(shù)據(jù),遍歷的是expandedTree.value:
<div ...>
{expandedTree.value.map((treeNode) => (
...
))}
</div>
看下效果,ok!正是我們需要的折疊后的效果!

對是否展開的節(jié)點做下標(biāo)記,在渲染的節(jié)點模板中我們暫且以button元素作為節(jié)點折疊、展開的修飾部件:
<div ...>
{treeNode.isLeaf ? (
/* 葉子節(jié)點臨時展示,留出間距,確保同級的父節(jié)點和葉子節(jié)點對齊 */
<span class='mr-1 inline-block w-[20px]'></span>
) : (
<button class='mr-1 inline-block h-[18px] w-[20px]'>
{/* 父節(jié)點的展開/折疊操作臨時用+、-代替 */}
{treeNode.expanded ? <span>-</span> : <span>+</span>}
</button>
)}
{treeNode.label}
</div>
再瞅一眼,good!

最后再來錦上添花,實現(xiàn)展開與折疊效果,給button綁定點擊事件:<button onClick={() => toggleNode(treeNode)} ...>,在TSX的setup方法中聲明下事件處理函數(shù):
const toggleNode = (node: ITreeNode) => {
// 對展開狀態(tài)取反
node.expanded = !node.expanded
}
這里對實際為IFlatTreeNode類型的節(jié)點更新expanded屬性,因為expandedTree計算屬性中參與計算的flatData是響應(yīng)式的,而計算屬性返回數(shù)據(jù)列表中的節(jié)點對象來自于傳入的data,節(jié)點對象屬性發(fā)生變化自然會觸發(fā)計算屬性重新計算啦看下效果,杠杠滴

存在的問題
聰明的小伙伴會提出這樣的問題:“小卷,現(xiàn)在咱們的樹節(jié)點是固定的,如果可以動態(tài)增刪節(jié)點,那么
IFlatTreeNode節(jié)點的length屬性豈不是不會變化了嘛?!”“非常好的問題!”咱們后面要進(jìn)一步使用計算屬性來修復(fù)這個問題,給善于思考的小伙伴一個大大的??
好了!學(xué)到這里,咱們一顆基本的樹組件就“畫”好了,后續(xù)咱們會繼續(xù)逐步迭代來豐富展示效果和交互體驗!大家加油!
以上就是Vue3非遞歸渲染Tree組件的初步實現(xiàn)代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue3 Tree組件實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
element-plus 下拉框?qū)崿F(xiàn)全選的示例代碼
本文主要介紹了element-plus 下拉框?qū)崿F(xiàn)全選的示例代碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05

