组件进阶
1、组件的生命周期与高级配置
组件在被创建出来到渲染完成会经历一系列过程。同样,组件的销毁也会经历一系列过程,组件从创建到销毁的这一系列过程被称为组件的生命周期。在Vue中,组件生命周期的节点会被定义为一系列的方法,这些方法被称为生命周期钩子。有了这些生命周期方法,我们可以在合适的时机来完成合适的工作。例如,在组件挂载前准备组件所需要的数据,当组件销毁时清除某些残留数据等。
Vue中提供了许多对组件进行配置的高级API接口,包括对应用或组件进行全局配置的API功能接口以及组件内部相关的高级配置项。
1.1、生命周期方法
首先,我们可以通过一个简单的示例来直观地感受一下组件生命周期方法的调用时机。
html
<div id="app">
<sub-com v-if="show">
{{content}}
</sub-com>
<button @click="changeShow">测试</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref} = Vue;
const sub = {
setup() {
console.log("组件创建前");
Vue.onBeforeMount(()=>{
console.log("组件即将挂载前");
})
Vue.onMounted(()=>{
console.log("组件挂载完成");
})
Vue.onBeforeUpdate(()=>{
console.log("组件即将更新前");
})
Vue.onUpdated(()=>{
console.log("组件更新完成");
})
Vue.onActivated(()=>{
console.log("被缓存的组件激活时调用");
})
Vue.onDeactivated(()=>{
console.log("被缓存的组件停用时调用");
})
Vue.onBeforeUnmount(()=>{
console.log("组件即将别卸载前调用");
})
Vue.onUnmounted(()=>{
console.log("组件被卸载后调用");
})
Vue.onErrorCaptured((error, instance, info)=>{
console.log("捕获到来自组件的异常时调用");
})
Vue.onRenderTracked((event)=>{
console.log("虚拟DOM重新渲染时调用", event);
})
Vue.onRenderTriggered((event)=>{
console.log("虚拟DOM被触发渲染时调用");
})
console.log("组件创建完成");
},
template: `
<div>
<slot></slot>
</div>`
}
const App = createApp({
setup() {
const show = ref(true);
const content = ref(0);
const changeShow = ()=>{
show.value = !show.value;
}
return {
show,
content,
changeShow
}
}
})
App.component("sub-com", sub);
App.mount("#app");
</script>
如以上代码所示,每个方法中都使用log标明了所调用的时机。运行代码,控制台将输出如下信息:

点击按钮:

从控制台打印的信息可以看出,本次页面渲染过程中只执行了6个组件的生命周期方法,我们使用了一个自定义的组件,在页面渲染的过程中只执行了组件的创建和挂载过程,但是并没有执行卸载的过程。在代码中,我们使用v-if指令来控制子组件的渲染,当渲染状态切换时,组件会相应地进行挂载和卸载操作。你可以尝试单击页面中的按钮,当子组件显示和隐藏时,对应的挂载和卸载生命周期方法会被调用。
在上面列举的生命周期方法中,有4个方法经常使用,分别是renderTriggered、renderTracked、beforeUpdate和updated方法。当组件中的HTML元素发生渲染或更新时,会调用这些方法,例如:
html
<div id="app">
<sub-com v-if="show">
{{content}}
</sub-com>
<button @click="changeShow">测试</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref} = Vue;
const sub = {
setup() {
console.log("组件创建前");
Vue.onBeforeUpdate(()=>{
console.log("组件即将更新前");
})
Vue.onUpdated(()=>{
console.log("组件更新完成");
})
Vue.onRenderTracked((event)=>{
console.log("虚拟DOM重新渲染时调用", event);
})
Vue.onRenderTriggered((event)=>{
console.log("虚拟DOM被触发渲染时调用");
})
},
template: `
<div>
<slot></slot>
</div>`
}
const App = createApp({
setup() {
const show = ref(true);
const content = ref(0);
const changeShow = ()=>{
content.value += 1;
}
return {
show,
content,
changeShow
}
}
})
App.component("sub-com", sub);
App.mount("#app");
</script>
运行上述代码,当单击页面中的按钮时,页面显示的计数会自增,同时,控制台打印的信息如下:

1.2、应用的全局配置选项
当调用Vue框架中的createApp方法后,会创建一个Vue应用实例,对于此应用实例,其内部封装了一个config对象,我们可以通过这个对象的一些全局选项来对其进行配置。常用的配置项有异常与警告捕获配置和全局属性配置。
在Vue应用运行过程中,难免会有异常和警告产生,我们可以自定义函数来对抛出的异常和警告进行处理。示例代码如下:
js
const App = Vue.createApp({});
App.config.errorHandler = (drr, vm, info)=>{
//捕获运行中产生的异常
//err参数是错误对象,info为具体的错误信息
}
App.config.warnHandler = (msg, vm, trace)=>{
//捕获运行中产生的警告
//msg是警告信息,trace是组件的关系回溯
}
之前,我们在使用组件时,组件内部使用到的数据要么是组件内部自己定义的,要么是通过外部属性从父组件传递进来的。在实际开发中,有些数据可能是全局的,例如应用名称、应用版本信息等,为了方便地在任意组件中使用这些全局数据,可以通过globalProperties全局属性对象进行配置,例如:
js
const App = Vue.createApp({});
//配置全局数据
App.config.globalProperties = {
version: '1.0.0'
}
const sub = {
//在setup函数中获取当前组件实例
setup() {
const instance = Vue.getCurrentInstance()
Vue.onMounted(()=>{
//在任意组件的任意地方都可以通过组件实例直接访问全局数据
console.log(instance.appContext.config.globalProperties.version);
})
}
}
App.component("sub-com", sub);
App.mount("#app");
1.3、组件的注册方式
组件的注册方式分为全局注册与局部注册两种。直接使用应用实例的component方法注册的组件都是全局组件,也就是说,可以在应用内的任何地方使用这些组件,包括其他组件内部,例如:
html
<div id="app">
<comp1></comp1>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const comp1 = {
template: `
<div>
组件1
<comp2></comp2>
</div>`
}
const comp2 = {
template: `
<div>
组件2
</div>`
}
App.component('comp1', comp1);
App.component('comp2', comp2);
App.mount('#app');
</script>

如以上代码所示,在comp2组件中直接可以使用comp1组件,全局注册组件虽然使用起来很方便,但很多时候这并不是最佳的编程方式。一个复杂的组件内部可能由许多子组件组成,这些子组件本身是不需要暴露到父组件外面的,这时如果使用全局注册的方式注册组件,就会污染全局的JavaScript代码,更理想的方式是使用局部注册的方式注册组件,示例代码如下:
html
<div id="app">
<comp1></comp1>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const comp2 = {
template: `
<div>
组件2
</div>`
}
const comp1 = {
components: {
'comp2': comp2
},
template: `
<div>
组件1
<comp2></comp2>
</div>`
}
App.component('comp1', comp1);
App.mount('#app');
</script>

如以上代码所示,comp2组件只能够在comp1组件内部使用。另外,对于单文件组件,我们后续会介绍setup语法糖,如果使用了此语法糖,只要在setup语法糖中直接导入组件即可直接使用,无须注册,这些后续再详细介绍。
2、组件props属性的高级用法
使用props可以方便地向组件传递数据。从功能上讲,props也可以称为组件的外部属性,通过props的传参差异,组件可以有很强的灵活性和扩展性。
2.1、对props属性进行验证
JavaScript是一种非常灵活且自由的编程语言。在JavaScript中定义函数时,无需指定参数的类型。这种编程风格虽然为开发者提供了极大的便利,但同时也牺牲了一定的安全性。例如,在Vue组件中,如果一个自定义组件需要通过props接收外部传入的数值,但调用方错误地传递了一个字符串类型的数据,这可能导致组件内部出现错误。
为了增强类型安全性,Vue允许在定义组件的props时添加约束,对其类型、默认值、是否可选等进行配置。这有助于确保组件接收到正确类型的数据,并在数据不符合预期时提供明确的错误信息。
html
<div id="app">
<comp1 :count="5"></comp1>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref, computed} = Vue;
const App = Vue.createApp({});
const comp1 = {
props: ['count'],
setup(props) {
const thisCount = ref(0);
const click = ()=>{
thisCount.value += 1;
}
const innerCount = computed(()=>{
return props.count + thisCount.value;
})
return {
thisCount,
click,
innerCount
}
},
template: `
<button @click="click">单机</button>
<div>计数:{{innerCount}}</div>`
}
App.component('comp1', comp1);
App.mount('#app');
</script>

在上述代码中,定义了一个名为count的外部属性,这个属性在组件内实际上用于控制组件计数的初始值。需要注意,在外部传递数值类型的数据到组件内部时,必须使用v-bind指令的方式进行传递,直接使用HTML属性设置的方式会将传递的数据作为字符串传递(而不是JavaScript表达式)。例如,下面的组件的使用方式,最终页面渲染的计数结果将不是预期的:
html
<comp1 count="5"></comp1>
虽然count属性的本意是作为组件内部计数的初始值,但调用方不一定能完全理解组件内部的逻辑,调用此组件时极有可能会传递非数值类型的数据,例如:
html
<comp1 :count="{}"></comp1>
页面渲染效果如图所示。

可以看到,其渲染结果并不正常。在Vue中,我们可以对定义的props进行约束来显式地指定其类型。当将组件的props配置项配置为列表时,表示当前定义的属性没有任何约束控制,如果将其配置为对象,则可以进行更多约束设置。修改上面代码中props的定义如下:
js
props: {
//定义此属性的类型为数值类型
type: Number,
//设置此属性是否必传
required: false,
//设置默认值
default: 10
},
此时,在调用此组件时,如果设置count属性的值不符合要求,则控制台会有警告信息输出,例如count设置的值不是数值类型,则会抛出如下警告:

在实际开发中,我们建议所有的props都采用对象的方式定义,显式地设置其类型、默认值等,这样不仅可以使组件调用时更加安全,也侧面为开发者提供了组件的参数使用文档。
如果只需要指定属性的类型,而不需要指定更加复杂的性质,可以使用如下方式定义:
js
props: {
//数值类型
count: Number,
//字符串类型
count2: String,
//布尔类型
count3: Boolean,
//数组类型
count4: Array,
//对象类型
count5: Object,
//函数类型
count6: Function
}
如果一个属性可能是多种类型,可以如下定义:
js
props:{
// 指定属性类型为字符串或数值
param:[String, Number]
}
在对属性的默认值进行配置时,如果默认值的获取方式比较复杂,也可以将其定义为函数,函数执行的结果会被作为当前属性的默认值,示例代码如下:
js
props: {
count: {
default: function() {
return 10
}
}
}
Vue中props的定义支持进行自定义验证。以上述代码为例,假设组件内需要接收的count属性的值必须大于数值10,则可以通过自定义验证函数实现:
js
props: {
count: {
validator: (value)=>{
if(typeof(value) != 'number' || value <= 10) {
return false
}
return true
}
}
},
当组件的count属性被赋值时,会自动调用验证函数进行验证,如果验证函数返回true,则表明此赋值是有效的,如果验证函数返回false,则控制台会输出异常信息。
2.2、props的只读性质
你可能已经发现了,对于组件内部来说,props是只读的。也就是说,我们不能在组件的内部修改props属性的值,可以尝试运行如下代码:
html
<div id="app">
<comp1 :count="5"></comp1>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref, computed} = Vue;
const App = Vue.createApp({});
const comp1 = {
props: {
count: {
validator: function(value) {
if(typeof(value) != 'number' || value <= 10) {
return false;
}
return true;
}
}
},
setup(props) {
const {count} = props;
const click = ()=>{
count += 1;
}
return {click}
},
template: `
<button @click="click">单机</button>
<div>计数:{{count}}</div>`
}
App.component('comp1', comp1);
App.mount('#app');
</script>
当click函数被触发时,页面上的计数并没有改变,并且控制台会抛出Vue警告信息。
props的这种只读性是Vue单向数据流特性的一种体现。对于所有的外部属性,props都只允许父组件的数据流动到子组件中,子组件的数据则不允许流向父组件。因此,在组件内部修改props的值是无效的。以计数器页面为例,如果我们定义props只是为了设置组件某些属性的初始值,完全可以使用计算属性来进行桥接,也可以将外部属性的初始值映射到组件的内部属性上,示例代码如下:
js
setup(props) {
const {count} = props;
const thisCount = ref(count);
const click = ()=>{
thisCount.value += 1;
}
const innerCount = computed(()=>{
count.value + thisCount.value
})
return {
innerCount,
thisCount,
click
}
}
2.3、组件数据注入
数据注入是一种便捷的组件间的数据传递方式。一般情况下,当父组件需要传递数据到子组件时,我们会使用props,但是当组件的嵌套层级很多,子组件需要使用到多层之外的父组件的数据时,就非常麻烦了,数据需要一层一层地进行传递。
html
<div id="app">
<my-list :count="5"></my-list>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref} = Vue;
const App = Vue.createApp({});
const listCom = {
props: {
count: Number
},
template: `
<div style="border:red solid 10px;">
<my-item v-for="i in this.count" :list-count="this.count" :index="i"></my-item>
</div>`
}
const itemCom = {
props: {
listCount: Number,//列表项总数
index: Number//当前列表项下标
},
template: `
<div style="border:blue solid 10px;">
<my-label :list-count="this.listCount" :index="this.index"></my-label>
</div>`
}
const labelCom = {
props: {
listCount: Number,
index: Number
},
template: `
<div>{{index}}/{{this.listCount}}</div>`
}
App.component("my-list", listCom);
App.component("my-item", itemCom);
App.component("my-label", labelCom);
App.mount('#app');
</script>
在上述代码中,我们创建了3个自定义组件,my-list组件用来创建一个列表视图,其中每一行的元素为my-item组件,在my-item组件中又使用了my-label组件进行文本显示。列表中的每一行会渲染出当前的行数以及总行数。运行上述代码,页面效果如图所示。

运行上述代码本身没有什么问题,烦琐的地方在于my-label组件中需要使用到my-list组件中的count属性,通过使用my-item组件,数据才能顺利进行传递。随着组件嵌套层数的增多,数据的传递将越来越复杂。对于这种场景,我们可以使用数据注入的方式来跨层级进行数据传递。
所谓数据注入,是指父组件可以向其所有子组件提供数据,不论在层级结构上此子组件的层级有多深。以上面的代码为例,my-label组件可以跳过my-item组件,直接使用my-list组件中提供的数据。
实现数据注入,需要使用provide与inject两个函数,提供数据的父组件需要设置provide配置项来提供数据,子组件需要设置inject配置项来获取数据。修改上述代码如下:
html
<div id="app">
<my-list :count="5"></my-list>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref, provide, inject} = Vue;
const App = Vue.createApp({});
const listCom = {
props: {
count: Number
},
setup(props) {
//这里需要提供组件内共享的数据
provide("listCount", props.count)
},
template: `
<div style="border:red solid 10px;">
<my-item v-for="i in this.count" :index="i"></my-item>
</div>`
}
const itemCom = {
props: {
index: Number
},
template: `
<div style="border:blue solid 10px;">
<my-label :index="this.index"></my-label>
</div>`
}
const labelCom = {
props: {
index: Number
},
setup() {
//此组件需要使用provide提供的数据,使用inject进行注入
const listCount = inject("listCount");
return {listCount}
},
template: `
<div>{{index}}/{{this.listCount}}</div>`
}
App.component("my-list", listCom);
App.component("my-item", itemCom);
App.component("my-label", labelCom);
App.mount('#app');
</script>
运行代码,程序依然可以很好地运行。使用数据注入的方式传递数据时,父组件不需要了解哪些子组件要使用这些数据,同样子组件也无须关心所使用的数据来自哪里。一定程度来说,这使代码的可控性降低了。因此,在实际开发中,我们要根据场景来决定使用怎样的方式来传递数据,而不是滥用注入技术。
3、组件Mixin技术
3.1、使用Mixin来定义组件
当我们开发大型前端项目时,可能会定义非常多的组件,这些组件中可能有部分功能是通用的,对于这部分通用的功能,如果每个组件都编写一遍将会非常烦琐,而且不利于之后的维护。
html
<div id="app">
<my-com1 title="组件1"></my-com1>
<my-com2 title="组件2"></my-com2>
<my-com3 title="组件3"></my-com3>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const com1 = {
props: ['title'],
template: `
<div style="border:red solid 2px;">
{{title}}
</div>`
}
const com2 = {
props: ['title'],
template: `
<div style="border:blue solid 2px;">
{{title}}
</div>`
}
const com3 = {
props:['title'],
template: `
<div style="border:green solid 2px;">
{{title}}
</div>`
}
App.component("my-com1", com1);
App.component("my-com2", com2);
App.component("my-com3", com3);
App.mount("#app");
</script>

在上述代码中定义的3个示例组件中,每个组件都定义了一个名为title的外部属性,这部分代码其实可以抽离出来作为独立的"功能模块",需要此功能的组件只需要"混入"此功能模块即可。示例代码如下:
html
<div id="app">
<my-com1 title="组件1"></my-com1>
<my-com2 title="组件2"></my-com2>
<my-com3 title="组件3"></my-com3>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
//将组件通用的部分定义成mixin模块
const myMixin = {
props: ['title']
}
const com1 = {
//引入需要的mixin模块
mixins: [myMixin],
template: `
<div style="border:red solid 2px;">
{{title}}
</div>`
}
const com2 = {
mixins: [myMixin],
template: `
<div style="border:blue solid 2px;">
{{title}}
</div>`
}
const com3 = {
props:['title'],
template: `
<div style="border:green solid 2px;">
{{title}}
</div>`
}
App.component("my-com1", com1);
App.component("my-com2", com2);
App.component("my-com3", com3);
App.mount("#app");
</script>
如以上代码所示,我们可以定义一个混入对象,混入对象中可以包含任意的组件定义选项,当此对象被混入组件时,组件会将混入对象中提供的选项引入当前组件内部。这类似于编程语言中的"继承"语法。
3.2、Mixin选项的合并
当混入对象与组件中定义了相同的选项时,Vue可以非常智能地对这些选项进行合并。不冲突的配置将完整合并,冲突的配置会以组件中自己的配置为准,例如:
html
<div id="app">
<com></com>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const myMixin = {
data() {
//定义mixin模块中的数据
return {
a: "a",
b: "b",
c: "c"
}
}
}
const com = {
mixins: [myMixin],
setup() {
const d = "d";
return {d}
},
//组件被创建后会调用,用来测试混入的数据情况
created() {
//a,b,c都存在
console.log(this.a, this.b, this.c, this.d);
},
template: `<div></div>`
}
App.component("com", com);
App.mount("#app");
</script>

在上述代码中,混入对象中定义了组件的属性数据,包含a、b和c三个属性,组件本身定义了d属性,最终组件在使用时,其内部的属性包含a、b、c和d四个属性。如果属性的定义有冲突,则会以组件内部定义的为准,例如:
html
<div id="app">
<com></com>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const myMixin = {
props: ['title'],
data() {
//定义mixin模块中的数据
return {
a: "a",
b: "b",
c: "c"
}
}
}
const com = {
mixins: [myMixin],
setup() {
return {
c: "C" //此数据与mixin模块中的有冲突,组件内的优先级更高
}
},
//组件被创建后会调用,用来测试混入的数据情况
created() {
//a,b,c都存在
console.log(this.c);
},
template: `<div></div>`
}
App.component("com", com);
App.mount("#app");
</script>

生命周期函数的这类配置项的混入与属性类的配置项的混入略有不同,不重名的生命周期函数会被完整混入组件,重名的生命周期函数被混入组件时,在函数触发时,会先触发Mixin对象中的实现,再触发组件内部的实现。这类似于面向对象编程中子类对父类方法的覆写。例如:
html
<div id="app">
<com></com>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const myMixin = {
mounted() {
console.log("Mixin对象mounted")
}
}
const com = {
mixins: [myMixin],
mounted() {
console.log("组件本身的mounted")
},
template: `<div></div>`
}
App.component("com", com);
App.mount("#app");
</script>

运行上述代码,当com组件被挂载时,控制台会先打印"Mixin对象mounted"之后,再打印"组件本身mounted"。
3.3、进行全局Mixin
Vue也支持对应用进行全局Mixin混入。直接对应用实例进行Mixin设置即可,示例代码如下:
js
const App = Vue.createApp({})
App.mixin({
mounted() {
console.log("Mixin对象mounted")
}
})
需要注意,虽然全局Mixin使用起来非常方便,但是这会使其后所有注册的组件都默认被混入这些选项,当程序出现问题时,这会增加排查问题的难度。全局Mixin技术非常适合开发插件,如开发组件挂载的记录工具等。
另外,在示例代码中,很多地方我们采用了选项式的API来编写组件,其实Mixin技术在Vue3之后已经不再被推荐使用了,Vue3之所以依然支持Mixin的语法,主要是为了兼容旧代码。在Vue3中,如果我们需要实现逻辑的复用,可以将其封装为函数,在组合式API中导入函数直接进行使用。
4、使用自定义指令
4.1、认识自定义指令
在Vue中,指令的使用无处不在,前面一直使用的v-bind、v-model、v-on等都是指令。Vue中也提供了自定义指令的能力,对于一些定制化的需求,配合自定义指令来封装组件,可以使开发过程变得非常容易。
4.1、认识自定义指令
Vue内置的指令已经提供了大部分核心的功能,但是有时仍需要直接操作DOM元素来实现业务功能,这时就可以使用自定义指令。我们可以先来看一个简单的示例。首先,新建一个名为html的文件,我们来实现如下功能:页面上提供一个input输入框,当页面被加载后,输入框默认处于焦点状态,即用户可以直接对输入框进行输入。示例代码如下:
html
<div id="app">
<input v-getfocus />
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
App.directive('getfocus', {
//当被绑定此指令的元素被挂载时调用
mounted(element) {
console.log("组件获得了焦点")
element.focus()
}
})
App.mount("#app");
</script>
如以上代码所示,调用应用实例的directive方法可以注册全局的自定义指令,上述代码中的getfocus是指令的名称,在使用时需要加上v-前缀。运行上述代码,可以看到,页面被加载时其中的输入框默认处于焦点状态,可以直接进行输入。
在自定义指令时,通常需要在组件的一些生命周期节点进行操作。自定义指令除支持mounted生命周期方法外,还支持使用beforeMount、beforeUpdate、updated、beforeUnmount和unmounted生命周期方法,我们可以选择合适的时机来实现自定义指令的逻辑。
上述示例代码中采用全局注册的方式来自定义指令,因此所有组件都可以使用,如果只想让自定义指令在指令的组件上可用,也可以在定义组件(局部注册)时,在组件内部进行directives配置来自定义指令,示例代码如下:
html
<div id="app">
<sub-com></sub-com>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
const sub = {
directives: {
//组件内部的自定义指令
getfocus: {
mouted(el) {
el.focus()
}
}
},
mounted() {
//组件挂载
console.log(this)
},
template: `
<div>
<input type='text'>
</div>`
}
App.component('sub-com', sub);
App.mount("#app");
</script>
4.2、自定义指令的参数
在4.1节中,我们演示了一个自定义指令的小例子,这个例子本身非常简单,没有为自定义指令进行赋值,也没有使用自定义指令的参数。我们知道,Vue内置的指令是可以设置值和参数的,例如v-on指令,可以设置值为函数来响应交互事件,也可以通过设置参数来控制要监听的事件类型。
自定义指令也可以设置值和参数,这些设置数据会通过一个param对象传递到指令中实现的生命周期方法中,示例代码如下:
html
<div id="app">
<input v-getfocus:custom="1"/>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
App.directive('getfocus', {
mounted(el, param) {
if(param.value == "1") {
el.focus()
}
//打印参数:custom
console.log('参数:' + param.arg)
}
})
App.mount("#app");
</script>

上述代码很好理解,指令设置的值1被绑定到param对象的value属性上,指令设置的custom参数被绑定到param对象的arg属性上。
有了参数,Vue自定义指令的使用非常灵活,通过不同的参数进行区分,我们可以很方便地处理复杂的组件渲染逻辑。
对于指令设置的值,也允许直接设置为JavaScript对象,例如下面的设置也是合法的:
html
<input v-getfocus:custom="{a:1, b:2}" />
html
<div id="app">
<input v-getfocus:custom="{a:1, b:2}"/>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
App.directive('getfocus', {
mounted(el, param) {
console.log(param.arg)
console.log(param.value)
}
})
App.mount("#app");
</script>

5、组件的Teleport功能
Teleport可以简单翻译为"传送,传递",这是Vue 3提供的新功能。有了Teleport功能,在编写代码时,开发者可以将相关的行为逻辑和UI封装到同一个组件中,提高代码的聚合性。
要明白Teleport功能如何使用,以及适用的场景,我们可以通过一个小例子来体会。如果我们需要开发一个全局弹窗组件,此组件自带一个触发按钮,当用户单击此按钮后,会弹出弹窗。新建一个名为html的测试文件,在其中编写如下核心示例代码:
html
<div id="app">
<my-alert></my-alert>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
App.component('my-alert', {
template: `
<div>
<button @click="show = true">弹出弹窗</button>
</div>
<div v-if="show" style="text-align: center;padding:20px;
position:absolute;top:45%;left:30%;width:40%;border:black solid 2px;
background-color:white">
<h3>弹窗</h3>
<button @click="show = false">隐藏弹窗</button>
</div>`,
setup() {
const show = Vue.ref(false);
return {show}
}
})
App.mount("#app");
</script>
在上述代码中,我们定义了一个名为my-alert的组件,这个组件中默认提供了一个功能按钮,点击后会弹出弹窗,按钮和弹窗的逻辑都被聚合到了组件内部。运行代码,效果如图所示。

目前看来,我们的代码运行没什么问题,但是此组件的可用性并不好,当我们在其他组件内部使用此组件时,全局弹窗的布局可能无法达到我们的预期。例如,修改HTML结构如下:
html
<div id="app">
<div style="position:absolute; width: 50px;">
<my-alert></my-alert>
</div>
</div>
再次运行代码,由于当前组件被放入了一个外部的div元素内,因此其弹窗布局会受到影响,效果如图所示。

有两种方式可以避免这种由于组件树结构的改变而影响组件内元素的布局的问题:
- 一种方式是将触发事件的按钮与全局的弹窗分成两个组件编写,保证全局弹窗组件挂载在body标签下,但这样会使得相关的组件逻辑分散在不同地方,不利于后续维护;
- 另一种方式是使用Teleport。
在定义组件时,如果组件模板中的一些元素只能挂载在指定的标签下,可以使用Teleport来指定,可以形象地理解Teleport的功能是将这部分元素"传送"到指令的标签下。以上述代码为例,可以指定全局弹窗只挂载在body元素下,修改代码如下:
html
<div id="app">
<div style="position:absolute; width: 50px;">
<my-alert></my-alert>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const App = Vue.createApp({});
App.component('my-alert', {
template: `
<div>
<button @click="show = true">弹出弹窗</button>
</div>
<teleport to="body">
<div v-if="show" style="text-align: center;padding:20px;
position:absolute;top:45%;left:30%;width:40%;border:black solid 2px;
background-color:white">
<h3>弹窗</h3>
<button @click="show = false">隐藏弹窗</button>
</div>
</teleport>`,
setup() {
const show = Vue.ref(false);
return {show}
}
})
App.mount("#app");
</script>

优化后的代码,组件本身在组件树中的任何位置,弹窗都能正确地布局。
6、综合示例
要求:
- 创建一个Vue组件。
- 演示组件生命周期钩子函数的使用。
- 配置全局属性或选项。
- 使用组件属性的高级用法,如动态绑定class和style。
- 使用Mixin技术,混入公共功能。
- 创建并使用自定义指令。
- 利用Teleport特性,将组件内容渲染到指定位置。
html
<div id="app">
<my-component></my-component>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="text/javascript">
const {createApp, ref, onMounted, onUnmounted} = Vue;
//创建一个Mixin对象,定义一些公共功能
const myMixin = {
mounted() {
console.log('Mixin对象mounted')
}
};
const App = {};
let app = createApp(App);
//创建一个自定义指令
const vFocus = {
mounted(el) {
el.focus();
}
};
//使用Mixin技术
app.mixin(myMixin);
app.directive('focus', vFocus);
//创建一个名为my-component的组件
app.component('my-component', {
setup() {
onMounted(()=>{
console.log('Component mounted');
});
onUnmounted(()=>{
console.log('Component destroyed');
});
const dynamicClass = ref('dynamic-class');
const dynamicStyle = ref({color: 'red'});
return {
dynamicClass,
dynamicStyle
}
},
template: `
<div>
<!-- 使用Teleport特性,将组件内容渲染到指定的位置 -->
<teleport to="#app">
<p :class="dynamicClass" :style="dynamicStyle">Hello,Vue!</p>
<input v-focus>
</teleport>
</div>`
});
//创建并挂载Vue实例到DOM元素上
app.mount("#app");
</script>
