技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

组件中的 data 为什么必须是一个函数呢?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/93-组件中的 data 为什么必须是一个函数呢?
本文目录11 个章节
  1. 一、先给结论
  2. 二、组件定义和组件实例不是同一个东西
  3. 三、如果 data 写成对象会怎样
  4. 四、Vue 2 中的规则
  5. 五、Vue 3 中的规则
  6. 六、函数不是魔法:必须返回新对象
  7. 七、和响应式转换有什么关系
  8. 八、相关的工厂函数场景
  9. 九、常见误解
  10. 总结
  11. 参考资料

组件中的 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 链接、重复表述和“函数自动创建作用域”等不严谨说法已整理或补充。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS