feature(性能):优化大量节点时卡顿问题。主要是通过减少布局循环、仅处理变动节点和连接线只画可见区域得线来解决。增加shift多选功能。

This commit is contained in:
ligz 2024-12-30 14:47:24 +08:00
parent 4332abce4d
commit 44cd5aad96
5 changed files with 200 additions and 46 deletions

View File

@ -681,7 +681,8 @@ class Render {
const data = this.mindMap.command[type](step)
if (data) {
this.renderTree = data
this.mindMap.render()
// 给前进后退增加类型 主要是当它这样操作时全量渲染 暂时未作两次数据得变更比对 这个是简单临时方案
this.mindMap.render(()=>{},"HISTORY_RECORD")
}
this.mindMap.emit('data_change', data)
}
@ -987,7 +988,12 @@ class Render {
const parent = node.parent
// 获取当前节点所在位置
const index = getNodeDataIndex(node)
//这里主要是 节点还未创建 只能记录下创建在父节点的位置等创建好了再添加
newNode.data.parentIndex = index
parent.nodeData.children.splice(index, 1, newNode)
//这里主要是 节点还未创建 只能清空父节点然后记录下父节点的uid等创建好了替换
node.parent = null;
node.parentUid = newNode.data.uid
})
// 如果同时对多个节点插入子节点,需要清除原来激活的节点
if (focusNewNode) {
@ -1330,6 +1336,10 @@ class Render {
// 目标节点
let existParent = exist.parent
let existBorthers = existParent.children
item.layerIndex =exist.layerIndex
item.parent =exist.parent
item.getSize()
item.needLayout = true
let existIndex = getNodeIndexInNodeList(exist, existBorthers)
if (existIndex === -1) {
return
@ -1432,6 +1442,22 @@ class Render {
} else {
const parent = node.parent
const index = getNodeDataIndex(node)
const children = node.children || [];
// 对子节点实例也操作
children.forEach((item)=>{
item.layerIndex =node.layerIndex
item.parent =node.parent
if(item.layerIndex === 1){
// 如果删除父节点后自己点变成1级节点 重新计算样式
item.getSize()
item.needLayout = true
}
})
parent.children.splice(
index,
1,
...children
)
parent.nodeData.children.splice(
index,
1,
@ -1526,11 +1552,16 @@ class Render {
return !item.isRoot
})
nodeList.forEach(item => {
item.layerIndex = toNode.layerIndex + 1
this.removeNodeFromActiveList(item)
removeFromParentNodeData(item)
toNode.setData({
expand: true
})
item.parent = toNode
item.getSize()
item.needLayout = true
toNode.children.push(item)
toNode.nodeData.children.push(item.nodeData)
})
this.emitNodeActiveEvent()

View File

@ -164,7 +164,11 @@ class MindMapNode {
this.updateGeneralization()
this.initDragHandle()
}
//子节点中是否包含这个节点
isHaveNode(node){
const children = this.children
return children.includes(node)
}
// 支持自定义位置
get left() {
return this.customLeft || this._left
@ -688,6 +692,41 @@ class MindMapNode {
](this, true)
this.renderer.emitNodeActiveEvent(isActive ? null : this)
}
// 使用shift键进行多选和取消多选
if (!readonly && e.shiftKey && enableCtrlKeyNodeSelection) {
this.isMultipleChoice = true
let isActive = this.getData('isActive')
if (!isActive){
const activeNodeList = this.renderer.activeNodeList
if(activeNodeList.length){
const children = this.parent.children
const currentIndex = children.indexOf(this)
let max = currentIndex
let min = currentIndex
let isAllChildren = true
for (let i = 0; i < activeNodeList.length; i++) {
let item = activeNodeList[i]
const ind = children.indexOf(item)
if(ind < 0){
isAllChildren = false
break;
} else{
ind >= max ? max = ind : null
min >= ind ? min = ind : null
}
}
if(isAllChildren){
let c = this.parent.children.slice(min,max+1)
c.forEach(child => {
this.mindMap.renderer[
'addNodeToActiveList'
](child, true)
this.renderer.emitNodeActiveEvent(child)
})
}
}
}
}
this.mindMap.emit('node_mousedown', this, e)
})
this.group.on('mouseup', e => {
@ -748,8 +787,11 @@ class MindMapNode {
if (
!(this.getData('isActive') && this.renderer.activeNodeList.length === 1)
) {
this.renderer.clearActiveNodeList()
this.active(e)
// 多选的时候 右键在已选中的节点上时 不取消其它节点的选中状态
if(!this.getData('isActive')){
this.renderer.clearActiveNodeList()
this.active(e)
}
}
this.mindMap.emit('node_contextmenu', e, this)
})
@ -873,14 +915,23 @@ class MindMapNode {
this.updateDragHandle()
}
}
// 渲染父节点的连接线 只有不可见得父节点才会去渲染 这样可以做到当只渲染可见连线时 出现单个节点没有连接父节点得情况
renderParentLine(node,performanceConfig){
const parNode = node.parent
if(!parNode) return
const isInClient = parNode.checkIsInClient(performanceConfig.padding)
//如果父节点可见 就不继续遍历
if(isInClient) return
parNode.renderLine()
this.renderParentLine(parNode,performanceConfig)
}
// 递归渲染
// forceRender强制渲染无论是否处于画布可视区域
// async异步渲染
render(callback = () => {}, forceRender = false, async = false) {
// 节点
// 重新渲染连线
this.renderLine()
// 不在全量渲染
//this.renderLine()
const { openPerformance, performanceConfig } = this.mindMap.opt
// 强制渲染、或没有开启性能模式、或不在画布可视区域内不渲染节点内容
// 根节点不进行懒加载,始终渲染,因为滚动条插件依赖根节点进行计算
@ -890,6 +941,10 @@ class MindMapNode {
this.checkIsInClient(performanceConfig.padding) ||
this.isRoot
) {
//给不可见的父节点画线 防止出现单独的节点没有连线问题
this.renderParentLine(this,performanceConfig)
//只给屏幕内的节点画线
this.renderLine()
if (!this.group) {
// 创建组
this.group = new G()

View File

@ -79,7 +79,8 @@ class Base {
}
// 创建节点实例
createNode(data, parent, isRoot, layerIndex, index, ancestors) {
// 增加参数isNotReset本质是不想让节点reset 但是目前只改了 logicalStructure结构的代码 为了不影响其它结构代码增加此参数做判断
createNode(data, parent, isRoot, layerIndex, index, ancestors,isNotReset) {
// 创建节点
// 库前置内容数据
const nodeInnerPrefixData = {}
@ -105,12 +106,21 @@ class Base {
newNode.layerIndex,
layerIndex
)
newNode.reset()
// 不再重置节点 节点的子节点添加 删除等 分别再对应的操作里处理 这样可以做到只针对修改节点进行处理而不是全量处理
if(!isNotReset){
newNode.reset()
}
newNode.layerIndex = layerIndex
if (isRoot) {
newNode.isRoot = true
} else {
newNode.parent = parent._node
// 这里是只增加父级节点时 操作代码 只进行了数据上的添加父节点 而还没实际创建父节点所以记录父节点UId
// 因为遍历从上而下所以此时父节点是肯定创建好的。所以此时可正常将父节点实例给赋值。
if(!newNode.parentUid){
newNode.parent = parent._node
}else{
newNode.parent = this.renderer.nodeCache[newNode.parentUid]
}
}
this.cacheNode(data._node.uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
@ -160,13 +170,22 @@ class Base {
newNode.layerIndex,
layerIndex
)
newNode.reset()
// 不再重置节点 节点的子节点添加 删除等 分别再对应的操作里处理 这样可以做到只针对修改节点进行处理而不是全量处理
if(!isNotReset){
newNode.reset()
}
newNode.nodeData = newNode.handleData(data || {})
newNode.layerIndex = layerIndex
if (isRoot) {
newNode.isRoot = true
} else {
newNode.parent = parent._node
// 这里是只增加父级节点时 操作代码 只进行了数据上的添加父节点 而还没实际创建父节点所以记录父节点UId
// 因为遍历从上而下所以此时父节点是肯定创建好的。所以此时可正常将父节点实例给赋值。
if(!newNode.parentUid){
newNode.parent = parent._node
}else{
newNode.parent = this.renderer.nodeCache[newNode.parentUid]
}
}
this.cacheNode(uid, newNode)
this.checkIsLayoutChangeRerenderExpandBtnPlaceholderRect(newNode)
@ -233,7 +252,16 @@ class Base {
this.root = newNode
} else {
// 互相收集
parent._node.addChildren(newNode)
if( !parent._node.isHaveNode(newNode)){
// 因为插入父级节点的时候还没创建节点还没创建号所以记录下要插入的父节点位置 创建好的时候再插入
// 这里同子节点关联父节点一样都是因为插入父节点的特殊性导致单独处理
const parentIndex = data.data.parentIndex
if(parentIndex || parentIndex === 0){
parent._node.children.splice(parentIndex, 1, newNode)
}else{
parent._node.addChildren(newNode)
}
}
}
return newNode
}

View File

@ -28,15 +28,38 @@ class LogicalStructure extends Base {
]
asyncRun(task)
}
/**
* 根据节点数组 获取 节点的UId以及父节点的UId 主要是用于将改动范围缩小到 当前激活节点和父节点 而不是全量变动
* @param arr
* @returns {*[]}
*/
getUidAndParentUid(arr){
const uidList = []
if(arr && arr.length){
arr.forEach(item=>{
uidList.push(item.uid)
if(item.parent){
uidList.push(item.parent.uid)
}
})
}
return uidList;
}
// 遍历数据计算节点的left、width、height
computedBaseValue() {
let sortIndex = 0
const activeUiDList = [...this.getUidAndParentUid(this.renderer.activeNodeList),...this.getUidAndParentUid(this.renderer.lastActiveNodeList)]
walk(
this.renderer.renderTree,
null,
(cur, parent, isRoot, layerIndex, index, ancestors) => {
let newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors)
let newNode = cur?._node
// 只有变动的节点 结构改变 和 执行撤销和前进的操作才进入创建节点的操作
if(!newNode || activeUiDList.includes(cur.data.uid) || this.checkIsNeedResizeSources() || this.renderer.renderSource === CONSTANTS.CHANGE_LAYOUT || this.renderer.renderSource === "HISTORY_RECORD"){
newNode = this.createNode(cur, parent, isRoot, layerIndex, index, ancestors,this.renderer.renderSource !== "HISTORY_RECORD")
}
// 将缓存节点移至外边去缓存。保证每个节点缓存都是最新的
this.cacheNode(cur.data.uid,newNode)
newNode.sortIndex = sortIndex
sortIndex++
// 根节点定位在画布中心位置
@ -62,12 +85,15 @@ class LogicalStructure extends Base {
(cur, parent, isRoot, layerIndex) => {
// 返回时计算节点的areaHeight也就是子节点所占的高度之和包括外边距
let len = cur.data.expand === false ? 0 : cur._node.children.length
// 这里是一次性计算出所有节点得childrenAreaHeight如果自身比子节点高度高则取自身高度 包括没有子节点得节点它的childrenAreaHeight为自身高度+边距)
// 节点间的上下间距 也不在多计算 如3个子节点时 只加2个间距而不是之前的3个间距 因为父节点间距的存在 所以不需要最后一个子节点再加间距
// 这个改动是为了一次性计算出节点的布局 不用在单独遍历计算了
cur._node.childrenAreaHeight = len
? cur._node.children.reduce((h, item) => {
return h + item.height
}, 0) +
(len + 1) * this.getMarginY(layerIndex + 1)
: 0
return h + (item.childrenAreaHeight > item.height ? item.childrenAreaHeight : item.height)
}, 0) + (len-1)* this.getMarginY(layerIndex + 1)
: cur._node.height + this.getMarginY(layerIndex + 1)
// 如果存在概要,则和概要的高度取最大值
let generalizationNodeHeight = cur._node.checkHasGeneralization()
? cur._node._generalizationNodeHeight +
@ -90,14 +116,25 @@ class LogicalStructure extends Base {
null,
(node, parent, isRoot, layerIndex) => {
if (node.getData('expand') && node.children && node.children.length) {
let marginY = this.getMarginY(layerIndex + 1)
// 第一个子节点的top值 = 该节点中心的top值 - 子节点的高度之和的一半
let top = node.top + node.height / 2 - node.childrenAreaHeight / 2
let totalTop = top + marginY
node.children.forEach(cur => {
cur.top = totalTop
totalTop += cur.height + marginY
})
// 这里的算法就一次性计算出所有节点的正确位置
// 第一个子节点的top值 = 该节点中心的top值 + 自身节点一半 - 子节点的高度之和的一半
let totalTop = node.top + node.height / 2 - node.childrenAreaHeight / 2
for (let i = 0; i < node.children.length; i++) {
let cur = node.children[i]
let nextNode = node.children[i+1]
// 这里只计算出节点的最高处的坐标 如果子节点高就是子节点的最高处为基准计算 如果是自身高 就以自身为基准计算
if( cur.height > cur.childrenAreaHeight){
cur.top = totalTop
totalTop += cur.height
}else{
cur.top = totalTop + cur.childrenAreaHeight / 2 - cur.height/2
totalTop += cur.childrenAreaHeight
}
// 如果还有下一个节点 那么就加上上下的间距
if(nextNode){
totalTop += this.getMarginY(layerIndex + 1)
}
}
}
},
null,
@ -107,25 +144,26 @@ class LogicalStructure extends Base {
// 调整节点top
adjustTopValue() {
walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.getData('expand')) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
},
null,
true
)
// 这一步 由于以上的改动 可以去掉了
/* walk(
this.root,
null,
(node, parent, isRoot, layerIndex) => {
if (!node.getData('expand')) {
return
}
// 判断子节点所占的高度之和是否大于该节点自身,大于则需要调整位置
let difference =
node.childrenAreaHeight2 -
this.getMarginY(layerIndex + 1) * 2 -
node.height
if (difference > 0) {
this.updateBrothers(node, difference / 2)
}
},
null,
true
)*/
}
// 更新兄弟节点的top

View File

@ -1160,6 +1160,8 @@ export const removeFromParentNodeData = node => {
if (!node || !node.parent) return
const index = getNodeDataIndex(node)
if (index === -1) return
// 这里同步将实例中的关联关系也删除。此改动是因为 不再createNode方法中做重置所以在这删除操作中单独处理
node.parent.children.splice(index, 1)
node.parent.nodeData.children.splice(index, 1)
}