Skip to content

Commit 58fbe54

Browse files
committed
perf: Optimize the rendering logic of front-end nodes
1 parent 90f4d59 commit 58fbe54

File tree

5 files changed

+227
-37
lines changed

5 files changed

+227
-37
lines changed

ui/src/views/application-workflow/index.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
<el-button icon="Plus" @click="showPopover = !showPopover"> 添加组件 </el-button>
2828
<el-button @click="clickShowDebug" :disabled="showDebug">
2929
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
30-
{{ $t('common.debug')}}</el-button
30+
{{ $t('common.debug') }}</el-button
3131
>
3232
<el-button @click="saveApplication(true)">
3333
<AppIcon iconName="app-save-outlined" class="mr-4"></AppIcon>
34-
{{ $t('common.save')}}
34+
{{ $t('common.save') }}
3535
</el-button>
3636
<el-button type="primary" @click="publicHandle"> 发布 </el-button>
3737

@@ -326,7 +326,7 @@ function getDetail() {
326326
saveTime.value = res.data?.update_time
327327
workflowRef.value?.clearGraphData()
328328
nextTick(() => {
329-
workflowRef.value?.renderGraphData(detail.value.work_flow)
329+
workflowRef.value?.render(detail.value.work_flow)
330330
})
331331
})
332332
}

ui/src/workflow/common/app-node.ts

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,24 @@ import ElementPlus from 'element-plus'
33
import * as ElementPlusIcons from '@element-plus/icons-vue'
44
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
55
import { HtmlResize } from '@logicflow/extension'
6-
76
import { h as lh } from '@logicflow/core'
87
import { createApp, h } from 'vue'
98
import directives from '@/directives'
109
import i18n from '@/locales'
1110
import { WorkflowType } from '@/enums/workflow'
1211
import { nodeDict } from '@/workflow/common/data'
12+
import { isActive, connect, disconnect } from './teleport'
1313
class AppNode extends HtmlResize.view {
1414
isMounted
15-
r
16-
app
17-
15+
r?: any
16+
component: any
17+
app: any
18+
root?: any
19+
VueNode: any
1820
constructor(props: any, VueNode: any) {
1921
super(props)
22+
this.component = VueNode
2023
this.isMounted = false
21-
this.r = h(VueNode, {
22-
properties: props.model.properties,
23-
nodeModel: props.model
24-
})
25-
26-
this.app = createApp({
27-
render: () => this.r
28-
})
29-
this.app.use(ElementPlus, {
30-
locale: zhCn
31-
})
32-
this.app.use(Components)
33-
this.app.use(directives)
34-
this.app.use(i18n)
35-
for (const [key, component] of Object.entries(ElementPlusIcons)) {
36-
this.app.component(key, component)
37-
}
38-
3924
if (props.model.properties.noRender) {
4025
delete props.model.properties.noRender
4126
} else {
@@ -137,13 +122,79 @@ class AppNode extends HtmlResize.view {
137122
this.isMounted = true
138123
const node = document.createElement('div')
139124
rootEl.appendChild(node)
140-
this.app?.mount(node)
125+
this.renderVueComponent(node)
141126
} else {
142127
if (this.r && this.r.component) {
143128
this.r.component.props.properties = this.props.model.getProperties()
144129
}
145130
}
146131
}
132+
componentWillUnmount() {
133+
super.componentWillUnmount()
134+
this.unmount()
135+
}
136+
getComponentContainer() {
137+
return this.root
138+
}
139+
protected targetId() {
140+
return `${this.props.graphModel.flowId}:${this.props.model.id}`
141+
}
142+
protected renderVueComponent(root: any) {
143+
this.unmountVueComponent()
144+
this.root = root
145+
const { model, graphModel } = this.props
146+
147+
if (root) {
148+
if (isActive()) {
149+
connect(this.targetId(), this.component, root, model, graphModel)
150+
} else {
151+
this.r = h(this.component, {
152+
properties: this.props.model.properties,
153+
nodeModel: this.props.model
154+
})
155+
this.app = createApp({
156+
render() {
157+
return this.r
158+
},
159+
provide() {
160+
return {
161+
getNode: () => model,
162+
getGraph: () => graphModel
163+
}
164+
}
165+
})
166+
167+
this.app.use(ElementPlus, {
168+
locale: zhCn
169+
})
170+
this.app.use(Components)
171+
this.app.use(directives)
172+
this.app.use(i18n)
173+
for (const [key, component] of Object.entries(ElementPlusIcons)) {
174+
this.app.component(key, component)
175+
}
176+
this.app?.mount(root)
177+
}
178+
}
179+
}
180+
181+
protected unmountVueComponent() {
182+
if (this.app) {
183+
this.app.unmount()
184+
this.app = null
185+
}
186+
if (this.root) {
187+
this.root.innerHTML = ''
188+
}
189+
return this.root
190+
}
191+
192+
unmount() {
193+
if (isActive()) {
194+
disconnect(this.targetId())
195+
}
196+
this.unmountVueComponent()
197+
}
147198
}
148199

149200
class AppNodeModel extends HtmlResize.model {

ui/src/workflow/common/edge.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
22
import { createApp, h as vh } from 'vue'
3-
3+
import { isActive, connect, disconnect } from './teleport'
44
import CustomLine from './CustomLine.vue'
55
function isMouseInElement(element: any, e: any) {
66
const rect = element.getBoundingClientRect()
@@ -15,7 +15,8 @@ const DEFAULT_WIDTH = 32
1515
const DEFAULT_HEIGHT = 32
1616
class CustomEdge2 extends BezierEdge {
1717
isMounted
18-
18+
customLineApp?: any
19+
root?: any
1920
constructor() {
2021
super()
2122
this.isMounted = false
@@ -28,6 +29,64 @@ class CustomEdge2 extends BezierEdge {
2829
}
2930
}
3031
}
32+
/**
33+
* 渲染vue组件
34+
* @param root
35+
*/
36+
protected renderVueComponent(root: any) {
37+
this.unmountVueComponent()
38+
this.root = root
39+
const { graphModel } = this.props
40+
if (root) {
41+
if (isActive()) {
42+
connect(
43+
this.targetId(),
44+
CustomLine,
45+
root,
46+
this.props.model,
47+
graphModel,
48+
(node: any, graph: any) => {
49+
return { model: node, graph }
50+
}
51+
)
52+
} else {
53+
this.customLineApp = createApp({
54+
render: () => vh(CustomLine, { model: this.props.model })
55+
})
56+
this.customLineApp?.mount(root)
57+
}
58+
}
59+
}
60+
protected targetId() {
61+
return `${this.props.graphModel.flowId}:${this.props.model.id}`
62+
}
63+
/**
64+
* 组件即将卸载勾子
65+
*/
66+
componentWillUnmount() {
67+
if (super.componentWillUnmount) {
68+
super.componentWillUnmount()
69+
}
70+
if (isActive()) {
71+
console.log('unmount')
72+
disconnect(this.targetId())
73+
}
74+
this.unmountVueComponent()
75+
}
76+
/**
77+
* 卸载vue
78+
* @returns
79+
*/
80+
protected unmountVueComponent() {
81+
if (this.customLineApp) {
82+
this.customLineApp.unmount()
83+
this.customLineApp = null
84+
}
85+
if (this.root) {
86+
this.root.innerHTML = ''
87+
}
88+
return this.root
89+
}
3190

3291
getEdge() {
3392
const { model } = this.props
@@ -57,14 +116,11 @@ class CustomEdge2 extends BezierEdge {
57116
height: customHeight
58117
}
59118

60-
const app = createApp({
61-
render: () => vh(CustomLine, { model: this.props.model })
62-
})
63119
setTimeout(() => {
64120
const s = document.getElementById(id)
65121
if (s && !this.isMounted) {
66-
app.mount(s)
67122
this.isMounted = true
123+
this.renderVueComponent(s)
68124
}
69125
}, 0)
70126

ui/src/workflow/common/teleport.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core'
2+
import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi'
3+
4+
let active = false
5+
const items = reactive<{ [key: string]: any }>({})
6+
7+
export function connect(
8+
id: string,
9+
component: any,
10+
container: HTMLDivElement,
11+
node: BaseNodeModel | BaseEdgeModel,
12+
graph: GraphModel,
13+
get_props?: any
14+
) {
15+
if (!get_props) {
16+
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
17+
return { nodeModel: node, graph }
18+
}
19+
}
20+
if (active) {
21+
items[id] = markRaw(
22+
defineComponent({
23+
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
24+
provide: () => ({
25+
getNode: () => node,
26+
getGraph: () => graph
27+
})
28+
})
29+
)
30+
}
31+
}
32+
33+
export function disconnect(id: string) {
34+
if (active) {
35+
delete items[id]
36+
}
37+
}
38+
39+
export function isActive() {
40+
return active
41+
}
42+
43+
export function getTeleport(): any {
44+
if (!isVue3) {
45+
throw new Error('teleport is only available in Vue3')
46+
}
47+
active = true
48+
49+
return defineComponent({
50+
props: {
51+
flowId: {
52+
type: String,
53+
required: true
54+
}
55+
},
56+
setup(props) {
57+
return () => {
58+
const children: Record<string, any>[] = []
59+
Object.keys(items).forEach((id) => {
60+
// https://github.com/didi/LogicFlow/issues/1768
61+
// 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据
62+
// 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive
63+
// 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题
64+
// 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0]
65+
66+
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
67+
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
68+
if (id.startsWith(props.flowId)) {
69+
children.push(items[id])
70+
}
71+
})
72+
return h(
73+
Fragment,
74+
{},
75+
children.map((item) => h(item))
76+
)
77+
}
78+
}
79+
})
80+
}

ui/src/workflow/index.vue

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div className="workflow-app" id="container"></div>
33
<!-- 辅助工具栏 -->
44
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
5+
<TeleportContainer :flow-id="flowId" />
56
</template>
67
<script setup lang="ts">
78
import LogicFlow from '@logicflow/core'
@@ -13,10 +14,12 @@ import '@logicflow/extension/lib/style/index.css'
1314
import '@logicflow/core/dist/style/index.css'
1415
import { initDefaultShortcut } from '@/workflow/common/shortcut'
1516
import Dagre from '@/workflow/plugins/dagre'
17+
import { getTeleport } from '@/workflow/common/teleport'
1618
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
1719
1820
defineOptions({ name: 'WorkFlow' })
19-
21+
const TeleportContainer = getTeleport()
22+
const flowId = ref('')
2023
type ShapeItem = {
2124
type?: string
2225
text?: string
@@ -56,9 +59,6 @@ const render = (data: any) => {
5659
lf.value.render(data)
5760
}
5861
const renderGraphData = (data?: any) => {
59-
if (data) {
60-
graphData.value = data
61-
}
6262
const container: any = document.querySelector('#container')
6363
if (container) {
6464
lf.value = new LogicFlow({
@@ -89,11 +89,14 @@ const renderGraphData = (data?: any) => {
8989
strokeWidth: 1
9090
}
9191
})
92+
lf.value.on('graph:rendered', () => {
93+
flowId.value = lf.value.graphModel.flowId
94+
})
9295
initDefaultShortcut(lf.value, lf.value.graphModel)
9396
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
9497
lf.value.setDefaultEdgeType('app-edge')
9598
96-
lf.value.render(graphData.value)
99+
lf.value.render(data ? data : {})
97100
98101
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
99102
id_list.forEach((id: string) => {

0 commit comments

Comments
 (0)