Vue 组件的 data 为什么必须是函数?
组件会被创建很多个实例,data 写成对象时所有实例共用同一个对象,一个实例改了,其他实例跟着变;写成函数后每个实例调用一次,各拿一份新对象。Vue 2 的根实例可以写对象,Vue 3 里 data 只接受函数。
简答
比如页面上放了三个计数按钮,都是同一个
ButtonCounter组件。点第一个,只应该第一个加 1,另外两个不动。组件定义只有一份,实例却可以有很多个。如果
data写成一个对象,所有实例拿到的都是这同一个对象,一个实例改了count,其他实例读到的也变了。写成函数后,Vue 每创建一个实例就调用一次这个函数,每次都返回一个新对象,实例之间就互不影响。Vue 2 文档的原话是:组件的data必须是函数,这样每个实例才能维护一份独立的数据副本。只创建一次的根实例没有共享问题,所以 Vue 2 里
new Vue({ data: { ... } })写对象也行。Vue 3 统一了写法,data不再接受普通对象,只接受返回对象的函数。要注意,如果函数返回的是外面定义好的同一个对象,比如
data() { return shared },实例之间照样共享。函数的作用在于每次新建对象,不在于写成函数这个形式。
先看共享对象会出什么问题
下面不用 Vue,只用普通 JavaScript 模拟“一个组件定义创建多个实例”。createInstance 每次创建实例时,按 data 的写法取出数据。
function createInstance(options) {
const data = typeof options.data === 'function' ? options.data() : options.data;
return { data };
}
const objectOptions = { data: { count: 0 } };
const a = createInstance(objectOptions);
const b = createInstance(objectOptions);
a.data.count += 1;
console.log(b.data.count); // 1
const functionOptions = {
data() {
return { count: 0 };
},
};
const c = createInstance(functionOptions);
const d = createInstance(functionOptions);
c.data.count += 1;
console.log(d.data.count); // 0
a 和 b 是两个实例,可只改了 a,b 也变成了 1。因为组件选项只定义了一次,objectOptions.data 就是一个对象,两个实例拿到的都是它的引用。
换成函数以后,每个实例创建时都调用一次 data(),函数体里的对象字面量每次都会生成新对象,所以 c 改了,d 还是 0。
为什么根实例可以写对象
问题的根源是“一份定义,多个实例”。Vue 2 的根实例用 new Vue({ el: '#app', data: { ... } }) 创建,这个选项对象只用一次,没有别的实例跟它抢数据,所以 Vue 2 文档里的根实例例子都直接写对象。
组件不一样。组件定义注册一次以后,模板里写几次就创建几个实例,所以 Vue 2 规定组件的 data 必须是函数。
Vue 3 有什么变化
Vue 3 的迁移指南写明,data 选项不再接受普通对象,只接受函数声明,根实例也一样:
createApp({
data() {
return { count: 0 };
},
}).mount('#app');
迁移指南还提到另一个变化:组件的 data 和 mixin、extends 里的 data 合并时,Vue 3 只合并第一层属性。比如 mixin 返回 user: { name, id },组件返回 user: { id: 2 },Vue 2 会把 user 里的字段深度合并,保留 name;Vue 3 直接用组件的 user 整个替换,结果只剩 { id: 2 }。
组合式 API 里不写 data,状态在 setup 里用 ref、reactive 创建。setup 本身每个实例执行一次,所以天然是每个实例一份,见《Vue 3 中 ref 和 reactive 有什么区别?》。
常见追问
data 能写成箭头函数吗?
能,但箭头函数里的
this不是组件实例。Vue 文档给的办法是用第一个参数拿实例,比如data: (vm) => ({ a: vm.myProp })。
data 里以 _ 或 $ 开头的属性为什么用 this 读不到?
Vue 文档说明,以
_或$开头的属性不会代理到组件实例上,因为可能和 Vue 内部属性、API 方法冲突,要用this.$data._property读取。