Vue 3 Options API:无关数据变更引发组件不必要重渲染问题
Vue 3 Options API 组件不必要重渲染问题分析与解决
问题场景
- 子组件的prop值来自父组件的一个method
- 该method无法替换为computed属性,因为需要对v-for循环中的特定项做处理后,再传值给对应的子组件
- 当method返回数组/对象这类引用类型时,会触发子组件不必要的重渲染;返回字符串、数字这类原始类型时则不会出现此问题
复现方式
修改父组件中与子组件无关的数据属性(比如示例中的showMenu),子组件会触发beforeUpdate钩子,打印重渲染日志。
父组件代码
<template> <div> <div id="menu"> <div @click="showMenu = !showMenu">Click Me</div> <div v-if="showMenu"> 打开控制台:修改无关属性本不应触发子组件重渲染,但由于在v-for中调用了返回数组的myMethod(chart),所以子组件会被重渲染。 </div> </div> <div v-for="(chart, index) in items" :key="index"> <MyComponent :table="myMethod(chart)" :title="chart.title" /> </div> </div> </template> <script> import MyComponent from './MyComponent.vue'; export default { components: { MyComponent, }, data: function () { return { showMenu: false, items: [{ value: 1 }, { value: 2 }], }; }, methods: { myMethod(item) { // 去掉[]返回字符串就不会触发子组件全量重渲染 return ['processed' + item.value]; } } }; </script>
子组件代码
<template> <div class="myComponent"> {{ table }} </div> </template> <script> export default { props: ['table'], beforeUpdate() { console.log('I have been rerendered'); }, }; </script> <style> .myComponent { width: 10em; height: 4em; border: solid 2px darkblue; } </style>
问题原因
Vue判断组件是否需要重渲染时,会对比prop的新旧值:
- 对于原始类型(字符串、数字、布尔等),采用值对比:只要值相同,就认为prop未变化
- 对于引用类型(数组、对象),采用引用地址对比:即使内容完全相同,只要是新创建的引用(比如每次调用
myMethod都返回新数组),Vue就会认为prop发生了变化,从而触发子组件重渲染
当父组件中showMenu这类无关属性变化时,父组件会触发重渲染,此时模板中调用myMethod(chart)会生成新的数组引用,导致子组件的table prop被判定为“变化”,进而触发不必要的重渲染。
解决方案
方案1:缓存处理后的结果(推荐)
在父组件中通过计算属性预先处理并缓存items的结果,避免每次渲染都生成新的引用:
<template> <div> <div id="menu"> <div @click="showMenu = !showMenu">Click Me</div> <div v-if="showMenu"> 现在修改showMenu不会触发子组件重渲染了 </div> </div> <div v-for="(item, index) in processedItems" :key="index"> <MyComponent :table="item.table" :title="item.title" /> </div> </div> </template> <script> import MyComponent from './MyComponent.vue'; export default { components: { MyComponent }, data() { return { showMenu: false, items: [{ value: 1 }, { value: 2 }], }; }, computed: { processedItems() { // 仅当items变化时,才重新生成处理后的数组和内部引用 return this.items.map(item => ({ ...item, table: ['processed' + item.value] })); } } }; </script>
计算属性processedItems只有在依赖的items变化时才会重新计算,父组件其他属性变化时,子组件的table prop引用不会改变,因此不会触发重渲染。
方案2:子组件使用memo控制重渲染
通过Vue的memo高阶组件,让子组件仅在prop内容真正变化时才重渲染:
<!-- MyComponent.vue --> <template> <div class="myComponent"> {{ table }} </div> </template> <script> import { memo } from 'vue'; export default memo({ props: ['table', 'title'], beforeUpdate() { console.log('I have been rerendered'); }, // 自定义对比逻辑,可选;默认是浅对比 shouldUpdate(newProps, oldProps) { // 对比数组内容是否一致 if (newProps.table.length !== oldProps.table.length) return true; for (let i = 0; i < newProps.table.length; i++) { if (newProps.table[i] !== oldProps.table[i]) return true; } // 对比title是否变化 return newProps.title !== oldProps.title; } }); </script>
memo默认会对prop做浅对比,若需要深对比,可自定义shouldUpdate函数来判断是否需要重渲染。
方案3:将引用类型转为原始类型(仅适用于简单场景)
如果业务允许,可将数组/对象序列化为字符串传递,利用原始类型的值对比避免重渲染:
<!-- 父组件模板修改 --> <MyComponent :table="JSON.stringify(myMethod(chart))" :title="chart.title" /> <!-- 子组件修改 --> <script> export default { props: ['table'], computed: { parsedTable() { return JSON.parse(this.table); } }, beforeUpdate() { console.log('I have been rerendered'); }, }; </script>
注意:此方法存在序列化/反序列化的性能开销,仅适合数据结构简单的场景。
内容的提问来源于stack exchange,提问作者daniel p
相关产品推荐
相关产品推荐

