vue3 组件粒度对组件渲染的影响

68 阅读5分钟

在开发复杂的用户界面时,合理的组件划分是保证应用程序高效和可维护性的关键。Vue3 提供了很多解决方案,能够帮助开发者构建性能优异且易于管理的组件体系。本文将探讨组件的更新机制,并通过具体实例分析优化策略,帮助开发者避免常见的性能问题。

组件的更新时机

在 Vue 3 中,响应式对象的依赖收集是组件级别的,每当组件模板中使用的响应式数据发生改变时,Vue 框架会触发整个组件的重新渲染过程,具体来说,Vue 会再次执行组件的渲染函数,创建一个新的虚拟 DOM(VDOM)树。接着 Vue 会将新的 VDOM 与旧的 VDOM 进行 Diff 算法比较,找出真正需要更新的 DOM 节点,然后更新需要变化的 DOM 节点,其他未变化的节点保持不变。

示例一:

<!-- ParentComponent.vue -->
<template>
  <div>----这里是父组件----</div>
  <div>{{ title }}</div>
  <div>{{ getRandomString() }}</div>

  <ChildComponent :message="message" />

  <button @click="changeTitle">Change title</button>
  <button @click="changeMessage">Change Message</button>
</template>

<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

const title = ref('this is title');
const message = ref('Hello in parent');

const changeTitle = () => {
  title.value = 'this is new title';
};

const changeMessage = () => {
  message.value = 'New message in parent';
};

const getRandomString = () => {
  console.log('ParentComponent 组件更新了!');
  return Math.floor(Math.random() * 100000);
};
</script>

子组件

<!-- ChildComponent.vue -->
<template>
  <div class="content">
    <div>----这里是子组件----</div>
    <div>{{ getRandomString() }}</div>
  </div>
</template>

<script setup>
import { defineProps } from 'vue';
defineProps({
  message: String,
});

const getRandomString = () => {
  console.log('ChildComponent 组件更新了!');
  return Math.floor(Math.random() * 10000);
};
</script>

<style>
.content {
  background-color: #e4f4e8;
  padding: 10px;
}
</style>

代码演练

在上述示例中,每次组件渲染调用渲染函数时,getRandomString() 都会执行,返回一个新的随机字符串,通过使用 getRandomString() 可以用来监测组件是否发生渲染。

根据上面的代码示例,思考以下几个问题:

问题 1:父组件和子组件模版中均未使用 message,message 仅作为子组件 ChildComponent 的一个传参,当执行 changeMessage 时,哪些组件会重新渲染?

问题 2:执行 changeTitle 时,哪些组件会重新渲染?

问题 3:同时执行 changeMessage 和 changeTitle, 又会发生什么变化?

按开头提到的组件的更新时机,那上面答案也其实很简单:

  • 父组件中执行 changeMessage 方法时,父子组件会同步触发了渲染,即便父组件和子组件内部没有使用 message 属性。
  • 执行 changeTitle 方法时,只有父组件触发了渲染,子组件不受影响。
  • 若同时执行 changeMessage 和 changeTitle,父子组件会同步触发了渲染,得益于 Vue3 的组件渲染的调度机制,一个组件内部多个响应式数据发生变更,组件渲染只会执行一次。

可以看出,如果父组件管理了子组件太多的状态,更新子组件的状态会引起父组件的更新

示例二:

<template>
  <div>
    {{ getRandomString() }}
    <!-- 将b对象传递给子组件,并监听来自子组件的更新事件 -->
    <child-component
      :b="state.a.b"
      @updateD="handleUpdateD"
      @updateB="handleUpdateB"
    >
    </child-component>
  </div>
</template>

<script>
import { reactive } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent,
  },
  setup() {
    // 定义嵌套对象结构
    const state = reactive({
      a: {
        b: {
          c: {
            d: 0,
          },
        },
      },
    });

    // 处理来自子组件的更新请求
    const handleUpdateD = (newD) => {
      state.a.b.c.d = newD;
    };

    const handleUpdateB = (newB) => {
      state.a.b = newB;
    };

    const getRandomString = () => {
      console.log('ParentComponent 组件更新了!');
      return Math.floor(Math.random() * 100000);
    };

    return {
      state,
      getRandomString,
      handleUpdateD,
    };
  },
};
</script>

ChildComponent.vue 子组件如下:

<template>
  <div>
    <p>Current D value in b.c.d: {{ b.c.d }}</p>
    <button @click="changeD">Change d</button>
    <button @click="changeB">Change b</button>
  </div>
</template>

<script>
export default {
  props: {
    b: {
      type: Object,
      required: true,
    },
  },
  emits: ['updateD', 'updateB'],
  setup(props, { emit }) {
    const changeD = () => {
      emit('updateD', Math.random());
    };

    const changeB = () => {
      emit('updateB', {
        c: {
          d: Math.random(),
        },
      });
    };

    return {
      changeD,
      changeB,
    };
  },
};
</script>

代码演练

上面的例子中,执行 handleUpdateD,父组件不会更新,但执行 handleUpdateB,父组件会更新。

渲染性能优化

Vue 3 的响应式系统和编译优化(如静态提升、树结构拍平)对细粒度组件更友好:

更精确的依赖追踪

细粒度组件的模板通常依赖更少的数据,数据变化时仅触发相关组件的更新,减少不必要的渲染。

<!-- 父组件 -->
<template>
  <ChildA :data="dataA" />
  <ChildB :data="dataB" />
</template>

当 dataA 变化时,父组件和 ChildA 重新渲染,ChildB 不受影响。

代码演练

如果改成下面的形式,当 dataA 变化时,只有 ChildA 重新渲染,ChildB 不受影响。

<!-- 父组件 -->
<template>
  <ChildA />
  <ChildB />
</template>

更高效的虚拟 DOM Diff

拆分后的组件具有更小的模板范围,虚拟 DOM 的 Diff 计算量更少。

编译优化

Vue 3 的编译器会优化静态内容(如 hoistStatic),细粒度组件更容易被优化为“块”(Block),减少运行时开销。

内存与实例化开销

组件粒度过细可能带来副作用:

组件实例内存开销

每个组件实例需要维护独立的响应式上下文、生命周期等,粒度过细会增加内存占用。

通信成本增加

父子组件通过 props/emit 通信,粒度过细可能导致频繁的数据传递,增加维护成本。

平衡建议:对高频更新的部分拆分为细粒度组件,对静态或低频更新的部分保持适当聚合。

组合式 API 的灵活性

Vue 3 的组合式 API(Composition API)允许逻辑复用与解耦,使得细粒度组件更易维护:

逻辑复用

将 UI 拆分为小组件,同时通过复用逻辑,避免重复代码。

// 复用逻辑
const useFormValidation = () => {
  // 校验逻辑...
};

// 组件内部
setup() {
  const { validate } = useFormValidation();
  return { validate };
}

实际开发中的权衡

高频更新组件:如实时输入框、动画元素,拆分为细粒度组件可减少渲染范围。

低频更新组件:如静态展示区块,过度拆分可能得不偿失。

状态管理策略:细粒度组件可能依赖全局状态(如 Pinia),需合理设计状态共享,避免 props 层层传递。

总结

  • 推荐细粒度场景:高频交互、独立数据更新的部分。
  • 推荐适当聚合场景:静态内容、低频更新或逻辑紧密耦合的部分。
  • 结合 Vue 3 特性:利用响应式优化、组合式 API 和编译优化,在性能与可维护性间取得平衡。

通过合理划分组件粒度,可以显著提升 Vue 3 应用的渲染效率,同时保持代码的可维护性。