避免内存泄漏

简介

如果你在用 Vue 开发应用,那么就要当心内存泄漏的问题。这个问题在单页应用 (SPA) 中尤为重要,因为在 SPA 的设计中,用户使用它时是不需要刷新浏览器的,所以 JavaScript 应用需要自行清理组件来确保垃圾回收以预期的方式生效。

内存泄漏在 Vue 应用中通常不是来自 Vue 自身的,更多地发生于把其它库集成到应用中的时候。

基本的示例

接下来的示例展示了一个由于在一个 Vue 组件中使用 Choices.js 库而没有将其及时清除导致的内存泄漏。等一下我们再交代如何移除这个 Choices.js 的足迹进而避免内存泄漏。

下面的示例中,我们加载了一个带有非常多选项的选择框,然后我们用到了一个显示/隐藏按钮,通过一个 v-if 指令从虚拟 DOM 中添加或移除它。这个示例的问题在于这个 v-if 指令会从 DOM 中移除父级元素,但是我们并没有清除由 Choices.js 新添加的 DOM 片段,从而导致了内存泄漏。

  1. <link rel="stylesheet prefetch" href="https://joshuajohnson.co.uk/Choices/assets/styles/css/choices.min.css?version=3.0.3">
  2. <script src="https://joshuajohnson.co.uk/Choices/assets/scripts/dist/choices.min.js?version=3.0.3"></script>
  3. <div id="app">
  4. <button
  5. v-if="showChoices"
  6. @click="hide"
  7. >Hide</button>
  8. <button
  9. v-if="!showChoices"
  10. @click="show"
  11. >Show</button>
  12. <div v-if="showChoices">
  13. <select id="choices-single-default"></select>
  14. </div>
  15. </div>
  1. new Vue({
  2. el: "#app",
  3. data: function () {
  4. return {
  5. showChoices: true
  6. }
  7. },
  8. mounted: function () {
  9. this.initializeChoices()
  10. },
  11. methods: {
  12. initializeChoices: function () {
  13. let list = []
  14. // 我们来为选择框载入很多选项
  15. // 这样的话它会占用大量的内存
  16. for (let i = 0; i < 1000; i++) {
  17. list.push({
  18. label: "Item " + i,
  19. value: i
  20. })
  21. }
  22. new Choices("#choices-single-default", {
  23. searchEnabled: true,
  24. removeItemButton: true,
  25. choices: list
  26. })
  27. },
  28. show: function () {
  29. this.showChoices = true
  30. this.$nextTick(() => {
  31. this.initializeChoices()
  32. })
  33. },
  34. hide: function () {
  35. this.showChoices = false
  36. }
  37. }
  38. })

为了实际观察一下这个内存泄露,请使用 Chrome 打开这个 CodePen 示例然后打开 Chrome 的任务管理器。Mac 下打开 Chrome 任务管理器的方式是选择 Chrome 顶部导航 > 窗口 > 任务管理;在 Windows 上则是 Shift+Esc 快捷键。现在点击展示/隐藏按钮 50 次左右。你应该在 Chrome 任务管理中发现内存的使用在增加并且从未被回收。

内存泄漏示例

解决这个内存泄漏问题

在上述的示例中,我们可以用 hide() 方法在将选择框从 DOM 中移除之前做一些清理工作,来解决内存泄露问题。为了做到这一点,我们会在 Vue 实例的数据对象中保留一个属性,并会使用 Choices API 中的 destroy() 方法将其清除。

通过这个更新之后的 CodePen 示例可以再重新看看内存的使用情况。

  1. new Vue({
  2. el: "#app",
  3. data: function () {
  4. return {
  5. showChoices: true,
  6. choicesSelect: null
  7. }
  8. },
  9. mounted: function () {
  10. this.initializeChoices()
  11. },
  12. methods: {
  13. initializeChoices: function () {
  14. let list = []
  15. for (let i = 0; i < 1000; i++) {
  16. list.push({
  17. label: "Item " + i,
  18. value: i
  19. })
  20. }
  21. // 在我们的 Vue 实例的数据对象中设置一个 `choicesSelect` 的引用
  22. this.choicesSelect = new Choices("#choices-single-default", {
  23. searchEnabled: true,
  24. removeItemButton: true,
  25. choices: list
  26. })
  27. },
  28. show: function () {
  29. this.showChoices = true
  30. this.$nextTick(() => {
  31. this.initializeChoices()
  32. })
  33. },
  34. hide: function () {
  35. // 现在我们可以让 Choices 使用这个引用
  36. // 在从 DOM 中移除这些元素之前进行清理工作
  37. this.choicesSelect.destroy()
  38. this.showChoices = false
  39. }
  40. }
  41. })

这样做的价值

内存管理和性能测试在快速交付的时候是很容易被忽视的,然而,保持小内存开销仍然对整体的用户体验非常重要。

考虑一下你的用户使用的设备类型,以及他们通常情况下的使用方式。他们使用的是内存很有限的上网本或移动设备吗?你的用户通常会做很多应用内的导航吗?如果其中之一是的话,那么良好的内存管理实践会帮助你避免糟糕的浏览器崩溃的场景。即便都不是,因为一个不小心,你的应用在经过持续的使用之后,仍然有潜在的性能恶化的问题。

实际的例子

在上述示例中,我们使用了一个 v-if 指令产生内存泄漏,但是一个更常见的实际的场景是使用 Vue Router 在一个单页应用中路由到不同的组件。

就像这个 v-if 指令一样,当一个用户在你的应用中导航时,Vue Router 从虚拟 DOM 中移除了元素,并替换为了新的元素。Vue 的 beforeDestroy() 生命周期钩子是一个解决基于 Vue Router 的应用中的这类问题的好地方。

我们可以将清理工作放入 beforeDestroy() 钩子,像这样:

  1. beforeDestroy: function () {
  2. this.choicesSelect.destroy()
  3. }

替代方案

我们已经讨论了移除元素时的内存管理,但是如果你打算在内存中保留状态和元素该怎么做呢?这种情况下,你可以使用内建的 keep-alive 组件。

当你用 keep-alive 包裹一个组件后,它的状态就会保留,因此就留在了内存里。

  1. <button @click="show = false">Hide</button>
  2. <keep-alive>
  3. <!-- `<my-component>` 即便被删除仍会刻意保留在内存里 -->
  4. <my-component v-if="show"></my-component>
  5. </keep-alive>

这个技巧可以用来提升用户体验。例如,设想一个用户在一个文本框中输入了评论,之后决定导航离开。如果这个用户之后导航回来,那些评论应该还保留着。

一旦你使用了 keep-alive,那么你就可以访问另外两个生命周期钩子:activateddeactivated。如果你想要在一个 keep-alive 组件被移除的时候进行清理或改变数据,可以使用 deactivated 钩子。

deactivated: function () {
  // 移除任何你不想保留的数据
}

总结

Vue 让开发非常棒的响应式的 JavaScript 应用程序变得非常简单,但是你仍然需要警惕内存泄漏。这些内存泄漏往往会发生在使用 Vue 之外的其它进行 DOM 操作的三方库时。请确保测试应用的内存泄漏问题并在适当的时机做必要的组件清理。