1.问题:Vue组件data为什么必须是个函数而Vue的根实例则没有此限制?
<div id="demo">
<h1>test</h1>
<comp></comp>
<comp></comp>
</div>
Vue.component('comp', {
template:'<div @click="counter++">{{counter}}</div>',
data: {counter: 0}
})
const app = new Vue({
el: '#demo',
});
2. 整体思路
由于局部组件存的动态创建,过程比较复杂,这里使用全局组件来对比。
- vue初始化,先调用initAssetRegisters方法,把所有全局组件的构造函数先初始化好,并且全局组件合并到vue的配置信息上。
- 这过程中会触发mergeOptions的strats.data 方法判断如果是组件时候,判断data是否为function 如果不是则警告提示。
- 根Vue或者组件实例化时,都会统一调用initData(),初始化data。
3. 分析源码
- 从源码中分析 src/core/global-api/assets.js vue初始化的时候调用了initAssetRegisters方法
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
- src/core/global-api/extend.js 定义了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
...
}
}
- src/core/util/options.js mergeOptions方法 进行合并
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)
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
}
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)
}
- 最终,无论是根vue实例化,还是组件实例化,都会统一走initData初始化。先实例化根vue,再实例化组件。
src\core\instance\state.js initState -> initData()
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true )
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
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
)
...
}
export function getData (data: Function, vm: Component): any {
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
4.总结:
- 在根Vue实例或者全局组件实例化时,会先把全局组件的构造函数初始化一次,然后通过initData统一实例化。
- 由于全局组件共用一个构造函数,所以在initData 初始化的时候,如果使用对象定义data,所有组件实例都会共用一个data对象,产生数据污染。而采用函数的方式通过工厂函数统一生成新data对象,有效规避的实例间共用污染问题。
- vue根实例全局只有一个,所以不会给其他实例修改,