是什么
需求:现在我们需要在一个组件中定义一个name
变量,并且把这个变量显示在界面上,代码:
<template>
<div>
{{ name }}
</div>
</template>
<script>
export default {
data() {
return {
name: 'syz'
}
}
}
</script>
那么问题来了,为什么组件中的data
需要是一个函数呢,可以将data
声明为一个对象吗,这样不是更直接吗?代码:
<template>
<div>
{{ name }}
</div>
</template>
<script>
export default {
data: {
name: 'syz'
}
}
</script>
如果是上面这种写法,不出意外的话,在vs code data处将会出现红线报错 data property in component must be a function
,提示我们data必须是一个function。
为什么
好,重点来了,为什么呢?原因就是:通过函数返回数据对象,保证了每个组件实例都有一个唯一的数据副本,避免了组件间数据互相影响。
先上结论:
每个子组件注册后会被处理成一个VueComponent
实例,类似下面的MyVueComponent(...)
, data
属性会挂在他的原型链上面
当子组件中的data
是一个Object
时,代码:
function MyVueComponent(){
}
MyVueComponent.prototype.data = {
name:'jack',
age:22,
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,55
可以看到,当componentA.data.age
赋值为55时,componentB.data.age
也被更改为55了
当子组件的data
是一个function
时,代码:
function MyVueComponent(){
this.data = this.data()
}
MyVueComponent.prototype.data = function() {
return {
age:22
}
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,22
可以看到,componentA.data.age
更改后,并不会影响到componentB.data.age
的值,也就是我们上面说的,避免了组件间数据互相影响
注意: 避免组件中数据相互影响,除了将data
定义为function
,还需要在data
中返回一个全新的Object(拥有独立的内存地址),我们知道,js中对象的赋值是引用传递,代码:
const myData = {
age:22
}
function MyVueComponent(){
this.data = this.data()
}
MyVueComponent.prototype.data = function() {
return myData
}
const componentA = new MyVueComponent();
const componentB = new MyVueComponent();
componentA.data.age=55;
console.log(componentA.data.age,componentB.data.age) // 55,55
可以看到,虽然我们将data
定义成function
了,但是在data
中return
了对象myData
,这里是引用传递,后续在修改data
中的值时,其实修改的是同一个内存地址的数据,所以还是存在数据相互影响了。
再看细节:
当我们需要在一个父组件中使用子组件时,以全局子组件为例子,代码:
// app.vue
<template>
<div id="app">
{{name}}
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
name: 'haha'
}
}
}
</script>
// main.js
import Vue from 'vue/dist/vue.esm.js'
import App from './App.vue'
Vue.component('app', App)
new Vue({
el: '#app',
template: '<app></app>'
})
首先,执行Vue.component(...)注册子组件
调用Vue.component()
这个方法,看一下Vue.component()
的内部实现:
var ASSET_TYPES = [
'component',
'directive',
'filter'
];
function initAssetRegisters (Vue) {
ASSET_TYPES.forEach(function (type) {
Vue[type] = function (
id,
definition
) {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
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);
}
// ...略去部分无关代码
this.options[type + 's'][id] = definition;
return definition
}
};
});
}
Vue初始化时会执行initAssetRegisters(...)
这个方法,那么Vue.component(...)
就出来了。
definition
参数就是我们在上面例子中调用Vue.component(...)
传进来的第二个参数,也就是:
{
name: 'App',
data() {
return {
name: 'haha'
}
}
}
接着会执行definition = this.options._base.extend(definition)
,会调用Vue.extend(...)
将外部传进来的对象经过各种处理,最后构造出一个VueComponent
的实例对象,当然,我们传进来的data
对象也挂在里面,在浏览器debugger看下过程数据会比较清楚。代码:
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name);
}
var 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;
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
if (name) {
Sub.options.components[name] = Sub;
}
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
cachedCtors[SuperId] = Sub;
return Sub
};
接着,执行new Vue(...)
代码:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
执行顺序:this._init(...)
-> initState(vm)
,也就是在initState(vm)
这个方法中会去初始化组件中data
数据,看下代码:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
}
看下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
)
}
// ...略去部分无关代码
}
可以看到,在第二行代码,会先判断data
是不是一个function
?
如果是function
,接着调用getData(...)
方法获取数据。
如果不是function
,就直接获取组件中的data
数据。
那什么情况下data
不是一个function
呢?
当我们使用new Vue(...)
实例的组件(一般是根节点)是不会被复用的。
因此也就不存在组件中数据相互影响的问题了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。