如果你是用 svelte、vue、react 等前端框架来开发思源的插件,那么一定要小心这个问题。
现代的前端框架中,往往有两个重要的钩子事件(各个框架的具体命名可能有所不同)
- onMount
- onDestroy
而如果在开发的时候符合下面的情况:
- 在 Dialog、Tab 中渲染了一个组件
- 组件中使用了副作用代码(例如监听器、setInverval 等),并且在 onDestroy 中清理副作用
那就一定注意了:一个不留神可能存在内存泄漏的风险。
一个简单的案例
这是一个非常简单的案例,使用 solid 编写(不知道这个的,可以把它当成 react 看,在这里差别不大),组件中使用 setInterval 来更新颜色,然后在 onCleanUp 中清理 timer。
import { showMessage, fetchPost, Protyle, type App } from "siyuan";
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
const randInt = () => Math.floor(255 * Math.random());
const Component = () => {
//略
let [color, setColor] = createSignal({r: 255, g: 255, b: 255});
let timer = setInterval(() => {
setColor({
r: randInt(),
g: randInt(),
b: randInt(),
});
console.log('update color', color());
}, 1000);
onMount(async () => {
//略
showMessage("Hello mount", 1500);
});
onCleanup(() => {
showMessage("Hello panel closed", 1500);
clearInterval(timer);
});
return (
<div class="b3-dialog__content">
<div class="plugin-sample__time">
System current time: <span id="time">{time}</span>
</div>
{/*略*/}
<style jsx dynamic>
{`
.plugin-sample__time {
background-color: rgb(${color().r}, ${color().g}, ${color().b});
}
`}
</style>
</div>
);
}
export default Component;
然后我们在某个 Dialog 中显示这个组件:
import Component from './hello.tsx';
const simpleDialog = (args: {
title: string, ele: HTMLElement | DocumentFragment,
width?: string, height?: string,
callback?: () => void;
}) => {
const dialog = new Dialog({
title: args.title,
content: `<div class="dialog-content" style="display: flex; height: 100%;"/>`,
width: args.width,
height: args.height,
destroyCallback: args.callback
});
dialog.element.querySelector(".dialog-content").appendChild(args.ele);
return dialog;
}
const showDialog = () => {
let container = document.createElement('div')
container.style.display = 'contents';
/* 渲染组件
相当于 svelte 的 new Component({target: container})
vue 的 createApp(Component).mount(container)
*/
render(() => Component(), container);
return simpleDialog({...args, ele: container, callback: () => {
console.log("Bye!");
}});
}
你注意到什么问题了吗?
如果你没注意到:恭喜你,踩坑了——Component 的 onCleanup 不会被自动调用,timer 也无法被正确清理,你踩中了一个内存泄漏的坑。
这里的原因在于,思源的 Dialog 销毁对于框架来说是外部的脚本行为,不涉及到前端组件的生命周期,他们也无法对这种“意外情况”进行响应。
这个很好理解,想想你用脚手架创建的 SPA 代码样例,是不是一般都是在顶层创建一个 App
组件,然后在 HTML 中引入一个顶层的 js 脚本,将 App
mount 到一个 #app
元素上?
只有把各种自定义的组件放到这个 App 下方,框架才能正确管理各种创建、销毁的声明周期——而在思源中写插件显然不符合这种情况。
如何解决?
处理方法非常简单:你必须要在 Dialog 的 destroyCallback
当中显式的调用组件的销毁方法。
比如在 solidjs 中,render
函数会返回一个 dispose
函数(销毁),我们只要在 callback 里调用 dispose 就完事大吉。
const showDialog = () => {
let container = document.createElement('div')
container.style.display = 'contents';
let disposer = render(() => Component(), container);
return simpleDialog({...args, ele: container, callback: () => {
disposer(); //必须显式地调用销毁,来触发组件的 Destroy 生命周期
console.log("Bye!");
}});
}
其他框架也是类似操作,例如在 vite-svelte 模板中,通过调用 pannel.$destroy
来明确地销毁组件。
openDIYSetting(): void {
let dialog = new Dialog({
title: "SettingPannel",
content: `<div id="SettingPanel" style="height: 100%;"></div>`,
width: "800px",
destroyCallback: (options) => {
console.log("destroyCallback", options);
//You'd better destroy the component when the dialog is closed
pannel.$destroy();
}
});
let pannel = new SettingExample({
target: dialog.element.querySelector("#SettingPanel"),
});
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于