From 2dee415a64e2a6b67140ec13193b354482ac54ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A1=97=E8=A7=92=E5=B0=8F=E6=9E=97?= <1013335014@qq.com> Date: Thu, 25 Jul 2024 16:40:47 +0800 Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/index.js | 14 ++- .../src/constants/defaultOptions.js | 8 ++ simple-mind-map/src/core/render/Render.js | 45 ++++++-- simple-mind-map/src/core/render/node/Node.js | 104 +++++++++--------- .../core/render/node/nodeGeneralization.js | 4 +- simple-mind-map/src/plugins/OuterFrame.js | 7 ++ simple-mind-map/src/utils/index.js | 34 +++--- 7 files changed, 135 insertions(+), 81 deletions(-) diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index 6e445c07..6dc28a83 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -194,7 +194,7 @@ class MindMap { this.renderer.reRender = true // 标记为重新渲染 this.renderer.clearCache() // 清空节点缓存池 this.clearDraw() // 清空画布 - this.render(callback, (source = '')) + this.render(callback, source) } // 获取或更新容器尺寸位置信息 @@ -288,7 +288,9 @@ class MindMap { // 更新配置 updateConfig(opt = {}) { + this.emit('before_update_config', this.opt) this.opt = this.handleOpt(merge.all([defaultOpt, this.opt, opt])) + this.emit('after_update_config', this.opt) } // 获取当前布局结构 @@ -379,6 +381,9 @@ class MindMap { // 导出 async export(...args) { try { + if (!this.doExport) { + throw new Error('请注册Export插件!') + } let result = await this.doExport.export(...args) return result } catch (error) { @@ -416,6 +421,11 @@ class MindMap { addContentToFooter, node } = {}) { + const { watermarkConfig, openPerformance } = this.opt + // 如果开启了性能模式,那么需要先渲染所有节点 + if (openPerformance) { + this.renderer.forceLoadNode(node) + } const { cssTextList, header, headerHeight, footer, footerHeight } = handleGetSvgDataExtraContent({ addContentToHeader, @@ -459,7 +469,7 @@ class MindMap { if (!ignoreWatermark && hasWatermark) { this.watermark.isInExport = true // 是否是仅导出时需要水印 - const { onlyExport } = this.opt.watermarkConfig + const { onlyExport } = watermarkConfig // 是否需要重新绘制水印 const needReDrawWatermark = rect.width > origWidth || rect.height > origHeight diff --git a/simple-mind-map/src/constants/defaultOptions.js b/simple-mind-map/src/constants/defaultOptions.js index fd8ff017..4874af62 100644 --- a/simple-mind-map/src/constants/defaultOptions.js +++ b/simple-mind-map/src/constants/defaultOptions.js @@ -228,6 +228,14 @@ export const defaultOpt = { // 自定义超链接的跳转 // 如果不传,默认会以新窗口的方式打开超链接,可以传递一个函数,函数接收两个参数:link(超链接的url)、node(所属节点实例),只要传递了函数,就会阻止默认的跳转 customHyperlinkJump: null, + // 是否开启性能模式,默认情况下所有节点都会直接渲染,无论是否处于画布可视区域,这样当节点数量比较多时(1000+)会比较卡,如果你的数据量比较大,那么可以通过该配置开启性能模式,即只渲染画布可视区域内的节点,超出的节点不渲染,这样会大幅提高渲染速度,当然同时也会带来一些其他问题,比如:1.当拖动或是缩放画布时会实时计算并渲染未节点的节点,所以会带来一定卡顿;2.导出图片、svg、pdf时需要先渲染全部节点,所以会比较慢;3.其他目前未发现的问题 + openPerformance: false, + // 性能优化模式配置 + performanceConfig: { + time: 250,// 当视图改变后多久刷新一次节点,单位:ms, + padding: 100,// 超出画布四周指定范围内依旧渲染节点 + removeNodeWhenOutCanvas: true,// 节点移除画布可视区域后从画布删除 + }, // 【Select插件】 // 多选节点时鼠标移动到边缘时的画布移动偏移量 diff --git a/simple-mind-map/src/core/render/Render.js b/simple-mind-map/src/core/render/Render.js index 10635779..03fed67f 100644 --- a/simple-mind-map/src/core/render/Render.js +++ b/simple-mind-map/src/core/render/Render.js @@ -32,7 +32,8 @@ import { checkIsNodeStyleDataKey, removeRichTextStyes, formatGetNodeGeneralization, - sortNodeList + sortNodeList, + throttle } from '../../utils' import { shapeList } from './node/Shape' import { lineStyleProps } from '../../themes/default' @@ -144,13 +145,40 @@ class Render { if (!this.mindMap.opt.enableDblclickBackToRootNode) return this.setRootNodeCenter() }) - // let timer = null - // this.mindMap.on('view_data_change', () => { - // clearTimeout(timer) - // timer = setTimeout(() => { - // this.render() - // }, 300) - // }) + // 性能模式 + this.performanceMode() + } + + // 性能模式,懒加载节点 + performanceMode() { + const { openPerformance, performanceConfig } = this.mindMap.opt + const onViewDataChange = throttle(() => { + if (this.root) this.root.render() + }, performanceConfig.time) + let lastOpen = false + this.mindMap.on('before_update_config', opt => { + lastOpen = opt.openPerformance + }) + this.mindMap.on('after_update_config', opt => { + if (opt.openPerformance && !lastOpen) { + // 动态开启性能模式 + this.mindMap.on('view_data_change', onViewDataChange) + this.forceLoadNode() + } + if (!opt.openPerformance && lastOpen) { + // 动态关闭性能模式 + this.mindMap.off('view_data_change', onViewDataChange) + this.forceLoadNode() + } + }) + if (!openPerformance) return + this.mindMap.on('view_data_change', onViewDataChange) + } + + // 强制渲染节点,不考虑是否在画布可视区域内 + forceLoadNode(node) { + node = node || this.root + if (node) node.render(() => {}, true) } // 注册命令 @@ -453,6 +481,7 @@ class Render { } this.mindMap.emit('node_tree_render_start') // 计算布局 + this.root = null this.layout.doLayout(root => { // 删除本次渲染时不再需要的节点 Object.keys(this.lastNodeCache).forEach(uid => { diff --git a/simple-mind-map/src/core/render/node/Node.js b/simple-mind-map/src/core/render/node/Node.js index b1dec1eb..417c46aa 100644 --- a/simple-mind-map/src/core/render/node/Node.js +++ b/simple-mind-map/src/core/render/node/Node.js @@ -683,7 +683,7 @@ class Node { } // 更新节点 - update() { + update(forceRender) { if (!this.group) { return } @@ -710,36 +710,11 @@ class Node { } } // 更新概要 - this.renderGeneralization() + this.renderGeneralization(forceRender) // 更新协同头像 if (this.updateUserListNode) this.updateUserListNode() // 更新节点位置 let t = this.group.transform() - // // 如果上次不在可视区内,且本次也不在,那么直接返回 - // let { left: ox, top: oy } = this.getNodePosInClient( - // t.translateX, - // t.translateY - // ) - // let oldIsInClient = - // ox > 0 && oy > 0 && ox < this.mindMap.width && oy < this.mindMap.height - // let { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top) - // let newIsNotInClient = - // nx + this.width < 0 || - // ny + this.height < 0 || - // nx > this.mindMap.width || - // ny > this.mindMap.height - // if (!oldIsInClient && newIsNotInClient) { - // if (!this.isHide) { - // this.isHide = true - // this.group.hide() - // } - // return - // } - // // 如果当前是隐藏状态,那么先显示 - // if (this.isHide) { - // this.isHide = false - // this.group.show() - // } // 如果节点位置没有变化,则返回 if (this.left === t.translateX && this.top === t.translateY) return this.group.translate(this.left - t.translateX, this.top - t.translateY) @@ -757,6 +732,17 @@ class Node { } } + // 判断节点是否可见 + checkIsInClient(padding = 0) { + const { left: nx, top: ny } = this.getNodePosInClient(this.left, this.top) + return ( + nx + this.width > 0 - padding && + ny + this.height > 0 - padding && + nx < this.mindMap.width + padding && + ny < this.mindMap.height + padding + ) + } + // 重新渲染节点,即重新创建节点内容、计算节点大小、计算节点内容布局、更新展开收起按钮,概要及位置 reRender() { let sizeChange = this.getSize() @@ -786,31 +772,41 @@ class Node { } // 递归渲染 - render(callback = () => {}) { + render(callback = () => {}, forceRender = false) { // 节点 // 重新渲染连线 this.renderLine() - if (!this.group) { - // 创建组 - this.group = new G() - this.group.addClass('smm-node') - this.group.css({ - cursor: 'default' - }) - this.bindGroupEvent() - this.nodeDraw.add(this.group) - this.layout() - this.update() - } else { - if (!this.nodeDraw.has(this.group)) { + const { openPerformance, performanceConfig } = this.mindMap.opt + // 强制渲染、或没有开启性能模式、或不在画布可视区域内不渲染节点内容 + if ( + forceRender || + !openPerformance || + this.checkIsInClient(performanceConfig.padding) + ) { + if (!this.group) { + // 创建组 + this.group = new G() + this.group.addClass('smm-node') + this.group.css({ + cursor: 'default' + }) + this.bindGroupEvent() this.nodeDraw.add(this.group) - } - if (this.needLayout) { - this.needLayout = false this.layout() + this.update(forceRender) + } else { + if (!this.nodeDraw.has(this.group)) { + this.nodeDraw.add(this.group) + } + if (this.needLayout) { + this.needLayout = false + this.layout() + } + this.updateExpandBtnPlaceholderRect() + this.update(forceRender) } - this.updateExpandBtnPlaceholderRect() - this.update() + } else if (openPerformance && performanceConfig.removeNodeWhenOutCanvas) { + this.remove(true) } // 子节点 if ( @@ -825,7 +821,7 @@ class Node { if (index >= this.children.length) { callback() } - }) + }, forceRender) }) } else { callback() @@ -841,11 +837,13 @@ class Node { } // 递归删除,只是从画布删除,节点容器还在,后续还可以重新插回画布 - remove() { + remove(keepLine = false) { if (!this.group) return this.group.remove() this.removeGeneralization() - this.removeLine() + if (!keepLine) { + this.removeLine() + } // 子节点 if (this.children && this.children.length) { this.children.forEach(item => { @@ -856,6 +854,10 @@ class Node { // 销毁节点,不但会从画布删除,而且原节点直接置空,后续无法再插回画布 destroy() { + this.removeLine() + if (this.parent) { + this.parent.removeLine() + } if (!this.group) return if (this.emptyUser) { this.emptyUser() @@ -863,11 +865,7 @@ class Node { this.resetWhenDelete() this.group.remove() this.removeGeneralization() - this.removeLine() this.group = null - if (this.parent) { - this.parent.removeLine() - } this.style.onRemove() } diff --git a/simple-mind-map/src/core/render/node/nodeGeneralization.js b/simple-mind-map/src/core/render/node/nodeGeneralization.js index 24dd7793..59e71a2d 100644 --- a/simple-mind-map/src/core/render/node/nodeGeneralization.js +++ b/simple-mind-map/src/core/render/node/nodeGeneralization.js @@ -85,7 +85,7 @@ function updateGeneralization() { } // 渲染概要节点 -function renderGeneralization() { +function renderGeneralization(forceRender) { if (this.isGeneralization) return this.updateGeneralizationData() const list = this.formatGetGeneralization() @@ -100,7 +100,7 @@ function renderGeneralization() { this.renderer.layout.renderGeneralization(this._generalizationList) this._generalizationList.forEach(item => { this.style.generalizationLine(item.generalizationLine) - item.generalizationNode.render() + item.generalizationNode.render(() => {}, forceRender) }) } diff --git a/simple-mind-map/src/plugins/OuterFrame.js b/simple-mind-map/src/plugins/OuterFrame.js index e0c1e4b6..2bcffcd4 100644 --- a/simple-mind-map/src/plugins/OuterFrame.js +++ b/simple-mind-map/src/plugins/OuterFrame.js @@ -297,6 +297,13 @@ class OuterFrame { if (range[0] === -1 || range[1] === -1) return const { left, top, width, height } = getNodeListBoundingRect(nodeList) + if ( + !Number.isFinite(left) || + !Number.isFinite(top) || + !Number.isFinite(width) || + !Number.isFinite(height) + ) + return const el = this.createOuterFrameEl( (left - outerFramePaddingX - diff --git a/simple-mind-map/src/utils/index.js b/simple-mind-map/src/utils/index.js index ad7e787a..3ecf947b 100644 --- a/simple-mind-map/src/utils/index.js +++ b/simple-mind-map/src/utils/index.js @@ -1382,22 +1382,24 @@ export const getNodeTreeBoundingRect = ( let minY = Infinity let maxY = -Infinity const walk = (root, isRoot) => { - if (!(isRoot && excludeSelf)) { - const { x, y, width, height } = root.group - .findOne('.smm-node-shape') - .rbox() - if (x < minX) { - minX = x - } - if (x + width > maxX) { - maxX = x + width - } - if (y < minY) { - minY = y - } - if (y + height > maxY) { - maxY = y + height - } + if (!(isRoot && excludeSelf) && root.group) { + try { + const { x, y, width, height } = root.group + .findOne('.smm-node-shape') + .rbox() + if (x < minX) { + minX = x + } + if (x + width > maxX) { + maxX = x + width + } + if (y < minY) { + minY = y + } + if (y + height > maxY) { + maxY = y + height + } + } catch (e) {} } if (!excludeGeneralization && root._generalizationList.length > 0) { root._generalizationList.forEach(item => {