函数组件和异步组件

"函数式组件" 和 "异步组件" 是 Vue 中两种不同定位的组件形态,前者通过 "无状态、无实例" 精简渲染流程,后者通过 "按需加载" 减少初始资源体积,二者从不同维度优化性能,具体解析如下:

一、函数式组件:无状态、无实例的 "轻量渲染器"

1. 核心定义

函数式组件是 仅接收 propscontext 作为参数、无自身状态(无 data/reactive)、无组件实例(无 this)、无生命周期钩子 的组件,本质是一个 "纯函数"------ 输入 props 后直接返回虚拟 DOM,不参与组件实例的创建和挂载流程。

在 Vue 2 中需通过 functional: true 声明,Vue 3 中则直接用 "无 <script setup> 的单文件组件" 或 "返回虚拟 DOM 的函数" 实现,例如:

组件UserCard

xml 复制代码
<!-- Vue 3 函数式组件:仅渲染,无状态 -->
<template functional>
  <div class="user-card">
    <img :src="props.avatar" alt="用户头像" />
    <div>{{ props.name }}</div>
  </div>
</template>

<script>
// 也可通过 JS 定义:接收 props,返回虚拟 DOM
export default function UserCard(props) {
  return h('div', { class: 'user-card' }, [
    h('img', { src: props.avatar, alt: '用户头像' }),
    h('div', props.name)
  ]);
}
</script>

2. 函数组件≠jsx

函数组件可以用jsx写,jsx只是一种语法,函数组件的强调在状态和实例

举个例子:

javascript 复制代码
// 用 JSX 写的普通组件(有状态,不是函数组件)
import { ref } from 'vue';

export default () => {
  // 有自身状态(count),不符合函数组件"无状态"特征
  const count = ref(0);

  // 有自身事件处理逻辑
  const handleAdd = () => {
    count.value++;
  };

  // JSX 渲染,但组件是普通组件
  return (
    <div>
      <span>计数:{count.value}</span>
      <button onClick={handleAdd}>+1</button>
    </div>
  );
};

2. 性能提升原理:跳过 "组件实例创建" 流程

Vue 普通组件的渲染需经历 "创建组件实例 → 初始化状态 → 执行生命周期 → 渲染虚拟 DOM" 等完整流程,而函数式组件会 跳过 "实例创建" 和 "状态初始化" 步骤 ,直接根据 props 生成虚拟 DOM,减少内存占用和渲染耗时。

3. 适用场景:纯展示、高频复用的轻量组件

仅当组件满足 "无状态、仅渲染" 时,用函数式组件才能提升性能,典型场景:

  • 列表项组件:如表格行、列表项(v-for 循环渲染几十上百个的场景,减少实例数量);
  • 纯展示组件:如标签(Tag)、头像(Avatar)、按钮组(ButtonGroup)等无交互或仅触发父组件事件的组件;
  • 高阶组件包装:如用于封装逻辑、生成新组件的 "容器组件"(无自身状态,仅转发 props)。

注意 :若组件需状态(如 ref/reactive)、生命周期(如 onMounted)或复杂交互(如内部事件处理),则不适合用函数式组件 ------ 强行使用会导致代码复杂度上升,反而抵消性能优势。

二、异步组件:按需加载的 "延迟渲染组件"

1. 核心定义

异步组件是 不在初始渲染时加载,而是在 "需要时"(如路由跳转、条件渲染触发)才动态加载组件代码 的组件,本质是通过 "代码分割" 将组件打包成独立的 chunk 文件,避免初始包体积过大。

Vue 3 中通过 defineAsyncComponent 声明,Vue 2 中通过 "返回 Promise 的函数" 声明,例如:

javascript 复制代码
// Vue 3 异步组件:路由跳转时才加载 UserDetail 组件
import { defineAsyncComponent } from 'vue';
import { Loading, ErrorComponent } from './components';

// 定义异步组件,指定加载函数、加载中/加载失败占位组件
const UserDetail = defineAsyncComponent({
  loader: () => import('./UserDetail.vue'), // 动态导入,打包成独立 chunk
  loadingComponent: Loading, // 加载中显示的组件
  errorComponent: ErrorComponent, // 加载失败显示的组件
  delay: 200, // 延迟 200ms 显示加载组件(避免闪屏)
  timeout: 3000 // 3 秒加载超时则显示错误组件
});

// 路由配置中使用:访问 /user/:id 时才加载 UserDetail
const routes = [
  { path: '/user/:id', component: UserDetail }
];

2. 性能提升原理:减少初始资源体积

普通组件会被打包到主包(如 app.js)中,若项目包含大量组件(如几十个页面组件),会导致初始包体积过大(如超过 2MB),首屏加载时间变长;而异步组件会 被单独打包成小 chunk(如 UserDetail.[hash].js ,初始加载时仅下载主包,需要时再通过网络请求加载组件 chunk,从而减少首屏加载时间和初始内存占用。

三、核心差异

维度 函数式组件 异步组件
核心特性 无状态、无实例、同步渲染 有状态 / 无状态均可、异步加载、延迟渲染
性能优化点 减少组件实例创建开销,提升渲染速度 减少初始包体积,提升首屏加载速度
适用组件类型 纯展示、高频复用的轻量组件 非首屏、条件触发的重量级组件
代码分割 不涉及代码分割,组件代码在主包中 强制代码分割,组件代码在独立 chunk 中

怎么理解函数式组件会 跳过 "实例创建" 和 "状态初始化" 步骤,呢?

Vue 中普通组件和函数式组件的渲染流程差异,本质是 "是否创建组件实例" 导致的流程分支。下面从源码的角度去拆分下这个过程:

一、普通组件的完整渲染流程(包含实例创建)

普通组件的渲染是一个 "从组件定义到 DOM 挂载" 的完整生命周期,可分为 5 个核心步骤,每一步都和 "组件实例" 强绑定:

1. 解析组件定义,准备创建实例

当 Vue 解析到模板中的组件标签(如 <UserForm>)时,会先读取该组件的选项定义(data/methods/computed 等),然后调用 Vue 内部的 createComponentInstance 方法,初始化一个组件实例对象(VNode 组件实例) 。这个实例对象会包含:

  • 基础属性:uid(唯一标识)、vnode(虚拟 DOM 节点)、parent(父实例)等;
  • 状态容器:ctx(上下文,用于存放 data/props 等)、setupState(组合式 API 的状态)等;
  • 方法引用:emit 方法、生命周期钩子队列等。

2. 初始化组件状态(实例的核心工作)

实例创建后,Vue 会执行 initComponent 方法,为实例 "注入" 状态和能力:

  • 处理 props :将父组件传入的 props 解析后挂载到实例的 ctx 中(如 this.props.name);
  • 初始化 data :执行 data() 函数,将返回的对象通过 reactive 转为响应式数据,挂载到实例(如 this.count);
  • 绑定 computed/watch:将计算属性和监听器与实例关联,依赖收集时绑定到实例的更新逻辑;
  • 处理生命周期 :将 mounted/updated 等钩子函数添加到实例的钩子队列,等待触发时机。

3. 执行初始化生命周期钩子

实例状态准备好后,Vue 会按顺序执行初始化阶段的生命周期钩子:

  • beforeCreate:此时 propsdata 尚未挂载到实例,无法访问;
  • createdpropsdata 已初始化,可通过 this 访问,但 DOM 尚未生成。

4. 渲染虚拟 DOM(VNode)

初始化完成后,Vue 调用实例的 render 方法(模板会被编译为 render 函数),生成组件的虚拟 DOM 树(VNode)。这个过程中,render 函数通过 this 访问实例上的 props/data(如 this.name),最终生成描述 DOM 结构的 VNode 对象(包含标签名、属性、子节点等信息)。

5. 挂载真实 DOM,执行挂载生命周期

  • 虚拟 DOM 转真实 DOM :Vue 调用 patch 方法,将 VNode 转换为真实 DOM 节点,并插入到父组件的 DOM 中;
  • 执行挂载钩子 :触发 beforeMount → 真实 DOM 挂载完成 → 触发 mounted
  • 实例关联 DOM :实例的 el 属性(Vue 2)或 vnode.el(Vue 3)指向真实 DOM,方便后续更新。

二、函数式组件的渲染流程(跳过实例创建)

函数式组件因为 "无状态、无实例",流程被大幅简化,直接跳过 "实例创建" 和 "状态初始化",仅保留 "输入 props → 输出 VNode" 的核心步骤:

1. 解析组件定义,确认函数式标识

当 Vue 解析到函数式组件(如 <template functional> 或返回 VNode 的函数)时,会识别其 "函数式" 标识(Vue 3 中通过 functional: true 或无状态函数判断),直接进入轻量渲染流程。

2. 直接接收 props 和上下文(无实例,无初始化)

  • 函数式组件没有实例,所以不需要创建 componentInstance 对象;

  • 父组件传入的 props 和上下文(slots/emit 等)会被直接打包成参数,传递给渲染函数(模板或 JSX 函数)。例如:

    • 模板式函数组件中,通过 props.xxx 直接访问数据,context.emit 触发事件;
    • JSX 函数组件中,函数参数直接接收 propscontext(props, context) => { ... })。

3. 生成虚拟 DOM(直接渲染,无生命周期)

函数式组件的 "渲染函数"(模板编译后的函数或 JSX 函数)会直接使用 propscontext 生成 VNode,过程中:

  • 不需要访问 this(因为没有实例);
  • 不需要处理响应式数据初始化(props 由父组件传入,已在父组件中完成响应式处理);
  • 没有生命周期钩子(无需执行 created/mounted 等)。

4. 挂载真实 DOM(复用父组件的挂载流程)

生成的 VNode 会直接进入父组件的 patch 流程,和父组件的其他节点一起被转换为真实 DOM。因为没有实例,所以 DOM 挂载后也不会触发任何生命周期钩子,完成渲染后即结束。

三、一句话总结

普通组件的渲染是 "先创建一个'管理者(实例)',由管理者统筹状态、生命周期和渲染 ",流程完整但冗余;函数式组件的渲染是 "无管理者,直接用输入数据生成输出结果",跳过所有和实例相关的步骤,因此更轻量、更快。

相关推荐
淮北4943 小时前
html + css +js
开发语言·前端·javascript·css·html
你的人类朋友3 小时前
适配器模式:适配就完事了bro!
前端·后端·设计模式
怪兽20144 小时前
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
java·缓存·面试
Setsuna_F_Seiei4 小时前
CocosCreator 游戏开发 - 利用 AssetsBundle 技术对小游戏包体积进行优化
前端·cocos creator·游戏开发
黄毛火烧雪下4 小时前
高效的项目构建和优化之前端构建工具
前端
90后的晨仔4 小时前
在 macOS 上轻松获取 GIF 图片总时长:多种实用方法详解
前端
技术钱4 小时前
vue3前端解析excel文件
前端·vue.js·excel
mapbar_front5 小时前
顺利通过试用期:避开三大陷阱,掌握三个关键点
前端·面试
掘金安东尼5 小时前
Transformers.js:让大模型跑进浏览器
开发语言·javascript·ecmascript