предисловие
Многие люди испытывают головную боль и сложную логику при столкновении с требованиями древовидных компонентов, кроме отображения, есть еще логика добавления или удаления поиска. Как правило, компонент дерева имеет несколько уровней.Если текущий уровень имеет следующий уровень, будет что-то вроде
children、listи другие атрибуты, структура данных обычно
const tree = [
{
name: 'a',
id: 1,
},
{
name: 'b',
id: 2,
children: [
{
name: 'c',
id: 3
}
]
},
]
Это, вероятно, интерфейс:
Вот следующий источник данных:
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 все будут делать так:
// 放在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>
);
}
Сначала используйте имя в качестве заголовка узла, а затем, если есть дочерние узлы, используйте тот же метод для отображения дочерних узлов.
Компонент готов, если мы хотим щелкнуть, как мы узнаем, какой узел на каком уровне щелкнул? Будете ли вы писать алгоритм поиска, передавать текущий идентификатор узла, а затем возвращаться назад, чтобы записать путь и отобразить его? Хотя это и можно сделать, но это явно неэлегантно Мы можем значительно оптимизировать этот процесс, пожертвовав пространством ради времени.В процессе обхода информация об узле передается следующей рекурсивной функции..
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>
));
}
Теперь, какой бы из них мы ни щелкнули, печатает текущий путь к узлу.
Добавление, удаление, изменение и проверка операций
Если мы сталкиваемся с добавлениями, удалениями, исправлениями и запросами на основе предыдущих условий, мы записали информацию, которая будет использоваться, поэтому мы можем использовать эту информацию для выполнения дополнений, удалений и изменений.
Нажмите, чтобы просмотреть общие правила CRUD
- Добавлено: необходимо знать идентификатор родительского узла (parent.push).
- Удалить: необходимо знать идентификатор родительского узла и идентификатор текущего узла (parent.splice(child))
- Изменение: необходимо знать идентификатор родительского узла и идентификатор текущего узла (Father. Sub = NewVal)
- Проверить: нужно знать идентификатор родительского узла ((родительский) => родитель. все дочерние элементы)
Фон вообще id, а фронтенд вообще ключ
Удалим кнопку прямо сейчас, удалим id (потому что мы используем только фронтальный тест, только ключ, если нужно передать его в фон, то для передачи id нужно следовать приведенным выше правилам), а затем используйте тот же метод для записи каждого ключа слоя
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]дети толкают новый элемент. Так что мы также должны написать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Измените метод onclick внутри:
<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>
<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>
После того, как мы добавим новый узел, первое, что нужно сделать, это изменить имя системы по умолчанию.Изменение и удаление аналогичны, но для изменения необходимо сохранить поле ввода для заполнения имени нового узла. Обычный метод заключается в управлении другим модальным компонентом, в котором есть вход. Нажмите OK, чтобы изменить. Для лучшего опыта я обычно изменяю прямо в строке. Сначала напишите компонент редактирования. Этот компонент обычно представляет собой кнопку. При нажатии он становится элементом ввода. Когда фокус теряется, модификация завершается.
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
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).
стеки и очереди
Закон стека — первый пришел, последний вышел; закон очереди — первый пришел — первый ушел, а производительность массива такова:
- Стек: arr.push(item); arr.pop()
- Очередь: arr.push(item); arr.shift()
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 это: Гуандун, Шэньчжэнь, район Наньшань, район Футянь, район Баоань, Гуанчжоу, район Тяньхэ, район Панью, район Хайчжу.
bfs
Возьмем, к примеру, поиск «Район Футянь».
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>
));
}
Процесс обхода таков:
dfs
Возьмем, к примеру, поиск «Район Футянь». Основываясь на предыдущем 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)
})
}
Процесс обхода таков:
Сценарий, которому удовлетворяет это решение: может работать только домашний путь узла, например, могут работать только два узла в Гуандуне и Шэньчжэне, а другие узлы отключены.
нисходящая глубина в глубину и восходящая глубина в глубину
Позвольте мне сначала упомянуть, что разница в коде обхода бинарного дерева заключается в том, где находится оператор обработки:
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. Если в этой структуре данных много провинций, проще использовать метод «сверху вниз», когда мы хотим быстро найти провинцию Гуандун; если в структуре данных города много районов, проще использовать метод «снизу вверх», чтобы быстро найти город принадлежит
Суммировать
- Встречайте компоненты древовидной структуры, давайте воспользуемся рекурсивным рендерингом
- Во время рекурсивного обхода запишите текущую информацию об узле в узел и доведите текущую информацию об узле до параметров следующей рекурсивной функции для последующих операций творога.
- Если информация об узле не записана в узле заранее во время рекурсивного рендеринга, некоторые последующие специальные операции должны использовать bfs или dfs.
- Наконец вТраверс во время записи информацииа такжеНе записывайте информацию, а затем используйте dfs и bfsКакой вариант лучше?
- Если вы используете dfs, вы также можете подумать, что лучше, нисходящий dfs или восходящий dfs
Пока мы следуем этой процедуре, если есть другое требование, связанное с древовидной структурой, то одна секунда, без давления
Обратите внимание на официальный аккаунт «Другой интерфейс», изучите интерфейс с другой точки зрения, быстро растем, играйте в новейшие технологии и исследуйте различные черные технологии вместе.