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

为啥这段代码会造成内存泄露啊?

  •  
  •   lp4298707 · 2024-04-28 15:05:11 +08:00 · 10585 次点击
    这是一个创建于 367 天前的主题,其中的信息可能已经有所发展或是发生改变。
    
    function handleData() {
      list = data.value;
      const now = new Date().getTime()
      list.forEach(item => {
        const isTop = item.remindEndTime > now
        item.shine = isTop;
        item.sort = isTop ? 0 : 1;
      })
      // 闪烁的放最前 再以更新时间排序
      list = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc'])
      visibleData.value = list
      list = null
    }
    
    onMounted(() => {
      flightClient.subscribe(WS_PREFIX + '/xxx/xxx', res => {
        data.value = res
        handleData()
      })
    
      timer = setInterval(() => {
        handleData()
      }, 300)
    })
    
    onBeforeUnmount(() => {
      clearInterval(timer)
    })
    

    每次调用我都把 list 置为空了 为什么还是会导致内存蹭蹭涨?

    如果把 list = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc']) 这段代码去掉 就没问题了

    75 条回复    2024-04-30 16:33:31 +08:00
    F7TsdQL45E0jmoiG
        1
    F7TsdQL45E0jmoiG  
       2024-04-28 15:10:01 +08:00
    两个 list 不是同一个 list
    NessajCN
        2
    NessajCN  
       2024-04-28 15:10:48 +08:00
    orderBy 定义呢?
    UGas2t22Svnlm2K2
        3
    UGas2t22Svnlm2K2  
       2024-04-28 15:12:23 +08:00
    这段代码存在潜在的内存泄漏风险,主要原因是 setInterval 和数据处理方式的结合导致的。以下是详细分析:
    潜在的内存泄漏原因:
    setInterval 和闭包:
    setInterval 函数会创建一个新的计时器,每 300 毫秒重复调用 handleData() 函数。
    每次调用 handleData() 都会创建一个闭包,闭包会捕获周围作用域中的变量引用,包括 data.value 、list 和 visibleData.value 。
    数据累积:
    当新的数据通过 websocket 订阅到达时,data.value 会更新,并调用 handleData() 函数。
    在 handleData() 内部,会创建一个新的数组 list ,并将 data.value 的值赋给它。
    然后对 list 进行排序,并将其赋值给 visibleData.value 。
    然而,原始的 list 数组,它仍然持有对旧 data.value 的引用,并没有被显式清除或垃圾回收。
    闭包引用:
    每个由 setInterval 创建的闭包都保留了对旧 list 数组的引用,即使它不再需要了。
    这就阻止了旧的 data.value 被垃圾回收,随着时间的推移导致内存累积。
    解决方案:
    在卸载时清除计时器:
    确保在 onBeforeUnmount 中正确调用 clearInterval(timer) 来停止计时器,并防止进一步创建闭包。
    避免不必要的数组创建:
    不要在每次 handleData() 调用中创建新的 list 数组,可以考虑直接对 data.value 数组进行排序,或使用更高效的排序算法。
    使用弱引用 (高级):
    如果需要在闭包中维护引用而不导致内存泄漏,可以考虑使用 WeakMap 或 WeakSet 数据结构。这些结构持有弱引用,不会阻止垃圾回收。
    注意: 使用弱引用需要仔细考虑对象生命周期和垃圾回收行为。
    其他注意事项:
    内存分析: 使用浏览器开发者工具或内存分析工具来跟踪内存使用情况并识别泄漏。
    数据大小: 如果 data.value 数组包含大量数据,内存影响会更显著。考虑高效处理大型数据集的策略。
    改进示例 (避免创建数组):
    function handleData() {
    const now = new Date().getTime();
    data.value.forEach(item => {
    const isTop = item.remindEndTime > now;
    item.shine = isTop;
    item.sort = isTop ? 0 : 1;
    });
    // 直接对 data.value 进行排序
    data.value = orderBy(data.value, ['sort', 'updateTime'], ['asc', 'desc']);
    visibleData.value = data.value;
    }
    yrk20212021
        4
    yrk20212021  
       2024-04-28 15:14:08 +08:00
    import { onMounted, onBeforeUnmount } from 'vue'

    // Assuming flightClient and WS_PREFIX are defined somewhere else

    let timer = null;

    function handleData() {
    const now = new Date().getTime();
    const newData = data.value.map(item => {
    const isTop = item.remindEndTime > now;
    return {
    ...item,
    shine: isTop,
    sort: isTop ? 0 : 1
    };
    });

    // Sort only if there are changes in shine or updateTime
    newData.sort((a, b) => {
    if (a.shine !== b.shine) {
    return a.shine ? -1 : 1;
    }
    return b.updateTime - a.updateTime;
    });

    visibleData.value = newData;
    }

    onMounted(() => {
    const subscription = flightClient.subscribe(WS_PREFIX + '/xxx/xxx', res => {
    data.value = res;
    handleData();
    });

    timer = setInterval(handleData, 300);
    });

    onBeforeUnmount(() => {
    clearInterval(timer);
    // Unsubscribe from WebSocket to prevent memory leaks
    flightClient.unsubscribe(subscription);
    });
    ProxyXAI
        5
    ProxyXAI  
       2024-04-28 15:14:28 +08:00
    这可能是因为你的 `orderBy` 函数在排序时创建了新的数组,而原有的 `list` 数组并没有被垃圾回收。这可能是因为 `visibleData.value` 仍然引用着这个数组,或者其他地方也有对这个数组的引用。

    你可以尝试在 `orderBy` 之后,手动将 `list` 设置为 `null`,或者使用 `list.length = 0` 清空数组,再设置 `visibleData.value = orderBy(...)`,这样可以确保 `list` 数组被垃圾回收。

    另外,你的 `handleData` 函数并没有声明 `list` 为局部变量,这可能导致它成为全局变量,这也可能是内存泄露的原因。你应该使用 `let` 或 `const` 关键字声明 `list`。

    示例代码:

    ```javascript
    function handleData() {
    let list = data.value;
    const now = new Date().getTime()
    list.forEach(item => {
    const isTop = item.remindEndTime > now
    item.shine = isTop;
    item.sort = isTop ? 0 : 1;
    })
    // 闪烁的放最前 再以更新时间排序
    visibleData.value = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc'])
    list.length = 0
    list = null
    }
    ```

    记住,JavaScript 的垃圾回收是基于引用的。只要一个对象没有被引用,它就会被垃圾回收。但如果你的代码中有对这个对象的引用,那么它就不会被回收,即使你手动设置为 `null`。
    flyqie
        7
    flyqie  
       2024-04-28 15:35:17 +08:00 via Android
    @yxd19 #6

    建议 @livid ,貌似还是不允许,只是不反馈就不查,要是 ai 生成后自己审阅过的貌似是允许的(因为这其实跟自己发区别不大了)。
    lp4298707
        8
    lp4298707  
    OP
       2024-04-28 15:36:02 +08:00
    @NessajCN orderBy 就是 lodash 的 orderBy
    wunonglin
        9
    wunonglin  
       2024-04-28 15:36:31 +08:00
    @Livid 这里都是 AI 回复鸭,麻了。
    flyqie
        10
    flyqie  
       2024-04-28 15:39:08 +08:00 via Android
    @flyqie #7

    不过哪怕审阅过貌似也是默认允许,如果举报好像也是会处理的。
    NessajCN
        11
    NessajCN  
       2024-04-28 15:44:46 +08:00
    @lp4298707 list 在哪儿定义的?
    ProxyXAI
        12
    ProxyXAI  
       2024-04-28 15:47:20 +08:00
    @yxd19 @livid 我错了, 我只是使用 gpt-4 期望帮助一下这个用户, 后面粘贴 ai 的了
    ProxyXAI
        13
    ProxyXAI  
       2024-04-28 15:47:40 +08:00
    后面不粘贴 AI
    lp4298707
        14
    lp4298707  
    OP
       2024-04-28 15:49:59 +08:00
    @morenacl 我改成这种方式也是不行的. visibleData.value = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc'])
    TVMXi8YPdB7idbjG
        15
    TVMXi8YPdB7idbjG  
       2024-04-28 15:57:37 +08:00
    建议贴上完整代码
    iosyyy
        16
    iosyyy  
       2024-04-28 16:01:04 +08:00
    visibleData.value 不还在吗 你只把 list = null 只改了 list 的指向 gc 扫的时候 visibleData.value 还在
    按 java gc 答的 不大知道 js gc 啥样
    iosyyy
        17
    iosyyy  
       2024-04-28 16:01:42 +08:00
    @iosyyy 也就是可达性分析还是会有 只是 list 不可达了不过 visibleData.value 还是可达的
    freeman12
        18
    freeman12  
       2024-04-28 16:02:25 +08:00
    建议放个复现链接
    Livid
        19
    Livid  
    MOD
       2024-04-28 16:04:58 +08:00
    @wunonglin 谢谢,那两个使用 AI 回复的账号已经被彻底 ban 。
    Livid
        20
    Livid  
    MOD
       2024-04-28 16:05:52 +08:00
    @flyqie 我没有时间看每天所有的新回复,但是如果收到报告,肯定会处理。
    qping
        21
    qping  
       2024-04-28 16:05:55 +08:00
    这段代码看着是想实现
    1. 订阅 websocket 的地址,然后更新到 data.value
    2. 如果 data.value 发生改变,重新排序( remindEndTime > now 的数据显示到最前面),更新到 visibleData.value

    如果上面需求理解正确的话,那实现就很奇怪
    1. 为什么 list 定义不在 handleData 内,既然用完置为 null ,那就不应该是个全局变量
    2. 为什么使用 setInterval 来更新数据,不使用 watch 来监听 data.value
    jy02534655
        22
    jy02534655  
       2024-04-28 16:07:30 +08:00
    list = data.value; 改成 const list = data.value;

    visibleData.value = list 改成深拷贝复制一份看看

    你这个每 0.3 秒就执行一次会不会太快了,setInterval 的话会不管 handleData 是否执行完成都会执行吧?
    要不在 handleData 里面写 settimeout ,等 handleData 执行完,并且通过 nextTick 自调用来实现循环看看呢
    lp4298707
        23
    lp4298707  
    OP
       2024-04-28 16:12:10 +08:00
    @NessajCN list 在函数外部定义的,我改成在 handleData 内部定义也是一样的结果
    lp4298707
        24
    lp4298707  
    OP
       2024-04-28 16:13:34 +08:00
    @qping 因为想要实现 remindEndTime > now 就显示最前面 因为需要实时刷新 now 我就设置的 1S 执行一次 handleData, 这里我因为想要快速看到效果 把时间设置得更短了
    lp4298707
        25
    lp4298707  
    OP
       2024-04-28 16:14:31 +08:00
    @jy02534655 你说的深拷贝 递归调用 我都试过了 都是一样的效果
    qping
        26
    qping  
       2024-04-28 16:20:23 +08:00
    @qping 第二个看明白了,是想不停的根据当前时间去重新计算排序

    你可以试试把 list 改成 handleData 内定义试试
    NessajCN
        27
    NessajCN  
       2024-04-28 16:21:35 +08:00
    @lp4298707
    list = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc']);
    改成
    orderBy(list, ['sort', 'updateTime'], ['asc', 'desc']);

    或者
    const ordered_list = orderBy(list, ['sort', 'updateTime'], ['asc', 'desc'])
    visibleData.value = ordered_list
    CivAx
        28
    CivAx  
       2024-04-28 16:24:30 +08:00   ❤️ 1
    @yxd19 #6 好酷!原来还可以这样 Highlight 页内文本!
    yxd19
        29
    yxd19  
       2024-04-28 16:27:02 +08:00
    @CivAx chrome 某次更新之后的功能吧。选中文字右键菜单。
    nevermoreluo
        30
    nevermoreluo  
       2024-04-28 16:30:55 +08:00
    打印过时间吗?
    有没有可能数据太多了。。。。setInterval 300 毫秒来不及处理?
    imherer
        31
    imherer  
       2024-04-28 16:35:06 +08:00
    @CivAx 这个 Highlight 是咋实现的哦
    Arrowing
        32
    Arrowing  
       2024-04-28 16:35:47 +08:00
    全是闭包,不出问题才怪呢。
    最主要的 orderBy 定义没写出来
    g0python32
        33
    g0python32  
       2024-04-28 16:48:49 +08:00   ❤️ 4
    lp4298707
        34
    lp4298707  
    OP
       2024-04-28 16:53:43 +08:00
    @Arrowing orderBy 就是 loadsh 的 orderBy
    lp4298707
        35
    lp4298707  
    OP
       2024-04-28 16:54:17 +08:00
    @nevermoreluo 我改成 2 秒也是一样的 只是内存增长速度变缓了
    lisongeee
        36
    lisongeee  
       2024-04-28 16:59:56 +08:00
    看起来你的 data 是 vue 里的的 ref ,如果是 vue 的话

    你换成 shallowRef 试试呢,ref 有深度劫持的问题,shallowRef 不会有这个问题
    asdjgfr
        37
    asdjgfr  
       2024-04-28 17:04:32 +08:00
    你这代码复制下来运行了一下也没有内存泄漏呀
    retanoj
        38
    retanoj  
       2024-04-28 17:23:30 +08:00
    @qping #21 我想问个小白问题
    subscribe 订阅了指定 topic 之后,每次 topic 有新消息来不就会触发 res => {} 函数吗?
    为啥还要用 setInterval 做个 timer
    gitdoit
        39
    gitdoit  
       2024-04-28 17:28:12 +08:00
    看了下 orderBy 的源码, 会创建新的数组;但是搞不懂为什么内存会持续增长. 大对象会被立即回收吗?
    chairuosen
        40
    chairuosen  
       2024-04-28 17:35:51 +08:00
    zhua 一下内存快照,看看谁增长的快
    chairuosen
        41
    chairuosen  
       2024-04-28 17:39:15 +08:00
    你 onBeforeUnmount 没有 unsubscribe ,会不会是这个,组件如果是每次父级刷新全新创建,就有问题了
    asasjajsajsd
        42
    asasjajsajsd  
       2024-04-28 17:45:22 +08:00
    不是,我有点奇怪,就不能后端给你干这个事情么,数据库排序很快啊
    ysc3839
        43
    ysc3839  
       2024-04-28 17:48:47 +08:00 via Android
    提一下,js 取时间戳可以直接写 +new Date()
    ysc3839
        44
    ysc3839  
       2024-04-28 17:56:12 +08:00 via Android
    @ysc3839 刚刚去测了一下性能,发现 Date.now()性能最好,+new Date()性能最差,new Date().getTime()排在中间。
    edward1987
        45
    edward1987  
       2024-04-28 17:57:32 +08:00
    神奇,不好复现啊主要,OP 看下能不能整个最小复现代码放出来看下。
    wawaguo
        46
    wawaguo  
       2024-04-28 17:58:18 +08:00
    来自 Code Copilot 的回答:
    wawaguo
        47
    wawaguo  
       2024-04-28 17:58:32 +08:00
    这段代码的内存泄露可能由多种因素引起。我会逐一分析可能的原因,并提供解决方案:

    1. 闭包中的变量未被释放
    闭包(比如在 setInterval 、forEach 或事件订阅回调中定义的函数)常常会意外捕获并持久保持一些变量,尤其是在你反复设置定时器或不断订阅事件时。如果这些闭包保持对外部变量(如 list )的引用,这些变量就不会被垃圾回收。

    解决方案:
    确保在不需要时解除对变量的引用,或者在组件卸载时清除所有相关的订阅和定时器。

    2. 定时器未正确清除
    在代码中,onBeforeUnmount 用来清除定时器,但如果因为某种原因这个生命周期钩子没有被正确调用(或者定时器的引用 timer 被意外修改),定时器将继续运行,从而可能导致内存泄露。

    解决方案:
    确保 timer 变量不会在其他地方被误修改。
    在组件的生命周期中确保 onBeforeUnmount 正确调用。
    3. Websocket 订阅未取消
    如果你的 Websocket 订阅在组件卸载时没有被取消,订阅中的回调函数可能持续持有一些变量的引用,从而导致内存泄露。

    解决方案:
    在 onBeforeUnmount 钩子中添加取消 Websocket 订阅的逻辑。

    4. 数据绑定导致的内存泄露
    如果使用了双向数据绑定或响应式数据系统(如 Vue 、React 状态),并且数据更新频率非常高,可能会导致旧数据对象未能及时回收。

    解决方案:
    减少不必要的数据变动。
    手动触发垃圾回收(这种方式并不推荐,因为它依赖于具体的 JavaScript 引擎实现)。
    5. 高频度的 DOM 操作或重渲染
    高频的更新可能导致 DOM 重绘或组件重渲染,如果处理不当,旧的 DOM 节点或组件实例可能无法被垃圾回收。

    解决方案:
    使用虚拟 DOM 或有效的重渲染策略来优化渲染过程。
    确保在更新数据前,不必要的 DOM 元素已被正确清除或重用。
    检查和调试方法:
    使用浏览器的开发者工具中的内存分析工具。
    检查定时器和事件监听器是否都被正确清理。
    观察内存使用情况,看是否随时间持续增长。
    通过这些分析和调整,你应该能够识别并修复代码中的内存泄露问题。如果需要具体的代码修改建议,我可以帮助进一步修改。
    RICKEYGONG
        48
    RICKEYGONG  
       2024-04-28 18:01:28 +08:00
    RICKEYGONG
        49
    RICKEYGONG  
       2024-04-28 18:03:07 +08:00   ❤️ 1
    @Livid ai 生成的
    Livid
        50
    Livid  
    MOD
       2024-04-28 18:06:04 +08:00
    @RICKEYGONG 47 楼的账号已经被彻底 ban 。
    zhhbstudio
        51
    zhhbstudio  
       2024-04-28 18:43:31 +08:00
    Vue2 还是 Vue3 ,数据量大概多少
    PS:Vue2 可以通过 composition-api 使用类似 Vue3 的 API
    xiangyuecn
        52
    xiangyuecn  
       2024-04-28 18:56:37 +08:00
    你这点代码能泄露个啥,甩锅给 vue ,寿命+1 。
    TKI
        53
    TKI  
       2024-04-28 19:41:18 +08:00
    wellerman
        54
    wellerman  
       2024-04-28 19:45:26 +08:00
    1. setInterval 执行用了匿名函数,由于匿名函数定义在全局,可能会导致内存泄露

    按 #4 楼给的方法,直接调用命名函数:
    timer = setInterval(handleData, 300);

    2. 防止 orderBy( 处理时间超过 300ms

    var working = false;
    function handleData() {
    if (working) {
    return;
    }
    working = true;
    ...
    working = false;
    }
    lulinchuanllc
        55
    lulinchuanllc  
       2024-04-28 20:54:03 +08:00
    flightClient.subscribe 在卸载时也应该 off 掉吧,否则这里的变量都被消息订阅列表里的订阅函数缓存了
    lp4298707
        56
    lp4298707  
    OP
       2024-04-28 21:53:01 +08:00
    @TKI 我运行这个看浏览器资源管理器也是占用了 1.4 个 G,你的没有这个情况吗?
    DOLLOR
        57
    DOLLOR  
       2024-04-29 01:53:00 +08:00
    建议把 data 和 visibleData 都改成浅响应( shallowRef )。
    我推测 orderBy 在处理 list 的时候,因为 vue 的响应式处理造成了过重的负担。
    visper
        58
    visper  
       2024-04-29 09:47:21 +08:00
    这里代码应该没有什么内存泄露,猜测可能是因为 timeout 300 毫秒执行频率太高,然后数组又太大,orderby 里面每次又生成一个新的 list 重新给 vue 再重新处理初始化响应。造成浏览器内存回收没这么及时。
    coderHu
        59
    coderHu  
       2024-04-29 09:47:32 +08:00
    现在解决了么?蹲个解决方案
    ColdBird
        60
    ColdBird  
       2024-04-29 09:52:32 +08:00
    如果 visibleData.value 不是 null ,说明 list 不是原来的 list 了,不然一定是 null
    话说这代码写的也有点抽象
    ColdBird
        61
    ColdBird  
       2024-04-29 09:54:17 +08:00
    @ysc3839 所以有的自动格式化工具会把+new Date()转成 Date.now()
    ColdBird
        62
    ColdBird  
       2024-04-29 09:57:06 +08:00
    感觉大概率就是 list 没有内部声明的原因,handleData 内声明一下 const list = XXX ,或者干脆不在前面生成 list ,直接 const list = orderBy
    TKI
        63
    TKI  
       2024-04-29 10:51:19 +08:00
    @lp4298707 #56 我这边看着占用 300M 左右,你是在浏览器的任务管理看的吗?;另外打开控制台,性能面板里看着内存没有一直增长。
    davin
        64
    davin  
       2024-04-29 11:30:05 +08:00
    变量作用域被污染。另外,可以使用 requestAnimationFrame 替代 setInterval 。
    privatezcoding
        65
    privatezcoding  
       2024-04-29 12:00:45 +08:00
    没有人关心从 flightClient 获取到的数据大小吗?
    privatezcoding
        66
    privatezcoding  
       2024-04-29 12:10:53 +08:00
    我猜测你这里是因为原始网络数据本身就是个比较大的 list 。然后你又用 setInterval 每隔 300ms 用 orderby 创建新的数组。

    解决方案:
    1. flightClient 获取数据的时候根据 remindEndTime 和 updateTime 排序
    2. setInterval 里直接对 remindEndTime 进行判断原地对数据进行处理
    chill777
        67
    chill777  
       2024-04-29 13:40:03 +08:00
    0.3s 执行一次,是不是太频繁了
    Marthemis
        68
    Marthemis  
       2024-04-29 18:11:44 +08:00
    这里没有内存泄漏,setinterval + orderBy 会创建大量的对象,因为 orderBy 的返回是一个新的数组。在内存快速增长到一定的阈值时会强制触发垃圾回收,从而最终稳定在一个较大的内存使用率值。(你可以加一个按钮去中断这个定时器,看看过一段时间内存会不会降下来)
    lp4298707
        69
    lp4298707  
    OP
       2024-04-30 16:28:52 +08:00
    @DOLLOR 感谢,这样确实解决了问题, 应该是 vue 内部没有及时回收这个资源? 在用 ref 的时候 内存只增不减 用 shallowRef 之后 到了一定的时间就会自动减少内存占用. 再次感谢
    lp4298707
        70
    lp4298707  
    OP
       2024-04-30 16:30:08 +08:00
    @wellerman 这种方式我试过,没有能解决问题, 最后我根据楼下的大佬的方式 使用 shallowRef 解决了这个问题
    lp4298707
        71
    lp4298707  
    OP
       2024-04-30 16:30:34 +08:00
    @TKI 是的 我在浏览器的任务管理器看的,最后我根据楼下的大佬的方式 使用 shallowRef 解决了这个问题
    lp4298707
        72
    lp4298707  
    OP
       2024-04-30 16:31:51 +08:00
    @visper 最后我根据楼上的大佬的方式 使用 shallowRef 解决了这个问题
    lp4298707
        73
    lp4298707  
    OP
       2024-04-30 16:32:06 +08:00
    @coderHu 最后我根据楼上的大佬的方式 使用 shallowRef 解决了这个问题
    lp4298707
        74
    lp4298707  
    OP
       2024-04-30 16:32:38 +08:00
    @privatezcoding 数据其实不大的, 最后我根据楼上的大佬的方式 使用 shallowRef 解决了这个问题
    lp4298707
        75
    lp4298707  
    OP
       2024-04-30 16:33:31 +08:00
    @ColdBird 内部声明我也试过的 一样的结果, 最后我根据楼下的大佬的方式 使用 shallowRef 解决了这个问题
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2397 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 13:07 · PVG 21:07 · LAX 06:07 · JFK 09:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.