V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rookie2luochao
V2EX  ›  程序员

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

  •  1
     
  •   rookie2luochao ·
    rookie-luochao · 35 天前 · 2981 次点击
    这是一个创建于 35 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景是这样,这段代码就是 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;
    }
    
    34 条回复    2024-05-15 09:43:49 +08:00
    wpyfawkes
        1
    wpyfawkes  
       35 天前
    我不是太懂 JavaScript,以下是 Claude 的回复

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

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

    return () => {
    clearInterval(timer)
    }
    }, [])
    不知道是不是我没用懂 react
    yuuko
        13
    yuuko  
       35 天前
    没看出啥问题,找到问题后能踢我一脚吗
    dulice
        14
    dulice  
       35 天前
    在 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
        15
    forbreak  
       35 天前
    大概就是切换 tab 之后,浏览器休眠了。切回来之后积攒的那些一起执行就爆掉了。 解决方案: 监控下切换标签的事件,切走的时候就别订阅了。切回来的处理一次 token 刷新。
    blankmiss
        16
    blankmiss  
       35 天前
    @wpyfawkes @dulice 不要在 V2EX 发布 AI 生成的内容 站规 有说明
    iosyyy
        17
    iosyyy  
       35 天前
    @dulice 不要直接引用 ai 内容
    superfatboy
        18
    superfatboy  
       35 天前
    #5 感觉不用,正常情况下,refresh_token 更新,sub.unsubscribe()肯定会 执行一次
    googleaccount
        19
    googleaccount  
       35 天前
    ```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
        20
    googleaccount  
       35 天前
    我没测试上面这些代码,还一个简单粗暴的办法就是把定时器存在 ref 上 就不会出现这些问题了。
    ```js
    const subscriptionRef = useRef(null);

    useEffeect(() => {
    ...
    if (subscriptionRef.current) {
    subscriptionRef.current.unsubscribe();
    }
    ...
    subscriptionRef.current = timer()
    ...
    }, [refresh_token]);
    return () => {
    subscriptionRef.current.unsubscribe();
    };
    })
    ```
    googleaccount
        21
    googleaccount  
       35 天前
    @googleaccount 咋格式呢
    ```javascript
    const subscriptionRef = useRef(null);

    useEffeect(() => {
    ...
    if (subscriptionRef.current) {
    subscriptionRef.current.unsubscribe();
    }
    ...
    subscriptionRef.current = timer()
    ...
    return () => {
    subscriptionRef.current.unsubscribe();
    };
    }, [refresh_token]);
    ```
    leokun
        22
    leokun  
       34 天前
    这段代码不会出现内存泄漏
    大家不妨设想一下 refresh_token, expireAt 每秒都在改变,这里也不会内存泄漏,内存泄漏应该在别处
    IvanLi127
        23
    IvanLi127  
       34 天前
    改成每秒刷一次看看内存会不会快速上涨吧,单看这代码没看出太大问题
    rookie2luochao
        24
    rookie2luochao  
    OP
       34 天前
    @yuuko 待我好好定位一下,搞清楚了踢你
    rookie2luochao
        25
    rookie2luochao  
    OP
       34 天前
    @vanchKong 我们两这写法思路一样,就是我比你多了 deps 依赖
    rookie2luochao
        26
    rookie2luochao  
    OP
       34 天前
    @rabbbit 问题应该不在 timer 上,应该是 v 友说的还是在 timer 没清除上面找原因
    rookie2luochao
        27
    rookie2luochao  
    OP
       34 天前
    @lisxour 好的,你的截图看不到,我觉得也是需要学习一下分析工具了
    rookie2luochao
        28
    rookie2luochao  
    OP
       34 天前
    @renmu 你说的这个原理我也想过,这算正常机制呢,还是说需要怎么去避免
    rookie2luochao
        29
    rookie2luochao  
    OP
       34 天前
    @ZENGQH 所以我需要通过是否离开标签页做清除吗?会不会麻烦了
    rookie2luochao
        30
    rookie2luochao  
    OP
       34 天前
    @googleaccount 换到 ref 上面,确实可能更靠谱
    rookie2luochao
        31
    rookie2luochao  
    OP
       34 天前
    @leokun 内存泄漏大概率就是这里,我也是直观上看不出来代码有问题,主要还是应该对 react effect 的执行机制没有理解透彻,同事也没看出来,只能说把 deps 写全,我只写一个 token 依赖是因为 token 和 expireAt 是同步变的,所以我就只写了 token 这个依赖,然后用 ref 引用,然后多清除几次 unsubscribe ,但是这只是尝试的解决办法
    vanchKong
        32
    vanchKong  
       34 天前
    @rookie2luochao 另外,我想说,可以试一下将 timer 用 useState 去定义,我开发 rn 的时候,碰到一种情况,就是 timer 不是用 useState 定义的,然后直接 clearInterval ,但是 timer 依然在执行的情况。
    rookie2luochao
        33
    rookie2luochao  
    OP
       34 天前
    @vanchKong 嗯嗯,谢谢哈,像 V 友说的用 useRef 更靠谱一点,自己还是需要找到能定位这种内存泄漏的方法,虽然目前我的产品定位内存泄漏时间线,现在有解决办法了,我看不能找到一个简单方法去定位
    realJamespond
        34
    realJamespond  
       34 天前
    rxjs 初始化在 effect 中不应该加依赖项,依赖项应该用 ref 保存在 rxjs 中引用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4623 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 09:59 · PVG 17:59 · LAX 02:59 · JFK 05:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.