关于Vue-extend和VueComponent问题小结

在一个非单文件组件中(一个文件中包含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 则不同

推荐阅读