专栏名称: 前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
目录
相关文章推荐
今日闵行  ·  再加码、无门槛!6折打车福利别错过→ ·  昨天  
班主任家园  ·  中国监狱2025招聘公告 ·  昨天  
896汽车调频  ·  武大靖,有新身份 ·  2 天前  
896汽车调频  ·  武大靖,有新身份 ·  2 天前  
51好读  ›  专栏  ›  前端从进阶到入院

为了解决内存泄露,我把 Vue 源码改了

前端从进阶到入院  · 公众号  ·  · 2025-02-19 17:16

正文

前言

彦祖们,好久不见,最近一直忙于排查单位业务的终端内存泄露问题,已经吃了不下 10 个 bug

但是排查内存泄露在前端领域属于比较冷门的领域了

这篇文章笔者将带你一步步分享业务实践中遇到的内存泄露问题以及如何修复的经历

本文涉及技术栈

  • vue2
场景复现


如果之前有看过我文章的彦祖们,应该都清楚

笔者所在的单位有一个终端叫做工控机(类似于医院挂号的终端),没错!所有的 bug 都源自于它😠

因为内存只有 1G 所以一旦发生内存泄露就比较可怕

不过没有这个机器 好像也不会创作这篇文章😺

复现 demo

彦归正传,demo 其实非常简单,只需要一个最简单的 vue2 demo 就可以了

  • App.vue
<template>  <div id




    
="app">    <button @click="render = true">renderbutton>    <button @click="render = false">destroybutton>    <Test v-if="render"/>  div>template><script>import Test from './test.vue'export default {  name'App',  components: {    Test  },  data () {    return {      renderfalse    }  }}script><style>#app {  font-family: Avenir, Helvetica, Arial, sans-serif;  -webkit-font-smoothing: antialiased;  -moz-osx-font-smoothing: grayscale;  text-align: center;  color#2c3e50;}style>
  • test.vue
<template>  <div class="test">    <div>{{ total }}div>    
      v-for="(item,index) in 1000"      :key="`${item}-${index}`"      class="item"    >      {{ item }}ipc-prod2.8    div>  div>template>
<script>export default {  name'Test',  data () {    return {      total1000    }  },  mounted () {    this.timer = setTimeout(() => {      this.total = 10000    }, 500)  },  beforeDestroy () {    clearTimeout(this.timer)  }}script>

复现 demo

以下流程建议彦祖们在  chrome 无痕模式下执行

  1. 我们点击 render 按钮渲染 test 组件,此时我们发现 dom 节点的个数来到了 2045

图片

考虑到有彦祖可能之前没接触过这块面板,下图展示了如何打开此面板

图片
  1. 500ms 后(定时器执行完成后,如果没复现可以把 500ms 调整为 1000ms, 1500ms),我们点击 destroy 按钮
  2. 我们点击面板这里的强制回收按钮(发现节点并没有回收,已发生内存泄露)
图片

如果你的浏览器是最新的 chrome,还能够点击这里的 已分离的元素(detached dom),再点击录制

图片

我们会发现此时整个 test 节点已被分离

图片

问题分析

那么问题到底出在哪里呢?

vue 常见泄露场景

笔者搜遍了全网,网上所说的不外乎以下几种场景

1.「未清除的定时器」

2.「未及时解绑的全局事件」

3.「未及时清除的 dom 引用」

4.「未及时清除的 全局变量」

5.「console 对引用类型变量的劫持」

好像第一种和笔者的场景还比较类似,但是仔细看看代码好像也加了

beforeDestroy () {  clearTimeout(this.timer)}

这段代码啊,就算不加,timer 执行完后,事件循环也会把它回收掉吧

同事提供灵感

就这样笔者这段代码来回测试了半天也没发现猫腻所在

这时候同事提供了一个想法说"total 更新的时候是不是可以提供一个 key"

改了代码后就变成了这样了

  • test.vue
<template>  <div class="test">    <div :key="renderKey">{{ total }}div>    
      v-for="(item,index) in 1000"      :key="`${item}-${index}`"      class="item"    >      {{ item }}ipc-prod2.8    div>  div>template>
<script>export default {  name'Test',  data () {    return {      renderKey0,      total1000    }  },  mounted () {    this.timer = setTimeout(() => {      this.total = 10000      this.renderKey = Date.now()    }, 500)  },  beforeDestroy () {    clearTimeout(this.timer)  }}script>

神奇的事情就这样发生了,笔者还是按以上流程测试了一遍,直接看结果吧

图片

我们看到这个 DOM 节点曲线,在 destroy 的时候能够正常回收了

问题复盘

最简单的 demo 问题算是解决了

但是应用到实际项目中还是有点困难

难道我们要把每个更新的节点都手动加一个 key 吗?

其实仔细想想,有点 vue 基础的彦祖应该了解这个 key 是做什么的?

不就是为了强制更新组件吗?

等等,强制更新组件?更新组件不就是 updated 吗?

updated 涉及的不就是八股文中我们老生常谈的 patch 函数吗?(看来八股文也能真有用的时候😺)

那么再深入一下, patch 函数内部不就是 patchVnode  其核心不就是 diff 算法吗?

首对首比较,首对尾比较,尾对首比较,尾对尾比较 这段八股文要是个 vuer 应该都不陌生吧?😺

动手解决

其实有了问题思路和想法

那么接下来我们就深入看看 vue 源码内部涉及的 updated 函数到底在哪里吧?

探索 vue 源码

我们找到 node_modules/vue/vue.runtime.esm.js

图片

我们看到了 _update 函数真面目,其中有个 __patch__ 函数,我们再重点查看一下

图片


图片

createPatchFunction 最后 return 了这个函数

图片

我们最终来看这个 updateChildren 函数

图片

其中多次出现了上文中所提到的八股文,每个都用 sameVnode进行了对比

  • function sameVnode
function sameVnode (a, b) {    return (a.key === b.key &&        a.asyncFactory === b.asyncFactory &&        ((a.tag === b.tag &&            a.isComment === b.isComment &&            isDef(a.data) === isDef(b.data) &&            sameInputType(a, b)) ||            (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));}

果然这里我们看到了上文中 key 的作用

key 不一样就会认作不同的 vnode

那么就会强制更新节点

对应方案

既然找到了问题的根本

在判定条件中我们是不是直接加个 || a.text !== b.text

强制对比下文本节点不就可以了吗?

修改 sameVnode

看下我们修改后的 sameVnode

function sameVnode (a, b) {    






请到「今天看啥」查看全文