动态创建组件依靠
Vue.extend
案例分析
用户通过点击新增按钮,新加下一个页签,要实现这个需求只能通过动态的创建组件,然后添加到页面中,下面来实现这个需求(页签内容部分就不做了,重点放在动态创建页签按钮)
案例实现
<div id="app" class="app">
<div class="title">XXX页面</div>
<div id="tabBox" class="tabBox"></div>
<div class="add" @click="add">+</div>
</div>
<template id="tab">
<div class="tab">{{tabname}}</div>
</template>
<script src="./lib/vue.js"></script>
<script>
const tab = {
template: '#tab',
props: ['tabname']
}
const vm = new Vue({
data: {
tabName: '',
base: '页签',
num: 1
},
methods: {
add() {
this.tabName = this.base + this.num
this.num++
const tabCmp = new (Vue.extend(tab))({propsData: {tabname: this.tabName}}).$mount()
document.getElementById('tabBox').appendChild(tabCmp.$el)
}
}
}).$mount('#app')
</script>
代码分析
-
创建一个vue页面,页面内包含3个
div
,
title
是页面的标题,
tabBox
用来放所有的页签,
add
监听用户的点击添加页签; -
局部注册一个组件
tab
,传入了
template
和
props
两项属性; -
在父组件的
methods
中添加add事件处理函数,重点就在这个事件处理函数中,首先生成
tabName
,然后利用
Vue.extend
一番操作得到组件
tabCmp
,最后将创建的组件
tabCmp
插入到
tabBox
这个元素中; -
当目前为止,当用户每次点击
add
按钮,便会在
tabBox
中插入一个
tab
组件,页面上也会做更新; -
在这一连串的操作中核心就是
Vue.extend
,下面具体认识以下这个API。
关于Vue.extend
先看Vue官网对于这个API的解释:
使用基础
Vue
构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data
选项是特例,需要注意 – 在
Vue.extend()
中它必须是函数
-
通俗的讲
Vue.extend
就是一个构造函数,它可以生成一个Vue实例(子类),可以像使用Vue构造函数一样使用他。特别注意的是他里面的data一定要是函数,这一点很好理解隔离作用域; - 将前面关键代码拆分一下理解,如下:
const tabCmp = new Vue.extend({
template: "#tab",
props: ['tabname']
}).$mount()
-
这样看就他跟
Vue
构造函数没区别,
$mount()
负责挂载,不是这里的重点,不赘述; -
但这还不够,这还不能完成上面的动态创建组件,
props
的值怎么传到
tabCmp
组件中,这里可以看一下Vue源码中关于
Vue. extend
的实现
Vue.extend的源码实现
下面代码片段是我从源码中摘取出来的
// 取自vue-js/src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
const Super = this
// ...
const Sub = function VueComponent (options) {
this._init(options)
}
// ...
Sub['super'] = Super
// ...
return Sub
}
// 取自vue-js/src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// ...
if (options && options._isComponent) {
// ...
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
}
-
跟着源码走一遍
new Vue.extend()
得到一个
Sub
, 调用
Sub
会执行的
_init
方法,
_init
方法接受一个options对象,在这里只传入
propsData
就相当于在标签中绑定了一个props值,继续往下代码会执行
mergeOptions()
,在这个函数中将父组件中的
options
和刚传入的子组件的
options
做合并并返回,然后赋值给父组件的
options
,至此就完成了在父组件中给动态组件传值的过程; -
回到案例中动态创建的组件
new Vue.extend(tab)
得到实例
Sub
,调用它并传入参数
(new Vue.extend(tab))({propsData: {tabname: this.tabName}})
,最后调用
$mount
挂载组件,至此就完成了组件的动态创建和挂载; -
回顾一下
Vue.extend
其实就是Vue暴露了一个接口允许我们在一个父Vue实例下动态的去创建另外一个子Vue实例并挂载到父Vue上。
动态创建的组件怎么监听自定义事件
<div id="app" class="app">
<div>XXX页面</div>
<div id="tabBox" class="tabBox"></div>
<div class="add" @click="add" ref="add">+</div>
</div>
<template id="tab">
<div class="tab" @click="changeColor">{{tabname}}</div>
</template>
<script src="./lib/vue.js"></script>
<script>
const tab = {
template: '#tab',
props: ['tabname'],
methods: {
changeColor() {
this.$emit('change-color')
}
}
}
const vm = new Vue({
data: {
tabName: '',
base: '页签',
num: 1
},
methods: {
add() {
this.tabName = this.base + this.num
this.num++
const tabCmp = new (Vue.extend(tab))({propsData: {tabname: this.tabName}}).$mount()
// 直接这样监听自定义事件
tabCmp.$on('change-color', () => {
let refAdd = this.$refs.add
refAdd.style.backgroundColor = 'red'
})
document.getElementById('tabBox').appendChild(tabCmp.$el)
}
}
}).$mount('#app')
</script>
-
在之前案例的基础上给tab页签增加了一个点击事件
changeColor
; -
在
methods
中添加函数并发射
change-color
事件; -
在动态创建组件的位置使用
tabCmp.$on
监听自定义事件; -
最后得到的效果是当用户点击
+
增加新的tab页签,用户点击页签,添加页签按钮背景变为红色。
封装动态创建组件的函数
function Create(components, propsData, parentNode) {
this.cmp = null
this.components = components
this.propsData = propsData
this._init()
this._insert(parentNode)
}
Create.prototype._init = function() {
this.cmp = new (Vue.extend(this.components))({propsData: this.propsData}).$mount()
}
Create.prototype._insert = function(parentNode) {
parentNode.appendChild(this.cmp.$el)
}
Create.prototype.on = function(eventName, callback) {
this.cmp.$on(eventName, callback)
}
使用封装的方法动态创建组件
const parent = document.getElementById('tabBox')
let create = new Create(tab, {tabname: this.tabName}, parent)
create.on('change-color', () => {
let refAdd = this.$refs.add
refAdd.style.backgroundColor = 'red'
})