樹形組件的需求,不少人遇到都以爲頭疼、邏輯複雜,除了展現以外,還要有增刪該查的邏輯。通常樹形組件具備多個層級,若是當前層級有下一個層級,會有像
children、list
等屬性,數據結構通常就是javascript
const tree = [
{
name: 'a',
id: 1,
},
{
name: 'b',
id: 2,
children: [
{
name: 'c',
id: 3
}
]
},
]
複製代碼
界面大概就是這種:前端
這裏先給出下文數據源:java
const data = [{"name":"廣東","id":1,"children":[{"name":"深圳","id":2,"children":[{"name":"南山區","id":3},{"name":"福田區","id":4},{"name":"寶安區","id":5}]},{"name":"廣州","id":6,"children":[{"name":"天河區","id":7},{"name":"番禺區","id":8},{"name":"海珠區","id":9}]}]}]
複製代碼
遞歸就是最常規的方式了,以antd的tree組件爲例,你們都會這樣作:node
// 放在react的class組件裏面
renderTree = (data = []) => {
return data.map(item => (
<TreeNode title={item.name}> {renderTree(item.children)} </TreeNode>
))
}
render() {
return (
<React.Fragment> <Tree defaultExpandAll={true} selectable={false}> <TreeNode title="root" > {this.renderTree(this.state.data)} </TreeNode> </Tree> </React.Fragment> ); } 複製代碼
先把名字做爲節點title,而後若是有子節點,就用一樣的方法渲染子節點。react
組件已經好了,若是咱們要點擊,咱們怎麼知道哪一個層級的哪一個節點被點了呢?是否是會寫一個搜索算法,傳入當前節點id,而後回溯去記錄路徑展現出來?這雖然能夠作到,但顯然是不優雅的,咱們只須要犧牲空間換時間的方法就能夠大大優化這個過程,便是在遍歷的過程當中把節點信息帶到下一個遞歸函數裏面去。算法
renderTree = (data = [], info = { path: '', id: '' }) => {
return data.map(item => (
<TreeNode title={ <Button onClick={() => console.log(`${info.path}/${item.name}`)}>{item.name}</Button> }> {this.renderTree(item.children, { path: `${info.path}/${item.name}`, id: `${info.id}/${item.id}` })} </TreeNode>
));
}
複製代碼
如今,咱們點擊哪個,就打印當前節點路徑了數組
若是遇到了增刪改查,基於前面的條件,咱們記錄了要用到的信息,因此能夠藉助這些信息進行增刪改查。ruby
後臺通常是id,對前端通常是keybash
咱們刪掉剛剛的按鈕,把id去掉(由於咱們如今僅僅用前端測試,只用key便可,若是須要傳到後臺,則須要遵照上面的規則傳id),而後用一樣的方法記錄每一層的keyantd
renderTree = (data = [], info = { path: '', key: '' }) => {
return data.map((item, index) => (
<TreeNode title={ <React.Fragment> {item.name} <Button onClick={() => { console.log(`${info.key}.${index}`.slice(1)) }}>新增節點</Button> </React.Fragment> }> {this.renderTree(item.children, { path: `${info.path}/${item.name}`, key: `${info.key}.${index}` })} </TreeNode> )); } 複製代碼
此時,咱們點擊天河區,打印出來的是0.1.0
,也就是咱們所點的是data[0].children[1].children[0]
,要給data[0].children[1].children[0]
的children push一個新元素。因此咱們還要寫一個相似lodash.get
的方法:
function get(target, keysStr) {
const keys = keysStr.split('.')
let res = target[keys.shift()]
while (res && keys.length) {
res = res.children[keys.shift()]
}
return res
}
複製代碼
Button
裏面的onclik方法改一下:
<Button onClick={() => {
const currentKeyPath = `${info.key}.${index}`.slice(1)
this.setState(({ data }) => {
const current = get(data, currentKeyPath) // 拿到當前節點
// 給children屬性追加一個新節點
;(current.children || (current.children = [])).push({ name: '新增的節點' })
return data
})
}}>新增節點</Button>
複製代碼
新增了一個奇奇怪怪的節點,巴不得立刻
更名了,刪除須要知道父節點key和當前節點key,咱們仍是繼續在title那裏加一個按鈕:
<Button onClick={() => {
const currentKeyPath = `${info.key}`.slice(1) // 父節點key路徑
this.setState(({ data }) => {
const current = get(data, currentKeyPath)
current.children.splice(index, 1) // 刪除當前節點第index個元素
return data
})
}}>刪除節點</Button>
複製代碼
咱們新增的了節點後,首先就是把系統默認名字改掉,改和刪都是差很少的,可是改須要維護一個輸入框來填寫新節點名字。常規的方法是另外控制一個Modal組件,這個Modal裏面有一個Input。點擊肯定便可修改。爲了更好的體驗,我一般是直接行內修改。先寫一個Edit組件,這個組件正常狀況下是一個按鈕,點擊了變成一個Input,失去焦點的時候修改完成
function Edit(props) {
const [value, setValue] = React.useState(props.value)
const [isEdit, setIsEdit] = React.useState(false)
const handleChange = React.useCallback((e) => {
setValue(e.target.value)
}, [setValue])
const handleBlur = React.useCallback((e) => {
const current = get(props.target, props.currentKeyPath)
current.name = value // 給當前節點的name賦值
props.setState(current) // 上層的setstate方法
setIsEdit(false)
}, [setValue, value])
return (
isEdit ?
<Input autoFocus={true} value={value} onChange={handleChange} onBlur={handleBlur} /> : <Button onClick={() => setIsEdit(true)}>修改節點</Button> ) } 複製代碼
有了Edit組件,咱們在title的元素裏面加上Edit組件:
<Edit
target={this.state.data}
value={item.value}
currentKeyPath={`${info.key}.${index}`.slice(1)}
setState={(state) => this.setState(state)}
/>
複製代碼
import { Input, Tree, Button } from 'antd';
import * as React from 'react';
const { TreeNode } = Tree;
function get(target, keysStr) {
const keys = keysStr.split('.')
let res = target[keys.shift()]
while (res && keys.length) {
res = res.children[keys.shift()]
}
return res
}
function Edit(props) {
const [value, setValue] = React.useState(props.value)
const [isEdit, setIsEdit] = React.useState(false)
const handleChange = React.useCallback((e) => {
setValue(e.target.value)
}, [setValue])
const handleBlur = React.useCallback((e) => {
const currnet = get(props.target, props.currentKeyPath)
console.log(props.target, currnet, props.currentKeyPath)
currnet.name = value
props.setState(currnet)
setIsEdit(false)
}, [setValue, value])
return (
isEdit ?
<Input
autoFocus={true}
value={value}
onChange={handleChange}
onBlur={handleBlur}
/> :
<Button onClick={() => setIsEdit(true)}>修改節點</Button>
)
}
const data = [
{ name: '廣東', id: 1, children: [
{ name: '深圳', id: 2, children: [
{ name: '南山區', id: 3 },
{ name: '福田區', id: 4 },
{ name: '寶安區', id: 5 },
] },
{
name: '廣州',
id: 6,
children: [
{ name: '天河區', id: 7 },
{ name: '番禺區', id: 8 },
{ name: '海珠區', id: 9 },
]
}
] }
];
export default class Test extends React.Component {
state = {
data,
};
render() {
return (
<React.Fragment>
<Tree defaultExpandAll={true} selectable={false}>
<TreeNode
title="root"
>
{this.renderTree(this.state.data)}
</TreeNode>
</Tree>
</React.Fragment>
);
}
renderTree = (data = [], info = { path: '', key: '' }) => {
return data.map((item, index) => (
<TreeNode title={
<React.Fragment>
{item.name}
<Button onClick={() => {
const currentKeyPath = `${info.key}.${index}`.slice(1)
this.setState(({ data }) => {
const current = get(data, currentKeyPath)
;(current.children || (current.children = [])).push({ name: '新增的節點' })
return data
})
}}>新增節點</Button>
<Button onClick={() => {
const currentKeyPath = `${info.key}`.slice(1)
this.setState(({ data }) => {
const current = get(data, currentKeyPath)
current.children.splice(index, 1)
return data
})
}}>刪除節點</Button>
<Edit
target={this.state.data}
value={item.value}
currentKeyPath={`${info.key}.${index}`.slice(1)}
setState={(state) => this.setState(state)}
/>
</React.Fragment>
}>
{this.renderTree(item.children, { path: `${info.path}/${item.name}`, key: `${info.key}.${index}` })}
</TreeNode>
));
}
}
複製代碼
不必定全部的場景都是空間換時間,只要不是頻繁操做樹結構的,只須要少許的搜索便可。樹搜索就兩種,廣度優先搜索(bfs)、深度優先搜索(dfs)
棧的規律是,先進後出;隊列的規律是,先進先出,在數組上的表現就是:
bfs是基於隊列實現,dfs是基於棧(遞歸也算是棧的一種體現)實現
對於文章最前面那個結構
const data = [
{ name: '廣東', id: 1, children: [
{ name: '深圳', id: 2, children: [
{ name: '南山區', id: 3 },
{ name: '福田區', id: 4 },
{ name: '寶安區', id: 5 },
] },
{
name: '廣州',
id: 6,
children: [
{ name: '天河區', id: 7 },
{ name: '番禺區', id: 8 },
{ name: '海珠區', id: 9 },
]
}
] }
];
複製代碼
使用bfs遍歷的順序(下文假設全是從左到右遍歷順序)是:廣東、深圳、廣州、南山區、福田區、寶安區、天河區、番禺區、海珠區;使用dfs的順序是:廣東、深圳、南山區、福田區、寶安區、廣州、天河區、番禺區、海珠區
以搜索"福田區"爲例
function bfs(target, name) {
const quene = [...target]
do {
const current = quene.shift() // 取出隊列第一個元素
current.isTravel = true // 標記爲遍歷過
if (current.children) {
quene.push(...current.children) // 子元追加到隊列後面
}
if (current.name === name) {
return current
}
} while(quene.length)
return undefined
}
複製代碼
再把renderTree方法裏面的操做取掉,加上遍歷過標紅邏輯,再加上bfs的邏輯:
componentDidMount() {
bfs(this.state.data, '福田區')
this.forceUpdate()
}
renderTree = (data = [], info = { path: '', key: '' }) => {
return data.map((item, index) => (
<TreeNode title={
<React.Fragment>
<span style={{ color: item.isTravel ? '#f00' : '#000' }}>{item.name}</span>
</React.Fragment>
}>
{this.renderTree(item.children, { path: `${info.path}/${item.name}`, key: `${info.key}.${index}` })}
</TreeNode>
));
}
複製代碼
遍歷過程是:
這種狀況能夠知足的場景:父節點所有disabled,只能對和當前等級的節點進行操做以搜索"福田區"爲例。基於前面的bfs,能夠很容易過渡到基於循環實現的dfs
function dfs(target, name) {
const quene = [...target]
do {
const current = quene.pop() // 改爲pop,取最後一個,後入先出
current.isTravel = true
if (current.children) {
quene.push(...[...current.children].reverse()) // 保證從左到右遍歷
}
if (current.name === name) {
return current
}
} while(quene.length)
return undefined
}
// 基於遞歸實現
function dfs(target = [], name) {
return target.find(x => {
x.isTravel = true
const isFind = x.name === name
return isFind ? x : dfs(x.children, name)
})
}
複製代碼
遍歷過程是:
這種方案知足的場景是:只能操做該節點的歸屬路徑,好比只能操做廣東和深圳兩個節點其餘節點disabled
先提一下,二叉樹前中後序遍歷,在代碼上的差異就在於處理語句放在哪一個位置:
function tree(node) {
if (node) {
console.log('前序遍歷')
tree(node.left)
console.log('中序遍歷')
tree(node.right)
console.log('後序遍歷')
}
}
複製代碼
對於dfs,也是有一樣的道理,咱們先把上面的改一下。以搜索"福田區"爲例
function dfs(target = [], name) {
return target.find(x => {
x.isTravel = true
const isFind = x.name === name
console.log('自上而下', x)
const ret = isFind ? x : dfs(x.children, name)
return ret
})
}
// => 廣東、深圳、南山區、福田區
// 自下而上
function dfs(target = [], name) {
return target.find(x => {
x.isTravel = true
const isFind = x.name === name
const ret = isFind ? x : dfs(x.children, name)
console.log('自下而上', x)
return ret
})
}
// => 南山區、福田區、深圳、廣東
複製代碼
大部分場景不須要講究哪一種dfs遍歷方式。若是這個數據結構有不少省,咱們想快速找到廣東省的時候,使用自上而下更容易;若是這個數據結構市下面有不少區,想快速找到屬於哪一個市則使用自下而上更容易
只要咱們按照這樣的套路,若是再來樹結構相關需求,那麼,來一個秒一個,毫無壓力
關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技