Vue3非遞歸渲染Tree組件的初步實現(xiàn)代碼
本節(jié)開始,咱們一起來開發(fā)一個tree
組件,通過逐步迭代,使其從光禿禿、毫無生機變?yōu)樯鷻C盎然、裝飾漂亮。
樹結(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
(標簽)、id
(節(jié)點標識id)和expanded
(是否展開)。很自然,我們可以為這種嵌套結(jié)構(gòu)的節(jié)點抽取出類型定義,創(chuàng)建一個維護樹節(jié)點類型定義的types.ts
文件: src/components/tree/types.ts
// 定義基本的樹節(jié)點類型 export interface ITreeNode { label: string // 節(jié)點標簽名 id?: string // 節(jié)點id,可為空 children?: ITreeNode[] // 子節(jié)點列表,可為空 expanded?: boolean //是否展開,空則表示默認折疊 }
這里咱們創(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)進行扁平化處理后,采用列表的形式來渲染,只不過樹的層級結(jié)構(gòu)是由節(jié)點元素按照層級進行一定的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)進行拍平處理。小伙伴們跟著小卷的思路,咱們先來實現(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
方法,在遍歷每個元素時進行轉(zhuǎn)存的處理,把結(jié)果存入箭頭回調(diào)函數(shù)的第一個參數(shù)result
中,而在reduce
的第2個參數(shù)中對這個存放轉(zhuǎn)存元素的數(shù)組進行初始化。 現(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é)點的標記屬性代替,也就是說,咱們要定義一個新的IFlattreeNode
結(jié)構(gòu)來替換掉原始的ITreeNode
,我們將在原來節(jié)點類型基礎(chǔ)上進行擴展: src/components/tree/types.ts
... // 擴展層級關(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
擴展了4個字段,其中parentId
可為空,originalNode
的引用有助于對其子節(jié)點做遞歸計算處理。
對generateFlatTree
函數(shù)進一步完善后導(dǎo)出,方法參數(shù)擴展level
和pid
參數(shù),使得遞歸時能綁定節(jié)點上下級關(guān)系,對拍平的節(jié)點這里我們從原節(jié)點拷貝出一個對象作為IFlatTreeNode
類型,后續(xù)會通過邏輯處理為其擴展屬性賦值,并最終把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)進行拍平處理,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
進行測試,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={{ /* 樹的層級縮進 */ 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
進行遍歷時,跳過這些長度的節(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
類型中擴展一個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ù)進行計算,得到真正要展示的數(shù)據(jù),自然我們想到在tree
組件的setup
方法中進行這樣的處理:
// 讓其變?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é)點做下標記,在渲染的節(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
屬性豈不是不會變化了嘛?!”“非常好的問題!”咱們后面要進一步使用計算屬性來修復(fù)這個問題,給善于思考的小伙伴一個大大的??
好了!學(xué)到這里,咱們一顆基本的樹組件就“畫”好了,后續(xù)咱們會繼續(xù)逐步迭代來豐富展示效果和交互體驗!大家加油!
以上就是Vue3非遞歸渲染Tree組件的初步實現(xiàn)代碼的詳細內(nèi)容,更多關(guān)于Vue3 Tree組件實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說明
今天小編就為大家分享一篇淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11vue router 通過路由來實現(xiàn)切換頭部標題功能
在做單頁面應(yīng)用程序時,一般頁面布局頭尾兩塊都是固定在布局頁面,中間為是路由入口。這篇文章主要介紹了vue-router 通過路由來實現(xiàn)切換頭部標題 ,需要的朋友可以參考下2019-04-04一款移動優(yōu)先的Solid.js路由solid router stack使用詳解
這篇文章主要為大家介紹了一款移動優(yōu)先的Solid.js路由solid router stack使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08Vue集成three.js并加載glb、gltf、FBX、json模型的場景分析
這篇文章主要介紹了Vue集成three.js,并加載glb、gltf、FBX、json模型,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09