网站首页 > 技术文章 正文
TypeScript 中的树节点
最初,我们需要为树节点定义一个类型。它将是一个通用类型,可用于任何类型的数据,并且仅包含两个字段:value和children。Value是一个泛型类型T,并且children是一个数组TreeNode<T>。
export interface TreeNode<T> {
value: T
children: TreeNode<T>[]
}
为了定位特定节点,我们将使用该getTreeNode函数遍历树到所需的节点。路径是一个数字数组,每个数字都是子节点的索引。
export function getTreeNode<T>(tree: TreeNode<T>, path: number[]): TreeNode<T> {
return path.reduce((node, i) => node.children[i], tree)
}
为了检索给定节点的所有值,我们将使用该getTreeValues函数。该函数返回特定节点及其所有子节点的值数组。
export function getTreeValues<T>(tree: TreeNode<T>): T[] {
return [tree.value, ...tree.children.flatMap(getTreeValues)]
}
我们现在可以定义我们的习惯树。节点的数据将由 表示,其中HabitTreeNodeValue包含习惯类别的 id,例如“健康”、“关系”、“工作”等。这还有一个可选的习惯 id 数组和可选颜色。例如,“幸福”类别没有自己的习惯;相反,它是该领域定义的其他习惯的组合children。
import { TreeNode } from "@reactkit/utils/tree"
import { HabitId } from "./habits"
export interface HabitTreeNodeValue {
id: string
habits?: HabitId[]
color?: number
}
export interface HabitTreeNode extends TreeNode<HabitTreeNodeValue> {}
export const habitTree: HabitTreeNode = {
value: {
id: "happiness",
color: 5,
},
children: [
{
value: {
id: "health",
color: 4,
},
children: [
{
value: {
id: "sleep",
habits: [
"sunlight",
"limitCoffee",
"noAlcohol",
"earlySleep",
"noLateFood",
"noWorkAfterDinner",
"noElectronicsInBedroom",
],
},
children: [],
},
{
value: {
id: "nutrition",
habits: ["morningFast", "noLateFood", "supplements", "content"],
},
children: [],
},
{
value: {
id: "body",
habits: ["outdoors", "exercise", "walk"],
},
children: [],
},
{
value: {
id: "mind",
habits: [
"meditation",
"learn",
"max",
"noWorkAfterDinner",
"noElectronicsInBedroom",
],
},
children: [],
},
],
},
{
value: {
id: "relationships",
color: 11,
},
children: [
{
value: {
id: "marriage",
habits: [
"compliment",
"review",
"help",
"noWorkAfterDinner",
"noElectronicsInBedroom",
],
},
children: [],
},
],
},
{
value: {
id: "work",
color: 2,
},
children: [
{
value: {
id: "productivity",
habits: [
"noWorkAfterDinner",
"sunlight",
"limitCoffee",
"noAlcohol",
"earlySleep",
"morningFast",
"prepare",
"noEarlyCoffee",
"noLateFood",
"outdoors",
"exercise",
"noElectronicsInBedroom",
],
},
children: [],
},
],
},
],
}
接下来,我们找到了一系列不知道它们正在树中使用的习惯。它们只是唯一习惯 ID 和有关它们的信息的列表,其中包括emoji、name和description。
使用 React 嵌套过滤器
借助 React,我们现在可以实现嵌套过滤器。当前类别以数字数组的形式存储在钩子中useState。“幸福”类别将是一个空数组,“健康”类别将是[0],“睡眠”类别将是[0, 0],依此类推。
import { capitalizeFirstLetter } from "@reactkit/utils/capitalizeFirstLetter"
import { getTreeNode, getTreeValues } from "@reactkit/utils/tree"
import { withoutDuplicates } from "@reactkit/utils/array/withoutDuplicates"
import { HStack, VStack } from "@reactkit/ui/ui/Stack"
import { TreeFilter } from "@reactkit/ui/ui/tree/TreeFilter"
import { useState, useMemo } from "react"
import styled from "styled-components"
import { HabitTreeNode, habitTree } from "./data/habitTree"
import { habitRecord } from "./data/habits"
import { Text } from "@reactkit/ui/ui/Text"
import { HabitItem } from "./HabitItem"
const Container = styled(HStack)`
width: 100%;
flex-wrap: wrap;
gap: 40px;
align-items: start;
`
const Content = styled(VStack)`
gap: 20px;
flex: 1;
`
const FilterWrapper = styled.div`
position: sticky;
top: 0;
`
const getCategoriesColors = (
{ value, children }: HabitTreeNode,
parentColor?: number
): Record<string, number | undefined> => {
const color = value.color ?? parentColor
return {
[value.id]: color,
...children.reduce(
(acc, child) => ({
...acc,
...getCategoriesColors(child, color),
}),
{}
),
}
}
const defaultColor = 3
export const CuratedHabits = () => {
const [path, setPath] = useState<number[]>([])
const values = useMemo(() => getTreeValues(habitTree), [])
const categoryColorRecord = useMemo(() => getCategoriesColors(habitTree), [])
const node = getTreeNode(habitTree, path)
const habits = withoutDuplicates(
getTreeValues(node).flatMap((value) => value.habits || [])
)
.map((id) => ({
id,
...habitRecord[id],
}))
.map((habit) => ({
...habit,
tags: values
.filter((value) => value.habits?.includes(habit.id))
.map((value) => ({
name: value.id,
color: categoryColorRecord[value.id] ?? defaultColor,
})),
}))
return (
<Container>
<FilterWrapper>
<TreeFilter
tree={habitTree}
renderName={(value) => capitalizeFirstLetter(value.id)}
value={path}
onChange={setPath}
/>
</FilterWrapper>
<Content>
<Text weight="bold" size={24}>
{capitalizeFirstLetter(node.value.id)} habits{" "}
<Text as="span" color="supporting">
({habits.length})
</Text>
</Text>
{habits.map((habit) => (
<HabitItem {...habit} key={habit.id} />
))}
</Content>
</Container>
)
}
使用该getTreeValues函数,我们将获得树中的所有习惯。每个习惯都有一个彩色标签,但并非所有习惯都定义了颜色字段。它仅存在于类别级别。因此,我们将使用getCategoriesColors来获取类别 ID 及其颜色的记录。这是一个递归函数,如果子类别没有定义自己的颜色,则将父类别的颜色分配给子类别。
为了获取当前节点,我们使用该getTreeNode函数。有些习惯可能存在于多个类别中。例如,“起床后看阳光”的习惯既属于“睡眠”又属于“生产力”类别。我们不想将其显示两次,因此我们使用该withoutDuplicates函数删除重复项。然后我们为每个独特的习惯添加标签列表。标签代表习惯所属的类别。我们使用categoryColorRecord来获取类别的颜色。
export function withoutDuplicates<T>(
items: T[],
areEqual: (a: T, b: T) => boolean = (a, b) => a === b
): T[] {
const result: T[] = []
items.forEach((item) => {
if (!result.find((i) => areEqual(i, item))) {
result.push(item)
}
})
return result
}
为了显示习惯,我们从habits数组中提取并利用该HabitItem组件。TreeFilter我们将依赖通用组件来过滤习惯。
import { useState, Fragment } from "react"
import styled, { useTheme } from "styled-components"
import { Circle } from "../Circle"
import { NonEmptyOnly } from "../NonEmptyOnly"
import { VStack, HStack } from "../Stack"
import { defaultTransitionCSS } from "../animations/transitions"
import { getVerticalPaddingCSS } from "../utils/getVerticalPaddingCSS"
import { Text } from "../Text"
import { handleWithStopPropagation } from "../../shared/events"
import { InputProps } from "../../props"
import { TreeNode } from "@reactkit/utils/tree"
interface TreeFilterProps<T> extends InputProps<number[]> {
tree: TreeNode<T>
renderName: (value: T) => string
}
const Content = styled(VStack)`
margin-left: 20px;
`
const Container = styled(VStack)`
cursor: pointer;
`
const Item = styled(HStack)`
${getVerticalPaddingCSS(4)}
align-items: center;
gap: 8px;
${defaultTransitionCSS}
`
export function TreeFilter<T>({
tree,
renderName,
value,
onChange,
}: TreeFilterProps<T>) {
const [hovered, setHovered] = useState<number[] | undefined>()
const { colors } = useTheme()
const recursiveRender = (node: TreeNode<T>, path: number[]) => {
const isSelected = value.every((v, i) => v === path[i])
let color = isSelected ? colors.text : colors.textShy
if (hovered) {
const isHovered = hovered.every((v, i) => v === path[i])
color = isHovered ? colors.text : colors.textShy
}
return (
<Container
onClick={handleWithStopPropagation(() => onChange(path))}
onMouseEnter={() => setHovered(path)}
onMouseLeave={() => {
setHovered(
path.length === 0 ? undefined : path.slice(0, path.length - 1)
)
}}
>
<Item
style={{
color: color.toCssValue(),
}}
>
<Circle
size={8}
background={isSelected ? colors.primary : colors.transparent}
/>
<Text weight="bold">{renderName(node.value)}</Text>
</Item>
<NonEmptyOnly
array={node.children}
render={(items) => (
<Content>
{items.map((child, index) => (
<Fragment key={index}>
{recursiveRender(child, [...path, index])}
</Fragment>
))}
</Content>
)}
/>
</Container>
)
}
return <>{recursiveRender(tree, [])}</>
}
它的 props 扩展了 generic InputProps,由value和onChangeprops 组成。在这种情况下,该值将是节点的路径。我们还必须将整个树传递给组件和一个将呈现节点名称的函数。
当我们渲染树结构时,我们无法避免递归。在函数中,我们通过比较prop 和参数recursiveRender来检查当前节点是否被选择。然后,我们根据筛选项的选择状态对它们应用不同的样式。我们还通过更改状态来更新项目悬停时的颜色。该函数用于防止点击事件向上冒泡到父元素。valuepathhoveredhandleWithStopPropagation
我们在组件内渲染节点的子节点Content,这将它们与父节点缩进,左侧有 20 像素的边距。由于我们不想Content在没有子组件时渲染组件,因此我们使用一个名为 的小辅助组件,NonEmptyOnly仅在子组件存在时才渲染它们。
import { ReactNode } from "react"
interface NonEmptyOnlyProps<T> {
array?: T[]
render: (array: T[]) => ReactNode
}
export function NonEmptyOnly<T>({ array, render }: NonEmptyOnlyProps<T>) {
if (array && array.length > 0) {
return <>{render(array)}</>
}
return null
}
- 上一篇: 浏览器事件模型(浏览器的事件机制)
- 下一篇: JavaScript动态绑定事件与静态绑定事件的区别
猜你喜欢
- 2024-09-20 Vue.js常见的20道前端面试题及答案
- 2024-09-20 AI面试官:我能胜任React(二)(我能胜任这份工作吗)
- 2024-09-20 70个JavaScript知识点详细总结(上)【实践】
- 2024-09-20 ReactDOM.render和ReactDOM.createPortal的区别
- 2024-09-20 学会使用Vue JSX,一车老干妈都是你的
- 2024-09-20 字节P8大佬爆肝整理,一文带你梳理React面试题!
- 2024-09-20 2024前端面试真题之—VUE篇(2020web前端经典面试题 vue)
- 2024-09-20 JS事件绑定的常用方式实例总结(js事件绑定的四种方式)
- 2024-09-20 JavaScript 的基本术语大全(javascript的基本概念)
- 2024-09-20 React18内核探秘:手写React高质量源码迈向高阶开发(超清完结)
- 1514℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 563℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 508℃MySQL service启动脚本浅析(r12笔记第59天)
- 486℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 485℃启用MySQL查询缓存(mysql8.0查询缓存)
- 465℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 445℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 442℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)