v 友们帮忙看看这段我几年前写的 定时刷新 token 的 js 代码,为什么会内存泄露?

218 天前
 rookie2luochao

背景是这样,这段代码就是 token 过期前 2 分钟刷新下 token ,但是它会在我切换到其他网页之后(这个刷新 token 的网页不关闭,后台运行),过很长一段时间在切回来就可能爆内存(可能 12 小时,可能 24 小时?),但是时间太短不会爆内存,所以我一直没找到很好的办法去测试定位

timer 是 rxjs 的定时器操作符

export function RefreshTokenComp({
  refreshTokenActor,
}: {
  refreshTokenActor: RequestActor<{
    refreshToken: string;
  }>;
}) {
  const [access$, updateAccess] = useAccessMgr();
  const access = useObservable(access$) || {};
  const { refresh_token, expireAt } = access;

  const [refreshTokenRequest] = useRequest(refreshTokenActor, {
    onSuccess({ arg }) {
      updateAccess(fromOAuthToken(arg.data));
    },
  });

  useEffect(() => {
    if (!refresh_token) return;
    const expiresIn = moment(expireAt).diff(moment(), "s") - 120; // token 过期前二分钟左右刷新
    const sub = timer(expiresIn * 1000).subscribe(() => {
      refreshTokenRequest({ refreshToken: refresh_token });
    });
    return () => {
      sub.unsubscribe();
    };
  }, [refresh_token]);

  return null;
}
3273 次点击
所在节点    程序员
34 条回复
wpyfawkes
218 天前
我不是太懂 JavaScript,以下是 Claude 的回复

函数实现
函数实现看起来是正确的。它使用了 useEffect 来处理 token 的刷新,这是 React 中常见的做法。然而,这段代码可能存在潜在的内存泄漏问题。在 useEffect 的清理函数中,我们取消了定时器的订阅,但是如果 refresh_token 在定时器触发之前改变,那么定时器可能会被意外地触发两次。我们应该在 useEffect 的依赖数组中添加 expiresIn 。
Laobai
218 天前
大概率是定时器创建了,没有被销毁
rookie2luochao
218 天前
@Laobai 肯定,我就是不知道怎么样导致它没有销毁,我以为写了 sub.unsubscribe() 就算销毁了
rookie2luochao
218 天前
@wpyfawkes 哈哈哈,这个是收费的吗,我去试试按这个建议改进一下呢
Brain777
218 天前
const sub = timer(expiresIn * 1000).subscribe(() => {
refreshTokenRequest({ refreshToken: refresh_token });
// 感觉应该需要在刷新之后清理定时器订阅
sub.unsubscribe();
});
rookie2luochao
218 天前
@wpyfawkes 应该是 expireAt ,这个假 AI😂
rookie2luochao
218 天前
@Brain777 从代码上面看我觉得可以不用,但是像你说的刷新之后多清除一下,能避免一些想不到的情况
lisxour
218 天前
可以直接用浏览器自带的分析工具,对比两次快照,看下有什么东西是一直加的。

rabbbit
218 天前
没看出啥问题,timer 换成 setTimeout 试试还会有问题吗?
renmu
218 天前
我猜是切换到其他标签页后,页面的定时器没有再执行,当你切回来后,页面没有执行的定时器一起被执行了,然后爆了
ZENGQH
218 天前
切换标签后 导致的计时器异常,有个属性可以判断是否为当前便签页
vanchKong
218 天前
以我浅薄的 rn 开发经历,我是这么写的:
useEffect(() => {
let timer = setInterval(() => {
...
}, 2000)

return () => {
clearInterval(timer)
}
}, [])
不知道是不是我没用懂 react
yuuko
218 天前
没看出啥问题,找到问题后能踢我一脚吗
XV5V7stzN1ns0TPL
218 天前
在 JavaScript 应用程序中,内存泄漏可能由多种原因引起,特别是当使用异步操作和事件监听器时。在你提供的代码片段中,有几个可能导致内存泄漏的潜在因素:

1. **定时器订阅未正确清理**:代码中使用了 RxJS 的 `timer` 创建了一个定时器,并订阅了它。在组件卸载时,通过调用 `sub.unsubscribe()` 来取消订阅,这是正确的做法。然而,如果组件卸载不完全或者 `unsubscribe` 调用时机不当,订阅可能不会被正确清理。

2. **闭包陷阱**:如果 `useEffect` 的回调函数或其返回的清理函数形成了闭包,它们可能会捕获并保留组件的状态,即使组件不再渲染。

3. **组件状态未清除**:如果组件状态(如 `access`)在组件卸载后仍然被某些引用或事件监听器保留,这可能会导致内存泄漏。

4. **全局事件监听器**:如果代码中有全局事件监听器,并且这些监听器引用了组件的状态或回调,它们可能会阻止组件被垃圾回收。

5. **异步更新队列**:JavaScript 的异步更新队列可能在组件卸载后仍然尝试更新组件状态,导致内存泄漏。

要解决这个问题,你可以尝试以下方法:

- **确保组件卸载**:使用 React DevTools 或类似工具检查组件是否真的被卸载了。

- **使用条件渲染**:只在需要时渲染组件,避免不必要的挂载和卸载。

- **优化状态管理**:避免在组件状态中存储大量数据或长生命周期的对象。

- **使用 WeakMap 或 WeakRef**:如果需要存储对组件的引用,使用 `WeakMap` 或 `WeakRef` 可以减少内存泄漏的风险。

- **避免长生命周期的闭包**:确保闭包不会捕获不必要的组件状态。

- **手动管理订阅**:如果可能,手动管理 RxJS 订阅的生命周期,确保在组件卸载前取消所有订阅。

- **测试和调试**:使用内存泄漏检测工具,如 Chrome DevTools 的 Heap Snapshot 比较,来识别和定位内存泄漏。

- **代码审查**:对相关代码进行彻底的审查,查找可能导致内存泄漏的逻辑错误。

最后,为了更好地测试和定位内存泄漏,你可以尝试以下方法:

- **自动化测试**:编写自动化测试来模拟长时间运行的场景。

- **性能监控**:在开发环境中使用内存分析工具,如 React DevTools 或其他 JavaScript 性能监控工具。

- **模拟长时间运行**:在测试环境中模拟长时间运行的场景,比如使用循环或定时器来频繁触发组件的挂载和卸载。

- **逐步调试**:逐步执行代码,检查在特定操作后内存的使用情况。

通过这些方法,你应该能够更准确地定位和解决内存泄漏问题。
forbreak
218 天前
大概就是切换 tab 之后,浏览器休眠了。切回来之后积攒的那些一起执行就爆掉了。 解决方案: 监控下切换标签的事件,切走的时候就别订阅了。切回来的处理一次 token 刷新。
blankmiss
218 天前
@wpyfawkes @dulice 不要在 V2EX 发布 AI 生成的内容 站规 有说明
iosyyy
218 天前
@dulice 不要直接引用 ai 内容
superfatboy
218 天前
#5 感觉不用,正常情况下,refresh_token 更新,sub.unsubscribe()肯定会 执行一次
googleaccount
218 天前
```js
export function RefreshTokenComp({
refreshTokenActor,
}: {
refreshTokenActor: RequestActor<{
refreshToken: string;
}>;
}) {
const [access$, updateAccess] = useAccessMgr();
const access = useObservable(access$) || {};
const { refresh_token, expireAt } = access;
let sub:Timer
const [refreshTokenRequest] = useRequest(refreshTokenActor, {
onSuccess({ arg }) {
updateAccess(fromOAuthToken(arg.data));
},
});

useEffect(() => {
if (!refresh_token) return;
if (sub) sub.unsubscribe();
const expiresIn = moment(expireAt).diff(moment(), "s") - 120; // token 过期前二分钟左右刷新
sub = timer(expiresIn * 1000).subscribe(() => {
refreshTokenRequest({ refreshToken: refresh_token });
});
return () => {
sub.unsubscribe();
};
}, [refresh_token]);

return null;
}
``` 试试这样
你这个 refresh_token 每隔两分钟就会变 说明 useEffect 里面的定时器每隔两分钟就会执行一次 执行多了不就爆了没,每次执行前清空一下就好了
googleaccount
218 天前
我没测试上面这些代码,还一个简单粗暴的办法就是把定时器存在 ref 上 就不会出现这些问题了。
```js
const subscriptionRef = useRef(null);

useEffeect(() => {
...
if (subscriptionRef.current) {
subscriptionRef.current.unsubscribe();
}
...
subscriptionRef.current = timer()
...
}, [refresh_token]);
return () => {
subscriptionRef.current.unsubscribe();
};
})
```

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1040538

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX