MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue计算属性与侦听器 性能调优与内存管理技巧

2022-01-056.4k 阅读

Vue 计算属性基础

在 Vue 应用开发中,计算属性是一个非常强大的功能。它允许我们基于现有数据进行复杂的计算,并将计算结果缓存起来。

假设有一个简单的 Vue 组件,我们需要根据两个数据字段计算它们的和。如果不使用计算属性,我们可能会在模板中直接进行计算:

<template>
  <div>
    <p>数值1: {{ num1 }}</p>
    <p>数值2: {{ num2 }}</p>
    <p>它们的和: {{ num1 + num2 }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num1: 10,
      num2: 20
    };
  }
};
</script>

虽然这样能得到正确结果,但每次模板重新渲染时,num1 + num2 都会重新计算。如果这个计算过程很复杂,将会浪费性能。

这时,计算属性就派上用场了。我们可以将这个计算逻辑封装到计算属性中:

<template>
  <div>
    <p>数值1: {{ num1 }}</p>
    <p>数值2: {{ num2 }}</p>
    <p>它们的和: {{ sum }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num1: 10,
      num2: 20
    };
  },
  computed: {
    sum() {
      return this.num1 + this.num2;
    }
  }
};
</script>

在上述代码中,sum 是一个计算属性。它会在 num1num2 发生变化时重新计算,并且会缓存计算结果。也就是说,如果 num1num2 都没有变化,再次访问 sum 时,不会重新执行 sum 函数中的计算逻辑,而是直接返回缓存的结果。

计算属性的缓存机制使得它在性能优化方面具有很大的优势。特别是在计算逻辑复杂,并且依赖的数据变化频率较低的情况下,使用计算属性可以显著提高应用的性能。

计算属性的依赖追踪

Vue 的计算属性之所以能实现高效的缓存,关键在于它的依赖追踪机制。每个计算属性都有一个依赖收集器,它会在计算属性求值时,记录下当前计算属性所依赖的所有响应式数据。

例如,在前面的 sum 计算属性中,它依赖于 num1num2。当 num1num2 发生变化时,Vue 会检测到这种变化,并标记 sum 计算属性为无效状态。下次访问 sum 时,它会重新计算,并再次缓存结果。

我们可以通过一个稍微复杂一点的例子来深入理解依赖追踪。假设我们有一个包含用户信息的 Vue 组件,用户有 firstNamelastName,我们要计算出完整的 fullName

<template>
  <div>
    <p>名: {{ firstName }}</p>
    <p>姓: {{ lastName }}</p>
    <p>全名: {{ fullName }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    };
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName;
    }
  }
};
</script>

在这个例子中,fullName 计算属性依赖于 firstNamelastName。当 firstNamelastName 中的任何一个发生变化时,fullName 会被重新计算。

计算属性的 setter

计算属性默认只有 getter 方法,用于获取计算结果。但在某些情况下,我们可能也需要为计算属性定义 setter 方法,以便在计算属性值被修改时执行一些逻辑。

例如,我们有一个表示用户年龄的计算属性 age,并且希望在设置 age 时,能够同时更新相关的 birthYear

<template>
  <div>
    <p>出生年份: {{ birthYear }}</p>
    <p>年龄: {{ age }}</p>
    <button @click="updateAge">增加年龄</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      birthYear: 1990
    };
  },
  computed: {
    age: {
      get() {
        const currentYear = new Date().getFullYear();
        return currentYear - this.birthYear;
      },
      set(newAge) {
        const currentYear = new Date().getFullYear();
        this.birthYear = currentYear - newAge;
      }
    }
  },
  methods: {
    updateAge() {
      this.age = this.age + 1;
    }
  }
};
</script>

在上述代码中,age 计算属性有一个 set 方法。当我们调用 this.age = this.age + 1 时,set 方法会被触发,从而更新 birthYear

侦听器基础

Vue 的侦听器提供了一种响应数据变化的机制。与计算属性不同,侦听器更侧重于在数据变化时执行副作用操作,比如异步请求、DOM 操作等。

我们来看一个简单的例子,假设我们有一个搜索框,当用户输入内容时,我们要根据输入内容进行搜索:

<template>
  <div>
    <input v-model="searchText" placeholder="搜索">
    <p>搜索结果: {{ searchResults }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchText: '',
      searchResults: []
    };
  },
  watch: {
    searchText(newValue, oldValue) {
      // 模拟异步搜索
      setTimeout(() => {
        this.searchResults = this.getSearchResults(newValue);
      }, 500);
    }
  },
  methods: {
    getSearchResults(text) {
      // 实际应用中这里会是一个真实的搜索逻辑
      return [text + '的模拟结果1', text + '的模拟结果2'];
    }
  }
};
</script>

在这个例子中,我们通过 watch 选项监听 searchText 的变化。当 searchText 发生变化时,会执行 searchText 对应的函数,在这个函数中,我们模拟了一个异步搜索操作,并更新 searchResults

深度监听

有时候,我们需要监听对象内部属性的变化,而不仅仅是对象引用的变化。这时候就需要用到深度监听。

假设我们有一个包含用户详细信息的对象 user,我们希望监听 user.address.city 的变化:

<template>
  <div>
    <input v-model="user.address.city" placeholder="城市">
    <p>监听到城市变化: {{ cityChanged }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: 'Alice',
        address: {
          city: 'Beijing'
        }
      },
      cityChanged: ''
    };
  },
  watch: {
    user: {
      handler(newValue, oldValue) {
        this.cityChanged = newValue.address.city;
      },
      deep: true
    }
  }
};
</script>

在上述代码中,通过设置 deep: true,我们开启了深度监听。这样,即使 user.address.city 发生变化,handler 函数也会被触发。

立即执行的侦听器

默认情况下,侦听器是在数据变化后才执行。但在某些场景下,我们希望在组件创建时就立即执行一次侦听器函数。

例如,我们有一个需要根据用户当前位置加载相关数据的功能,并且希望在组件创建时就获取一次位置信息并加载数据:

<template>
  <div>
    <p>加载的数据: {{ loadedData }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userLocation: null,
      loadedData: []
    };
  },
  watch: {
    userLocation: {
      handler(newValue) {
        this.loadedData = this.fetchDataByLocation(newValue);
      },
      immediate: true
    }
  },
  methods: {
    fetchDataByLocation(location) {
      // 实际应用中这里会是一个根据位置获取数据的逻辑
      return [location + '的模拟数据'];
    },
    getLocation() {
      // 模拟获取位置信息
      this.userLocation = 'Somewhere';
    }
  },
  created() {
    this.getLocation();
  }
};
</script>

在这个例子中,通过设置 immediate: trueuserLocation 的侦听器在组件创建时就会立即执行一次 handler 函数,从而在获取到位置信息后就加载相关数据。

计算属性与侦听器性能对比

在性能方面,计算属性和侦听器各有优劣,需要根据具体场景选择使用。

计算属性由于其缓存机制,在依赖数据变化不频繁且计算逻辑复杂的情况下,性能表现非常好。因为只有依赖数据发生变化时才会重新计算,否则直接返回缓存结果。

而侦听器则更适合处理异步操作和副作用。但如果侦听器监听的数据变化频繁,并且每次变化都执行复杂的逻辑,可能会导致性能问题。

例如,我们有一个展示商品列表的组件,商品列表根据用户选择的分类进行过滤。如果使用计算属性:

<template>
  <div>
    <select v-model="selectedCategory">
      <option value="all">全部</option>
      <option value="electronics">电子产品</option>
      <option value="clothes">服装</option>
    </select>
    <ul>
      <li v-for="product in filteredProducts" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedCategory: 'all',
      products: [
        { id: 1, name: '手机', category: 'electronics' },
        { id: 2, name: 'T恤', category: 'clothes' },
        { id: 3, name: '电脑', category: 'electronics' }
      ]
    };
  },
  computed: {
    filteredProducts() {
      if (this.selectedCategory === 'all') {
        return this.products;
      }
      return this.products.filter(product => product.category === this.selectedCategory);
    }
  }
};
</script>

在这个例子中,使用计算属性 filteredProducts 来过滤商品列表。只有当 selectedCategory 发生变化时,filteredProducts 才会重新计算,性能较好。

如果使用侦听器:

<template>
  <div>
    <select v-model="selectedCategory">
      <option value="all">全部</option>
      <option value="electronics">电子产品</option>
      <option value="clothes">服装</option>
    </select>
    <ul>
      <li v-for="product in filteredProducts" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedCategory: 'all',
      products: [
        { id: 1, name: '手机', category: 'electronics' },
        { id: 2, name: 'T恤', category: 'clothes' },
        { id: 3, name: '电脑', category: 'electronics' }
      ],
      filteredProducts: []
    };
  },
  watch: {
    selectedCategory(newValue) {
      if (newValue === 'all') {
        this.filteredProducts = this.products;
      } else {
        this.filteredProducts = this.products.filter(product => product.category === newValue);
      }
    }
  }
};
</script>

这里使用侦听器来实现同样的功能。每次 selectedCategory 变化时,都会执行侦听器函数来重新计算 filteredProducts。相比计算属性,在这个场景下,计算属性的性能会更优,因为它利用了缓存机制。

计算属性的性能调优

  1. 合理使用缓存:确保计算属性依赖的数据确实会变化,并且变化频率较低。如果计算属性依赖的数据变化非常频繁,缓存可能无法带来显著的性能提升,甚至可能因为频繁的无效化和重新计算而降低性能。
  2. 避免复杂计算嵌套:尽量避免在计算属性中进行过于复杂的嵌套计算。如果计算逻辑非常复杂,可以考虑将其拆分成多个简单的计算属性,或者封装成独立的函数。例如:
<template>
  <div>
    <p>结果: {{ finalResult }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num1: 10,
      num2: 20,
      num3: 30
    };
  },
  computed: {
    intermediateResult1() {
      return this.num1 + this.num2;
    },
    intermediateResult2() {
      return this.num2 * this.num3;
    },
    finalResult() {
      return this.intermediateResult1 * this.intermediateResult2;
    }
  }
};
</script>

在这个例子中,通过将复杂计算拆分成多个中间计算属性,使得每个计算属性的逻辑更清晰,并且在依赖数据变化时,只有相关的计算属性会重新计算,提高了性能。

侦听器的性能调优

  1. 防抖和节流:当侦听器监听的数据变化频繁时,可以使用防抖和节流技术来控制侦听器函数的执行频率。
    • 防抖:在数据变化后,延迟一定时间再执行侦听器函数。如果在延迟时间内数据又发生了变化,则重新计时。例如,在搜索框输入时,我们不希望每次输入都立即发起搜索请求,而是在用户停止输入一段时间后再发起请求。
<template>
  <div>
    <input v-model="searchText" placeholder="搜索">
    <p>搜索结果: {{ searchResults }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchText: '',
      searchResults: []
    };
  },
  watch: {
    searchText: {
      handler(newValue) {
        if (this.debounceTimer) {
          clearTimeout(this.debounceTimer);
        }
        this.debounceTimer = setTimeout(() => {
          this.searchResults = this.getSearchResults(newValue);
        }, 500);
      },
      immediate: true
    }
  },
  methods: {
    getSearchResults(text) {
      // 实际应用中这里会是一个真实的搜索逻辑
      return [text + '的模拟结果1', text + '的模拟结果2'];
    }
  }
};
</script>
- **节流**:限制侦听器函数在一定时间内只能执行一次。比如,在滚动条滚动事件中,我们可能希望每隔一定时间执行一次处理函数,而不是每次滚动都执行。
<template>
  <div @scroll="handleScroll">
    <p>滚动位置: {{ scrollPosition }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      scrollPosition: 0,
      throttleTimer: null
    };
  },
  methods: {
    handleScroll() {
      if (!this.throttleTimer) {
        this.scrollPosition = window.pageYOffset;
        this.throttleTimer = setTimeout(() => {
          this.throttleTimer = null;
        }, 200);
      }
    }
  }
};
</script>
  1. 减少不必要的监听:只监听真正需要的数据源。如果一个侦听器监听了过多的数据,可能会导致不必要的函数执行,降低性能。例如,在一个包含多个表单字段的组件中,如果某个侦听器只需要关注其中一个字段的变化,就不要监听整个表单对象。

Vue 中的内存管理

在 Vue 应用中,合理的内存管理对于应用的性能和稳定性至关重要。当组件被销毁时,如果没有正确清理相关的资源,可能会导致内存泄漏。

  1. 清除定时器:如果在组件中使用了定时器,在组件销毁时一定要清除定时器。例如:
<template>
  <div>
    <p>当前时间: {{ currentTime }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentTime: new Date(),
      timer: null
    };
  },
  created() {
    this.timer = setInterval(() => {
      this.currentTime = new Date();
    }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.timer);
  }
};
</script>

在上述代码中,beforeDestroy 钩子函数中清除了定时器 this.timer,避免了内存泄漏。

  1. 解绑事件监听器:如果在组件中手动绑定了 DOM 事件监听器,在组件销毁时要解绑这些监听器。比如:
<template>
  <div ref="myDiv">
    <p>点击次数: {{ clickCount }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      clickCount: 0
    };
  },
  mounted() {
    this.$refs.myDiv.addEventListener('click', this.handleClick);
  },
  methods: {
    handleClick() {
      this.clickCount++;
    }
  },
  beforeDestroy() {
    this.$refs.myDiv.removeEventListener('click', this.handleClick);
  }
};
</script>

这里在 mounted 钩子函数中绑定了 click 事件监听器,在 beforeDestroy 钩子函数中解绑了该监听器,防止内存泄漏。

  1. 处理组件间的引用:当一个组件持有对其他组件或外部对象的引用时,在组件销毁时要确保这些引用被正确清理。例如,有一个父组件包含一个子组件,并且父组件在某个方法中保存了对子组件的引用:
<!-- ParentComponent.vue -->
<template>
  <div>
    <ChildComponent ref="child"></ChildComponent>
    <button @click="useChild">使用子组件</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      childRef: null
    };
  },
  methods: {
    useChild() {
      this.childRef = this.$refs.child;
      this.childRef.doSomething();
    }
  },
  beforeDestroy() {
    this.childRef = null;
  }
};
</script>
<!-- ChildComponent.vue -->
<template>
  <div>
    <p>子组件</p>
  </div>
</template>

<script>
export default {
  methods: {
    doSomething() {
      console.log('子组件执行操作');
    }
  }
};
</script>

在父组件的 beforeDestroy 钩子函数中,将 childRef 设置为 null,解除对子组件的引用,避免潜在的内存泄漏。

计算属性与内存管理

虽然计算属性本身不会直接导致内存泄漏,但如果计算属性返回的是一个引用类型,并且这个引用在组件销毁后仍然被持有,可能会引发内存问题。

例如,计算属性返回一个对象,并且在模板中使用了这个对象:

<template>
  <div>
    <p>{{ myObject.value }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    myObject() {
      return { value: '一些值' };
    }
  }
};
</script>

在这种情况下,虽然 myObject 是一个计算属性,但由于模板中持有对返回对象的引用,即使组件销毁,这个对象可能仍然存在于内存中。如果这种情况频繁发生,可能会导致内存占用过高。

为了避免这种情况,可以考虑在组件销毁时,手动清除对这些对象的引用。例如,可以在 beforeDestroy 钩子函数中,将模板中使用的相关变量设置为 null

侦听器与内存管理

侦听器在内存管理方面也需要特别注意。如果侦听器函数中创建了一些外部资源,如定时器、网络请求等,在组件销毁时要确保这些资源被正确清理。

比如,在侦听器中创建了一个定时器:

<template>
  <div>
    <input v-model="count">
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
      timer: null
    };
  },
  watch: {
    count(newValue) {
      if (this.timer) {
        clearInterval(this.timer);
      }
      this.timer = setInterval(() => {
        this.count++;
      }, 1000);
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
};
</script>

在上述代码中,当 count 变化时,会创建一个定时器。在组件销毁时,通过 beforeDestroy 钩子函数清除定时器,防止内存泄漏。

另外,如果侦听器监听的是一个对象,并且在侦听器函数中对该对象进行了深度操作,可能会导致对象的引用无法被正确释放。例如,在侦听器中对监听的对象进行了属性添加或删除操作,并且这些操作导致对象的引用关系变得复杂,在组件销毁时,要确保这些引用关系被正确清理,以避免内存泄漏。

通过合理运用计算属性和侦听器,并结合良好的内存管理技巧,可以使 Vue 应用在性能和稳定性方面得到显著提升。在实际开发中,要根据具体的业务场景,仔细权衡计算属性和侦听器的使用,同时注意内存管理的细节,以打造高效、稳定的前端应用。