setTimeout回调无法被GC回收引发内存泄漏的解决方案
问题根因
你观测到的setTimeout回调无法被GC、堆内存持续上涨,是三个逻辑缺陷共同导致的:
- 未在组件卸载阶段清理定时器:只要
setTimeout没有被clearTimeout主动清除,事件循环就会一直持有回调函数的引用,回调闭包中捕获的整个组件实例、props、state都无法被GC回收,只要组件发生一次重挂载,旧实例的内存就会永久泄漏。 - 轮询节奏不依赖请求完成状态:当前逻辑是到固定间隔就发起新请求,完全不管上一次请求是否结束,弱网环境下会出现大量pending状态的请求堆积,每个未完成的请求都会持有对应的response、闭包引用,进一步推高内存。
- 未处理组件卸载后的请求回调:组件卸载后返回的fetch请求依然会执行dispatch逻辑,无意义持有Redux store和组件相关引用。
修复方案
1. 完善组件生命周期清理逻辑
在组件中增加挂载标记、请求控制器,卸载时统一清理资源:
componentDidMount() { // 标记组件挂载状态 this._isMounted = true; // 初始化请求终止控制器 this.abortController = new AbortController(); // 发起首次轮询 this.fetchDisplayContent(1000); } componentWillUnmount() { // 标记组件已卸载 this._isMounted = false; // 清除未执行的定时器 clearTimeout(this._contentInterval); // 终止所有未完成的请求 this.abortController.abort(); }
2. 改造Redux请求逻辑,支持主动终止
给fetch请求增加signal参数支持,忽略主动终止请求触发的错误:
export function fetchContent(url, lastUpdated, webkey, signal) { return (dispatch) => { let now = new Date(); let rev = now.getTime()/1000; url += url.includes('?') ? "&rev="+rev : "?rev="+rev; if (webkey) url += "&webkey="+webkey; fetch(url, { signal }) .then((response) => { if (!response.ok) throw Error(response.statusText); return response.json(); }) .then((content) => { if (Number(content.last_update) > Number(lastUpdated)){ dispatch(contentFetchSuccess(content)) } }).catch((err) => { // 主动终止的请求不需要抛错 if (err.name !== 'AbortError') { dispatch(contentHasErrored(true)); } }); }; }
注意:
fetchModuleContent按照相同逻辑改造,补充signal参数接入AbortController即可。
3. 重构轮询逻辑,避免请求堆积
去掉原有的repeat参数,等上一次请求完成(无论成功/失败)后再安排下一次定时器,从根源避免请求堆积:
fetchDisplayContent = (fetchInterval = 25000) => { clearTimeout(this._contentInterval); this._contentInterval = setTimeout(async () => { // 组件已卸载直接终止逻辑 if (!this._isMounted) return; const requestParams = []; let requestAction = this.props.fetchContent; // 按当前模式拼接请求参数 if (this._mode === "preview") { const url = config.WEB_SERVER + '/display/fetchDisplayContents?mode=prvw&localLastUpdate=0&display=' + this._displayId; requestParams.push(url, 0, this._webkey, this.abortController.signal); } else if (this._mode === "web") { const url = config.WEB_SERVER + '/display/fetchDisplayContents?mode=web&localLastUpdate=0&display=' + this._displayId; requestParams.push(url, 0, this._webkey, this.abortController.signal); } else if (this._mode === "module") { const url = config.WEB_SERVER + '/display/fetchModuleContents?mode=prvw&module=' + this._module + '&project=' + this._project + '&module_id=' + this._module_id; requestAction = this.props.fetchModuleContent; requestParams.push(url, this._webkey, this.abortController.signal); } else { const url = typeof config.DISPLAY === "number" ? this._baseURL + '/data/displayContents.json' : this._baseURL + '/data/display_' + this._displayId + '_contents/displayContents.json'; requestParams.push(url, 0, this._mode, this.abortController.signal); } try { // 等待当前请求完成再安排下一次轮询 await requestAction(...requestParams); } finally { // 组件仍处于挂载状态才继续轮询 if (this._isMounted) { this.fetchDisplayContent(); } } }, fetchInterval); }
验证标准
改造完成后用浏览器性能面板录制10分钟以上,期间手动触发几次组件重挂载,满足以下特征即为修复完成:
- 定时器回调数量不会随运行时间持续累加
- 触发GC后堆内存可以回落到稳定基线,不会出现单边持续上涨
- 网络面板不会出现大量堆积的pending状态请求
内容的提问来源于stack exchange,提问作者irongerman
相关产品推荐
相关产品推荐

