mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
fix: frontend memory leaks — Monaco dispose, TQ cache eviction, useEffect cleanup
- Dispose Monaco editor instances on dialog close to prevent cycle leak - Replace gcTime: Infinity with finite TTLs and evict orphaned subscription queryKeys - Add missing useEffect cleanup for timers, move setTimeout out of useMemo
This commit is contained in:
parent
2c766e1ada
commit
b8fbabae04
@ -444,6 +444,12 @@ export const CurrentProxyCard = () => {
|
|||||||
[setState],
|
[setState],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 处理代理组变更
|
// 处理代理组变更
|
||||||
const handleGroupChange = useCallback(
|
const handleGroupChange = useCallback(
|
||||||
(event: SelectChangeEvent<string>) => {
|
(event: SelectChangeEvent<string>) => {
|
||||||
|
|||||||
@ -425,7 +425,7 @@ function useIPInfo() {
|
|||||||
queryKey: [IP_INFO_CACHE_KEY],
|
queryKey: [IP_INFO_CACHE_KEY],
|
||||||
queryFn: getIpInfo,
|
queryFn: getIpInfo,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 60 * 60 * 1000,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: 1,
|
retry: 1,
|
||||||
|
|||||||
@ -168,6 +168,13 @@ export const EditorViewer = ({
|
|||||||
}
|
}
|
||||||
}, [open, syncMaximizedState])
|
}, [open, syncMaximizedState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
|
|||||||
@ -34,11 +34,13 @@ import {
|
|||||||
requestIdleCallback,
|
requestIdleCallback,
|
||||||
} from 'foxact/request-idle-callback'
|
} from 'foxact/request-idle-callback'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
@ -149,6 +151,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
[t],
|
[t],
|
||||||
)
|
)
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -481,6 +484,13 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
getInterfaceNameList()
|
getInterfaceNameList()
|
||||||
}, [fetchContent, fetchProfile, getInterfaceNameList, open])
|
}, [fetchContent, fetchProfile, getInterfaceNameList, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const validateGroup = () => {
|
const validateGroup = () => {
|
||||||
const group = formIns.getValues()
|
const group = formIns.getValues()
|
||||||
if (group.name === '') {
|
if (group.name === '') {
|
||||||
@ -1105,6 +1115,9 @@ export const GroupsEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
import { open } from '@tauri-apps/plugin-shell'
|
import { open } from '@tauri-apps/plugin-shell'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
|
||||||
@ -96,7 +96,11 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 新增状态:是否显示下次更新时间
|
// 新增状态:是否显示下次更新时间
|
||||||
const [showNextUpdate, setShowNextUpdate] = useState(false)
|
const [showNextUpdate, setShowNextUpdate] = useState(false)
|
||||||
|
const showNextUpdateRef = useRef(false)
|
||||||
const [nextUpdateTime, setNextUpdateTime] = useState('')
|
const [nextUpdateTime, setNextUpdateTime] = useState('')
|
||||||
|
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
|
||||||
const { uid, name = 'Profile', extra, updated = 0, option } = itemData
|
const { uid, name = 'Profile', extra, updated = 0, option } = itemData
|
||||||
|
|
||||||
@ -178,6 +182,10 @@ export const ProfileItem = (props: Props) => {
|
|||||||
setShowNextUpdate(!showNextUpdate)
|
setShowNextUpdate(!showNextUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
showNextUpdateRef.current = showNextUpdate
|
||||||
|
}, [showNextUpdate])
|
||||||
|
|
||||||
// 当组件加载或更新间隔变化时更新下次更新时间
|
// 当组件加载或更新间隔变化时更新下次更新时间
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showNextUpdate) {
|
if (showNextUpdate) {
|
||||||
@ -192,19 +200,18 @@ export const ProfileItem = (props: Props) => {
|
|||||||
|
|
||||||
// 订阅定时器更新事件
|
// 订阅定时器更新事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let refreshTimeout: number | undefined
|
|
||||||
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
|
||||||
const handleTimerUpdate = (event: Event) => {
|
const handleTimerUpdate = (event: Event) => {
|
||||||
const source = event as CustomEvent<string> & { payload?: string }
|
const source = event as CustomEvent<string> & { payload?: string }
|
||||||
const updatedUid = source.detail ?? source.payload
|
const updatedUid = source.detail ?? source.payload
|
||||||
|
|
||||||
// 只有当更新的是当前配置时才刷新显示
|
// 只有当更新的是当前配置时才刷新显示
|
||||||
if (updatedUid === itemData.uid && showNextUpdate) {
|
if (updatedUid === itemData.uid && showNextUpdateRef.current) {
|
||||||
debugLog(`收到定时器更新事件: uid=${updatedUid}`)
|
debugLog(`收到定时器更新事件: uid=${updatedUid}`)
|
||||||
if (refreshTimeout !== undefined) {
|
if (refreshTimeoutRef.current !== undefined) {
|
||||||
clearTimeout(refreshTimeout)
|
clearTimeout(refreshTimeoutRef.current)
|
||||||
}
|
}
|
||||||
refreshTimeout = window.setTimeout(() => {
|
refreshTimeoutRef.current = window.setTimeout(() => {
|
||||||
fetchNextUpdateTime(true)
|
fetchNextUpdateTime(true)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@ -214,13 +221,13 @@ export const ProfileItem = (props: Props) => {
|
|||||||
window.addEventListener('verge://timer-updated', handleTimerUpdate)
|
window.addEventListener('verge://timer-updated', handleTimerUpdate)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (refreshTimeout !== undefined) {
|
if (refreshTimeoutRef.current !== undefined) {
|
||||||
clearTimeout(refreshTimeout)
|
clearTimeout(refreshTimeoutRef.current)
|
||||||
}
|
}
|
||||||
// 清理事件监听
|
// 清理事件监听
|
||||||
window.removeEventListener('verge://timer-updated', handleTimerUpdate)
|
window.removeEventListener('verge://timer-updated', handleTimerUpdate)
|
||||||
}
|
}
|
||||||
}, [fetchNextUpdateTime, itemData.uid, showNextUpdate])
|
}, [fetchNextUpdateTime, itemData.uid])
|
||||||
|
|
||||||
// local file mode
|
// local file mode
|
||||||
// remote file mode
|
// remote file mode
|
||||||
|
|||||||
@ -27,11 +27,13 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -56,6 +58,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
const { profileUid, property, open, onClose, onSave } = props
|
const { profileUid, property, open, onClose, onSave } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -343,6 +346,13 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
fetchProfile()
|
fetchProfile()
|
||||||
}, [fetchContent, fetchProfile, open])
|
}, [fetchContent, fetchProfile, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSave = useLockFn(async () => {
|
const handleSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
await saveProfileFile(property, currData)
|
await saveProfileFile(property, currData)
|
||||||
@ -469,6 +479,9 @@ export const ProxiesEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -29,11 +29,13 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import {
|
import {
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -251,6 +253,8 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const themeMode = useThemeMode()
|
const themeMode = useThemeMode()
|
||||||
|
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
|
|
||||||
const [prevData, setPrevData] = useState('')
|
const [prevData, setPrevData] = useState('')
|
||||||
const [currData, setCurrData] = useState('')
|
const [currData, setCurrData] = useState('')
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
@ -536,6 +540,13 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
fetchProfile()
|
fetchProfile()
|
||||||
}, [fetchContent, fetchProfile, open])
|
}, [fetchContent, fetchProfile, open])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const validateRule = () => {
|
const validateRule = () => {
|
||||||
if ((ruleType.required ?? true) && !ruleContent) {
|
if ((ruleType.required ?? true) && !ruleContent) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -770,6 +781,9 @@ export const RulesEditorViewer = (props: Props) => {
|
|||||||
language="yaml"
|
language="yaml"
|
||||||
value={currData}
|
value={currData}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2, // 根据语言类型设置缩进大小
|
tabSize: 2, // 根据语言类型设置缩进大小
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import yaml from 'js-yaml'
|
import yaml from 'js-yaml'
|
||||||
|
import type { editor } from 'monaco-editor'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -189,6 +190,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [visualization, setVisualization] = useState(true)
|
const [visualization, setVisualization] = useState(true)
|
||||||
const skipYamlSyncRef = useRef(false)
|
const skipYamlSyncRef = useRef(false)
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||||
const [values, setValues] = useState<{
|
const [values, setValues] = useState<{
|
||||||
enable: boolean
|
enable: boolean
|
||||||
listen: string
|
listen: string
|
||||||
@ -453,6 +455,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
}, [visualization])
|
}, [visualization])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
editorRef.current?.dispose()
|
||||||
|
editorRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const initDnsConfig = useCallback(async () => {
|
const initDnsConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const dnsConfigExists = await invoke<boolean>(
|
const dnsConfigExists = await invoke<boolean>(
|
||||||
@ -1057,6 +1066,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
value={yamlContent}
|
value={yamlContent}
|
||||||
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
|
||||||
className="flex-grow"
|
className="flex-grow"
|
||||||
|
onMount={(editorInstance) => {
|
||||||
|
editorRef.current = editorInstance
|
||||||
|
}}
|
||||||
options={{
|
options={{
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
minimap: {
|
minimap: {
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const useIconCache = ({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 30 * 60 * 1000,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
subscriptionCacheKey ?? '$sub$__disabled__',
|
subscriptionCacheKey ?? '$sub$__disabled__',
|
||||||
]) ?? fallbackData,
|
]) ?? fallbackData,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
gcTime: Infinity,
|
gcTime: 30_000,
|
||||||
enabled: subscriptionCacheKey !== null,
|
enabled: subscriptionCacheKey !== null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -243,8 +243,11 @@ export const useMihomoWsSubscription = <T>(
|
|||||||
}, [subscriptionCacheKey])
|
}, [subscriptionCacheKey])
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
|
if (subscriptionCacheKey) {
|
||||||
|
queryClient.removeQueries({ queryKey: [subscriptionCacheKey] })
|
||||||
|
}
|
||||||
setDate(Date.now())
|
setDate(Date.now())
|
||||||
}, [setDate])
|
}, [queryClient, subscriptionCacheKey, setDate])
|
||||||
|
|
||||||
return { response, refresh, subscriptionCacheKey, wsRef }
|
return { response, refresh, subscriptionCacheKey, wsRef }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -309,20 +309,22 @@ export const useCustomTheme = () => {
|
|||||||
styleElement.innerHTML = effectiveInjectedCss + globalStyles
|
styleElement.innerHTML = effectiveInjectedCss + globalStyles
|
||||||
}
|
}
|
||||||
|
|
||||||
const { palette } = muiTheme
|
|
||||||
setTimeout(() => {
|
|
||||||
const dom = document.querySelector('#Gradient2')
|
|
||||||
if (dom) {
|
|
||||||
dom.innerHTML = `
|
|
||||||
<stop offset="0%" stop-color="${palette.primary.main}" />
|
|
||||||
<stop offset="80%" stop-color="${palette.primary.dark}" />
|
|
||||||
<stop offset="100%" stop-color="${palette.primary.dark}" />
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return muiTheme
|
return muiTheme
|
||||||
}, [mode, theme_setting, userBackgroundImage, hasUserBackground])
|
}, [mode, theme_setting, userBackgroundImage, hasUserBackground])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
const dom = document.querySelector('#Gradient2')
|
||||||
|
if (dom) {
|
||||||
|
dom.innerHTML = `
|
||||||
|
<stop offset="0%" stop-color="${theme.palette.primary.main}" />
|
||||||
|
<stop offset="80%" stop-color="${theme.palette.primary.dark}" />
|
||||||
|
<stop offset="100%" stop-color="${theme.palette.primary.dark}" />
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
return () => clearTimeout(id)
|
||||||
|
}, [theme.palette.primary.main, theme.palette.primary.dark])
|
||||||
|
|
||||||
return { theme }
|
return { theme }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user