如果你是用 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"), }); }
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于