组件中的 data 为什么必须是一个函数呢?
一、先给结论
在 Vue 2 和 Vue 3 的 Options API 组件中,data 应该是一个返回初始状态对象的函数:
export default {
data() {
return {
count: 0,
}
},
}
原因不是因为“函数类型比对象类型更响应式”,而是因为组件定义可能被创建成多个组件实例,每个实例都必须拿到一份独立的数据对象。
如果组件定义直接保存一个对象,那么所有实例都会引用同一个对象;一个实例修改 count,其他实例也会看到变化。
二、组件定义和组件实例不是同一个东西
组件定义可以理解为一个可复用的“模板/工厂描述”:
const Counter = {
data() {
return { count: 0 }
},
}
当模板中多次使用它:
<Counter />
<Counter />
<Counter />
Vue 会根据同一个组件定义创建三个不同的组件实例。每个实例都有自己的 props、data、computed、methods 和生命周期。
data() 会在创建每个实例时分别调用:
function createComponentState() {
return { count: 0 }
}
const stateA = createComponentState()
const stateB = createComponentState()
console.log(stateA === stateB) // false
因此,修改 stateA.count 不会影响 stateB.count。
三、如果 data 写成对象会怎样
Vue 2 的组件定义如果这样写:
const Counter = {
// Vue 2 组件中不允许这样写 data
data: {
count: 0,
},
}
概念上相当于:
const sharedData = { count: 0 }
const instanceA = { data: sharedData }
const instanceB = { data: sharedData }
instanceA.data.count++
console.log(instanceB.data.count) // 1
这个问题的根本是 JavaScript 对象是引用类型,而不是 Vue 响应式本身造成的:
const a = { count: 0 }
const b = a
b.count++
console.log(a.count) // 1
如果每个实例分别拥有不同的对象,即使这些对象内部字段相同,也不会发生共享:
const a = { count: 0 }
const b = { count: 0 }
b.count++
console.log(a.count) // 0
四、Vue 2 中的规则
Vue 2 官方文档明确要求:组件的 data 必须是返回对象的函数:
Vue.component('button-counter', {
data() {
return {
count: 0,
}
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>
`,
})
如果直接写对象,Vue 2 会在开发环境给出类似“组件 data 必须是函数”的警告。
为什么根实例可以写对象
Vue 2 的根实例通常只创建一次:
new Vue({
el: '#app',
data: {
message: 'Hello Vue',
},
})
它没有被当成可重复创建的组件定义,因此可以使用对象。即使根实例也写成函数,通常也能工作,但 Vue 2 传统用法允许根实例使用对象。
要区分:
- 根 Vue 实例:历史上可以直接写
data: {}; - 可复用组件:
data必须是返回新对象的函数。
五、Vue 3 中的规则
Vue 3 Options API 组件仍然使用函数形式:
<script>
export default {
data() {
return {
count: 0,
}
},
}
</script>
Vue 3 的应用创建方式不同:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
这里的 App 本身也是一个组件定义。对于 Vue 3 Options API,使用 data() 返回初始状态是兼容且推荐的写法。
如果使用 Composition API 或 <script setup>,则不再写 data 选项:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">
You clicked me {{ count }} times.
</button>
</template>
此时 count 是当前组件 setup 作用域中的状态;每次创建组件实例,setup 都会为该实例执行并创建自己的 ref。
六、函数不是魔法:必须返回新对象
下面的写法仍然会共享状态:
const shared = {
count: 0,
}
export default {
data() {
// 函数返回的仍然是同一个外部对象
return shared
},
}
正确方式是在每次调用时创建对象:
export default {
data() {
return {
count: 0,
form: {
keyword: '',
tags: [],
},
}
},
}
不仅最外层对象要新建,嵌套对象和数组也不能从模块级变量直接复用:
// 不推荐:所有实例共享同一份数组
const defaultTags = []
export default {
data() {
return {
tags: defaultTags,
}
},
}
可以使用工厂函数统一生成默认状态:
function createInitialState() {
return {
count: 0,
tags: [],
form: {
keyword: '',
},
}
}
export default {
data() {
return createInitialState()
},
}
七、和响应式转换有什么关系
在组件实例创建过程中,Vue 会把 data() 返回的对象纳入当前实例的响应式系统:
- Vue 2 使用 getter/setter 转换对象已有属性;
- Vue 3 Options API 会把 data 状态接入 Vue 3 的响应式机制;
- 每个组件实例有自己的 data 对象,因此每个实例也有独立的响应式依赖;
data函数本身并不是响应式系统,函数的主要作用是生成独立对象。
所以“data 必须是函数”主要解决的是实例之间的数据隔离,不是为了解决“函数才能被 Proxy 监听”。
八、相关的工厂函数场景
1. Prop 的对象/数组默认值
Prop 的对象或数组默认值也需要工厂函数,以免多个组件实例共享同一个默认值。
Vue 2/Options API 常见写法:
export default {
props: {
options: {
type: Object,
default: () => ({
pageSize: 20,
}),
},
tags: {
type: Array,
default: () => [],
},
},
}
Vue 3.5 的 <script setup> 类型 props 还可以通过编译器生成默认值;无论写法如何,默认对象/数组都应避免跨实例共享。
2. Composable 的状态
Composable 也应在函数内部创建 state:
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
如果把 const count = ref(0) 放在模块顶层,那么所有调用 useCounter() 的组件会共享它;这可能是 Pinia/全局状态想要的效果,但如果目标是每个组件独立,就应该放在函数内部。
九、常见误解
误解 1:组件 data 写对象一定会导致所有页面共用
准确说法是:组件定义复用同一个 data 对象时会共享;根实例只有一个时没有多个实例之间的污染问题。
误解 2:函数返回对象后就完全不会共享
如果函数返回模块级对象、缓存对象或同一个嵌套数组,仍然可能共享。关键是每次调用都创建需要隔离的引用类型值。
误解 3:Vue 3 取消了 data
Vue 3 仍支持 Options API 和 data();只是新项目常使用 Composition API,不需要再把状态放在 data 选项中。
误解 4:data 函数负责监听数据
监听由 Vue 的响应式系统完成。data() 负责生成实例的初始状态,随后 Vue 才会把它接入响应式转换。
总结
- 可复用组件会创建多个实例;
- 如果组件
data是同一个对象,所有实例就会共享引用; data()在每个实例创建时执行并返回新对象,从而隔离状态;- Vue 2 根实例历史上允许
data: {},组件则要求data(); - Vue 3 Options API 仍使用
data(),Composition API 则用函数作用域中的ref/reactive; - 函数必须真正创建新对象,不能返回模块级共享对象;
- prop 默认对象/数组和 composable 状态也要根据是否需要隔离来选择工厂函数或共享状态。
参考资料
原文作者:霞霞哇。原文关于组件复用、对象引用和 data 工厂函数的核心解释予以保留;过时的单一 Vue 2 API 链接、重复表述和“函数自动创建作用域”等不严谨说法已整理或补充。