本文仅针对 vue 2.6.x。
解析
首先,我们构造一个 component data 为 object 的 demo:
<div id="demo">
<h1>vue component data</h1>
<comp></comp>
</div>
<script>
Vue.component("comp", {
template: "<div>I am comp</div>",
data: { count: 1 },
});
const app = new Vue({
el: "#demo",
});
</script>
接着打开控制台看看信息:

这里可以用一个笨办法:直接找到报错的地方。
strats.data = function (parentVal: any, childVal: any, vm?: Component): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' &&
warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
显然,当没有传递 vm 实例的时候,就会检测 childVal 的类型是否为 function,如果不是 function 就会发出警告。
而想找到是谁调用了 strats.data,我们可以借助调试器:

在这里可以清晰的看到调用栈。
在上面的调用栈中,我们看到是 Vue.extend 触发了这个流程,而这显然是组件触发的,我们来到 Vue.extend 看看:
**Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}**
可以看到,上面的代码中,调用 mergeOptions 时只传递选项,并没有传递 vm 实例。我们顺着这条线看下去:
export function mergeOptions(parent: Object, child: Object, vm?: Component): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
可以看到,这里 mergeField 的第三个参数 vm 由于前面没有传递,所以肯定是 undefined,最终导致在 strats.data 会走 if 流程,发出警告。
分析到这里,我们已经知道为什么 vue 组件的 data 类型不为 function 的时候会发出警告了。
但是为什么要这样设计呢?
要搞清楚这个问题,我们需要来到初始化数据的位置,即 initData:
function initData(vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' &&
warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(`The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
可以看到这里对 data 的处理:如果 data 是函数,则直接执行,并且将其结果返回作为 data 选项的值,否则直接返回 data。
也就是说,vue 认为,直接返回 object 类型的 data,会导致问题。
会导致什么样的问题呢?
我们知道,vue 在初始化流程中,组件注册只会有一次,而在实际使用过程中,组件则是多例子的,如下 demo:
<div id="demo">
<h1>vue component data</h1>
<comp></comp>
<comp></comp>
<comp></comp>
</div>
<script>
Vue.component("comp", {
template: "<div @click='count++'>{{ count }}</div>",
data: {
count: 1,
},
});
const app = new Vue({
el: "#demo",
});
</script>
如果这里获取到的都是相同的 data,那么页面中的 3 个 comp 都将共享同一个 data 数据,这将造成数据污染,显然不是作为开发人员希望看到的。
而根组件本身就是单例的,所以不会出现数据污染的情况,所以即使直接返回 data 也是可以接受的。