13.3 函数式组件
一个函数式组件就是一个普通函数,返回值是虚拟 DOM;它无状态、无实例,但照样可以用
mountComponent挂载、用patchComponent更新。
一、什么是函数式组件
function MyFuncComp(props) {
return { type: 'h5', children: props.title }
}
- 函数式组件本质上就是一个普通函数,该函数的返回值是虚拟 DOM;
- 它没有自身状态(没有
data、没有setup、没有组件实例),但仍然可以接收由外部传入的 props; - 在 Vue.js 3 中仍然保留函数式组件,主要是因为它的简单性,而不是因为性能好——即使是有状态组件,其初始化性能消耗也非常小。
二、props 怎么声明
给函数式组件定义 props,需要在组件函数上添加静态的 props 属性:
// 定义 props:函数式组件靠静态属性声明 props 选项
MyFuncComp.props = {
title: String
}
使用时与普通组件没有区别:
const CompVNode = {
type: MyFuncComp,
props: { title: '函数式组件' }
}
三、渲染器要做的改动(一):mountComponent 识别函数
同一个 mountComponent 既处理有状态组件,也处理函数式组件,靠 vnode.type 的类型分辨:
function mountComponent(vnode, container, anchor) {
// 检查是否是函数式组件
const isFunctional = typeof vnode.type === 'function'
// 通过 vnode 获取组件的选项对象,即 vnode.type
let componentOptions = vnode.type || {}
if (isFunctional) {
// 函数式组件:函数本身就是渲染函数,它上面的静态 props 就是 props 选项
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
const {
render, data,
props: propsOption,
beforeCreate, created,
beforeMount, mounted, beforeUpdate, updated,
setup
} = componentOptions
// ...后续流程与有状态组件共用
}
- 有状态组件:
vnode.type是选项对象,直接从里面取render/data/props/ 生命周期; - 函数式组件:把
vnode.type(函数)当作render,把vnode.type.props(静态属性)当作props选项,之后照常走下去。
四、渲染器要做的改动(二):patch 允许函数类型
patch 里判断 vnode 类型时,原来只把对象当作组件,现在要把函数也算进来:
} else if (
// type 是对象 → 有状态组件
// type 是函数 → 函数式组件
typeof type === 'object' ||
typeof type === 'function'
) {
if (!n1) {
// 挂载组件
mountComponent(n2, container, anchor)
} else {
// 更新组件
patchComponent(n1, n2, container)
}
} else if (type === 'xxx') {
// 处理其他类型的 vnode
}
少写
typeof type === 'function'这一条,函数类型的 vnode 会被当成「其它类型」直接跳过,页面上什么都不渲染。
五、与有状态组件的差异
| 维度 | 函数式组件 | 有状态组件 |
|---|---|---|
| 形态 | 函数(返回值是虚拟 DOM) | 选项对象 / setup 组件 |
| 状态 | 无自身状态,只能靠外部 props | data / setup 提供内部状态,可自更新 |
| 实例 | 没有真正的组件实例语义(无 data、setupState 等) | 有组件实例,承载 props、状态、生命周期 |
| 生命周期 | 不需要注册自己的生命周期 | 支持 onMounted / onUnmounted 等 |
| props | 通过函数上的静态 props 属性声明 | 通过选项对象里的 props 声明 |
| 挂载 | mountComponent(isFunctional 分支) | mountComponent |
| 更新 | patchComponent(比较 props 后重跑渲染函数) | patchComponent |
更新流程上两者共用 patchComponent:先用 hasPropsChanged 比较新旧 props,有变化才重新解析并写回 instance.props:
function patchComponent(n1, n2, container) {
const instance = (n2.component = n1.component)
const { props } = instance
if (hasPropsChanged(n1.props, n2.props)) {
// 注意这里用的是 n2.type.props(组件上声明的 props 选项)
const [nextProps] = resolveProps(n2.type.props, n2.props)
for (const k in nextProps) {
props[k] = nextProps[k]
}
for (const k in props) {
if (!(k in nextProps)) delete props[k]
}
}
// 之后重新执行渲染函数即可
}
六、易错点
resolveProps(n2.type.props, n2.props)依赖组件上声明的静态 props 选项:函数式组件若不写MyFuncComp.props = { title: String },props 解析拿不到类型定义,更新也会失效。
别把
vnode.props(本次运行时传入的原始 props)当成vnode.type.props(组件上静态声明的 props 选项)传给componentOptions.props——前者只是碰巧能用,后者才是正确写法。
函数式组件没有实例状态,函数体里只有入参
props可用,不要指望this上能拿到数据。
相关
- 13.2.1 封装 defineAsyncComponent 函数:另一种「组件形态的特殊处理」,靠包装组件 + 异步加载器实现