在一个非单文件组件中(一个文件中包含n个组件,最常见的就是单个html文件中存在多个组件),如果我们需要在这个文件中创建n个组件,然后再页面上展示,这时候我们就需要先定义组件,然后注册组件,最后使用组件。在定义组件这一步,我们就需要使用到 extend 这个方法。当然,也可以在一个html文件中使用多个 new Vue () 来注册组件,但是这么做有问题,下面再说。
Vue.extend(option)
官方文档解释:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
组件选项:templete模板(el属性只能在 new Vue() 时使用)、data数据、methods方法、computed计算属性等等正常组件拥有的。
我的理解:首先 extend 是 Vue 自带的一个全局方法,该方法接收一个对象作为参数,在实例化的时候,通过传递的参数将一个空的 Vue实例 进行扩展,并以此来创造出一个 Vue 子类,也就是我们说的 Vue 组件。
使用方法:
1、非单文件组件内部所有组件全部使用 Vue.extend() 方式注册(未指定根组件,无法渲染)
2、非单文件组件中使用 new Vue() 注册根组件,其余子组件则使用 Vue.extend() 方式注册。
3、全部使用 new Vue() 注册组件(若是存在组件嵌套,则子组件内部双向绑定的的数据失效)
1、全部使用 Vue.extend() 方式注册组件
const vm2 = Vue.extend({
template:
`<div id='root2'>
{{msg}}
</div>`,
data() {
return {
msg: 'root2',
count: 2,
pets: 'dog'
}
}
})
const vm1 = Vue.extend({
template:
`<div id='root1'>
{{msg}}
<!-- 使用 root2 组件 -->
<vm2 />
</div>`,
data() {
return {
msg: 'root1',
count: 1,
pets: 'dog'
}
},
components: {
// 注册子组件
vm2
}
})
无法展示,页面也不会报错,因为根本没有指定根组件进行渲染
2、使用第二种方式分别注册子组件和根组件
注册子组件const vm1 = Vue.extend({
template:
`<div id='root2'>
{{msg}}
<input v-model="count" />
</div>
`,
data() {
return {
msg: 'root2',
count: 2,
pets: 'dog'
}
}
})
注册根组件
// 注册根组件
const vm = new Vue({
el: '#root1',
data() {
return {
msg: 'root1',
count: 1,
pets: 'dog'
}
},
components: {
// 注册子组件
vm1
}
})
根组件以及子组件使用
<div id='root1'>
{{msg}}
<input v-model="count" />
<!-- 使用 root2 组件 -->
<vm1 />
</div>
页面展示效果正常,且双向绑定数据正常
3、全部使用 new Vue 定义组件。其实在正常的开发中,这个方法用的极少,因为我们的开发一般都是单文件组件,在工程中每个组件都是通过 new Vue() 创建的,直接挂载到 根组件 app上。但是在单文件组件中,我们一般不使用多个 new Vue() 来创建组件,这是因为在使用 new Vue() 时,必须要传入一个 el 属性,这样会导致html页面上存在多个根节点,如果你的根节点嵌套了,那嵌套的根节点中绑定的数据会失效,不会展示。例如:
<div id='root1'>
{{msg}}
<input v-model="count" />
<select name="pets" id="pet-select" v-model="pets">
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
</select>
<div id='root2'>
{{msg}}
<input v-model="count" />
<select name="pets" id="pet-select" v-model="pets">
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
</select>
</div>
</div>
在这个 html 文件中,存在两个根节点 ,root1、root2,两个跟节点内部的子节点完全一样,绑定的数据也完全一样,但是 root1根节点包裹住了 root2根节点。
const vm = new Vue({
el: '#root1',
data() {
return {
msg: 'root1',
count: 1,
pets: 'dog'
}
},
})
const vm1 = new Vue({
el: '#root2',
data() {
return {
msg: 'root2',
count: 2,
pets: 'dog'
}
}
})
按照原本的想法,两个节点展示的数据应该完全一样,但是在页面上的效果是这样的。
可以看到,只有外部的root1根节点展示了对的数据, root2的根节点数据要么为空不展示,要么展示的是错误数据。
如果我们使用 Vue.extend() 来注册子组件又会是什么情况呢?
首先,注册root2组件,其实就是将root2的所有节点放在了 templete 属性内部,用字符串模板包裹
const vm1 = Vue.extend({
template:
`<div id='root2'>
{{msg}}
<input v-model="count" />
<select name="pets" id="pet-select" v-model="pets">
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
</select>
</div>`,
data() {
return {
msg: 'root2',
count: 2,
pets: 'dog'
}
}
})
2、在父组件中注册 root2 组件
const vm = new Vue({
el: '#root1',
data() {
return {
msg: 'root1',
count: 1,
pets: 'dog'
}
},
components: {
vm1
}
})
3、使用 root2 组件
<div id='root1'>
{{msg}}
<input v-model="count" />
<select name="pets" id="pet-select" v-model="pets">
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
</select>
<!-- 使用 root2 组件 -->
<vm1 />
</div>
4、页面效果
结论:如果是在非单组件文件(或者是html页面),最好是只用 一个new Vue()注册一个根组件,其余子组件则是用 Vue.extend() 注册。否则如果使用 new Vue() 注册所有组件的话,若是存在组件包裹的情况,则被包裹的组件内部双向数据绑定会失效。
VueComponent在组件定义之后,我们其实还没有去理解这个过程和内部操作,下面我们就来剖析一下,看看在 Vue.extend() 之后,发生了什么。
首先,我们来看看 Vue.extend() 之后,返回的是一个什么东西。
样例代码就不贴了,就是上面的 vm1 实例。打印 vm1 之后,看看是个啥
打印之后发现,这玩意是个函数,而且还是个构造函数。在这个函数里面啥操作也没做,只不过调用了 _init() 方法。
Vue.extend = function (extendOptions) {
/*****其余操作***/
var Sub = function VueComponent(options) {
console.log("VueComponent被调用了");
this._init(options);
};
/*****其余操作***/
return Sub;
};
}
所以说,
1、组件的本质就是一个 【VueComponent 的构造函数】,且这个函数是 Vue.extend() 生成的。
2、在使用组件时,我们只需要写上组件标签,Vue 会自动帮我们生成 组件的实例对象( 因为组件的本质就是一个构造函数,构造函数被调用之后,当然会产生实例对象),即 Vue 帮我们执行的 new VueCopmonennt(options)
3、特别注意,每次调用 Vue.extend(),返回的都是一个新的组件,因为是通过函数返回的。这个地方我们看看上面的源码就能知道,因为 每次调用之后返回的 Sub 都是不一样的。
4、关于this指向,
a、通过 Vue.extend() 配置的组件,在data,methods,component,watch等可能用到 this 的地方,this 指向的是 【VueComponent 的实例对象】
b、通过new Vue() 配置的组件,在data,methods,component,watch等可能用到 this 的地方,this 指向的是 【Vue 的实例对象】
验证一下:
分别在上面的实例 vm,vm1上配置 show 方法,在方法内部打印当前this
点击按钮之后,查看 this 指向
展开的地方太大了,就不展开了,但是在控制台上对比发现,除了 一个是 Vue {} 一个是 VueComponent {} 之外,内部的所有属性全部一致,包括数据劫持,数据代理,底层方法等等。
组件管理之前说的Vue.extend(option) 这个模块时,说到了非单文件组件内部,最好是使用Vue.extend(option) 来定义子组件,然后使用 new Vue(option) 来注册根组件,从而使得 根组件好方便管理子组件,那么从那里能看出来管理状态呢?
看看上面的this 指向问题,展开之后,发现 一个 $children属性,这是一个数组
在 new Vue() 配置的组件中,发现存在一个 VueComponent {} 实例对象,这个对象指向的就是 vm1实例对象
而在 Vue.extend() 配置的组件中,发现这是一个空数组,这就是因为, 根组件调用了 vm1子组件,而 vm1子组件,内部是没有调用别的子组件的。
这就是 Vue 的组件管理模式
总结如何使用 Vue.extend() 定义一个组件:
1、Vue.extend(option) 和 new Vue(option) 创建组件时所传入的 option 配置项几乎一样.
区别在于:
(a)、el不能指定:所有的组件最终只会由一个vm管理,由这个vm中的 el 指定挂载容器
(b)、data必须写成函数:避免组件复用时,数据存在引用关系。
Vue.extend() 定义组件的本质:
本质上是 调用了 VueComponent () 这个构造函数,去返回了一个 【VueComponent 实例对象】,且每次在Vue.extend() 调用时,返回的组件实例对象都不一样
非单文件组件中定义根组件和子组件
原则上,默认一个非单文件组件中 只存在一个 new Vue() 定义的根组件,可以有无数个 Vue.extend() 定义的子组件,这是因为,如果所有组件都用 new Vue() 定义,那么如果存在组件包裹的情况,子组件内部双向绑定的数据不会生效。如果都用 Vue.extend() 定义组件,那么则没有指定根组件,无法渲染。
this指向问题使用 new Vue() 定义的组件,在组件内部能用到 this 的地方,this指向为【Vue实例对象】
使用 Vue.extend() 定义的组件,~~~~~~~~~~~~~this指向为【VueComponent实例对象】
官网补充
这里说明了,使用 VueComponent 和 new Vue 的异同。但是其实还有一点:在 new Vue 中,传递的 data 属性,可以是对象,也可以是函数( 当然,我们还是推荐函数写法 ),但是在VueComponent 中传递的 data 属性,则只能是函数,因为 new Vue 注册的是 根组件,不存在复用情况,data中的属性不存在引用关系,不会导致数据错乱,但是VueComponent 则不同