该文章主要对 TreeNode
的 onChecked
时,如何跟上级还有下级做联动所记录的
先看传入到tree下的 treeData 结构
const treeData = [
{
title: '1',
key: '1',
children: [
{
title: '1-1',
key: '1-1',
children: [
{
title: '1-1-1',
key: '1-1-1',
}
]
}
]
},
{
title: '2',
key: '2',
children: [
{
title: '2-1',
key: '2-1',
children: [
{
title: '2-1-1',
key: '2-1-1',
}
]
}
]
}
]
Tree 使用了Provider 向下级组件传递onNodeChecked 事件
源码中用 class
组件的形式写的, 下面给个例子:
export const TreeContext = React.createContext(null);
const Tree = () => {
const onNodeChecked = () => {}
return (
<TreeContext.Provider
value={{
onNodeChecked
}}
>
<NodeList>
<TreeContext.Provider>
)
}
消费provider, 类组件例子
const ContextTreeNode: React.FC<TreeNodeProps> = props => (
<TreeContext.Consumer>
{context => <InternalTreeNode {...props} context={context} />}
</TreeContext.Consumer>
);
函数组件,消费context
const TreeNode = () => {
const context = useContext(TreeContext)
return (
<div />
)
}
在Tree 接收到treeData 这个props 的时候会进行数据转换
在源码中,在 getDerivedStateFromProps
这个生命周期对treeData 进行转换。
class Tree extends Component {
static getDerivedStateFromProps(props, prevState) {
const { treeData, fieldNames } = props;
const newState = { }
if (treeData) {
newState.treeData = treeData;
const entitiesMap = convertDataToEntities(treeData, { fieldNames });
newState.keyEntities = {
...entitiesMap.keyEntities,
};
}
}
}
convertDataToEntities 将 treeData 的key 当作键名,value 是当前节点,另外添加他的父节点,最后数据结构应该是这样:
以上面treeData 为例子
{
1: {
title: '1',
key: '1',
parentNode: null,
children: [
{
title: '1-1',
key: '1-1',
children: [
{
title: '1-1-1',
key: '1-1-1',
}
]
}
]
},
'1-1': {
title: '1-1',
parentNode: '1',
key: '1-1',
children: [
{
title: '1-1-1',
key: '1-1-1',
}
]
}
'1-1-1': {
parentNode: '1-1',
title: '1-1-1',
key: '1-1-1',
}
}
- 我们尝试自己写一下这个方法
// 建立双链表形式的map数据,
const convertDataToEntities = (treeData) => {
const result = {};
formatedTreeData(treeData, result, 0, 0);
console.log(result);
return {
keyEntities: result
}
}
const formatedTreeData = (treeData, hash, parentKey, level) => {
const parentNode = hash[parentKey] || null;
treeData.forEach((_item) => {
hash[_item.key] = {
..._item,
parentNode: parentNode,
level: level,
}
if (_item.children) {
formatedTreeData(_item.children, hash, _item.key, level + 1)
}
})
}
// const { keyEntities } = convertDataToEntities(treeData)
接下来实现 onNodeChecked, 选中当前节点时他的下级节点,以及上级节点联动关系
先根据
keyEntities
获取当前树的最大深度, 因为我们当前keyEntities 已经保存有树的深度了,所以只要一次遍历就可以获取到最大的深度
在遍历过程中同时对每一层节点进行保存, 即第一层有哪些节点,第二层有哪些节点,我们命名为levelMap
然后维护chekedKeys 这个已选中的节点
从最开始那层 从上而下,遍历每一层节点, 如果当前checkedKeys 包含 当前levelMap[level][item] 那么他的children 应该也需要添加到checkedKeys 里面
从最后那层 由下而上,遍历每层节点,如果当前checkedKeys 包含当前节点的所有children,那么他的父级节点需要被添加到checkedKeys 里面
const onNodeChecked = (e, checkedNode, isChecked) => {
const { levelMap, maxLevel } = getLevelEntities(keyEntities);
if (isChecked) {
// const keys = [...checkedKeys, checkedNode.key];
const keys = ['1-2']
const { halfChecked, checkedKeys } = fillConductCheck(keys, levelMap, maxLevel);
console.log(checkedKeys); // 会发现 [1-1-1, 1-1, 1]
}
}
const getLevelEntities = (keyEntities) => {
let maxLevel = 0
const levelMap = {}
Object.keys(keyEntities).forEach((_item) => {
const current = keyEntities[_item];
const { level } = current;
maxLevel = Math.max(maxLevel, level);
if (typeof levelMap[level] === 'undefined') {
levelMap[level] = [];
}
levelMap[level].push(current);
})
return { levelMap, maxLevel }
}
const fillConductCheck = (keys, levelEntities, maxLevel) => {
const checkedKeys = new Set(keys);
const halfCheckedKeys = new Set();
// 从上而下 勾选
for (let level = 0; level <= maxLevel; level += 1) {
const entities = levelEntities[level] || new Set();
entities.forEach(entity => {
const { key, children = [] } = entity;
if (checkedKeys.has(key)) {
children
.forEach(childEntity => {
checkedKeys.add(childEntity.key);
});
}
});
}
const visitedKeys = new Set();
for (let level = maxLevel; level >= 0; level -= 1) {
const entities = levelEntities[level] || new Set();
entities.forEach(entity => {
const { parentNode, node } = entity;
// Skip if no need to check
if (!entity.parentNode || visitedKeys.has(entity.parentNode.key)) {
return;
}
let allChecked = true;
let partialChecked = false;
(parentNode.children || [])
.forEach(({ key }) => {
const checked = checkedKeys.has(key);
if (allChecked && !checked) {
allChecked = false;
}
if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
partialChecked = true;
}
});
if (allChecked) {
checkedKeys.add(parentNode.key);
}
if (partialChecked) {
halfCheckedKeys.add(parentNode.key);
}
visitedKeys.add(parentNode.key);
});
}
return {
checkedKeys: Array.from(checkedKeys),
halfCheckedKeys: halfCheckedKeys,
};
}