一、组件的基础
1. 组件的定义
- 定义组件:应用API(component)。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="root"></div> <script> const app = Vue.createApp({ // 根组件 template: `<div><hello/><world/></div>` }); // 子组件 app.component('hello', { template: `<div>hello</div>` }); // 子组件 app.component('world', { template: `<div>world</div>` }) const vm = app.mount('#root'); </script> </body> </html>
2. 组件的复用性
- 组件定义后可以重复使用,且互不影响。
const app = Vue.createApp({ // 根组件 template: `<div> <counter /> <counter /> <counter /> </div>` }); // 子组件 app.component('counter', { data() { return { count: 0 }; }, template: `<div @click='count++'>{{count}}</div>` });
3. 全局组件
- app.component() 定义的组件都是全局组件。
- 全局组件,只要定义了,到处可以使用,性能不高,但使用方便。
- 名字建议,小写字母单词,中间用横线间隔。
4. 局部组件
- 定义局部组件使用时,需要用到 components 选项。
- components 用于声明一组可用于组件实例中的组件。
// 子组件 const HelloWorld = { template: `<div>hello world</div>` } const app = Vue.createApp({ // components: { 'hello-world': HelloWorld } components: { HelloWorld }, // 根组件 template: `<div> <hello-world> </div>` });
- 局部组件,定义了,要注册后才能使用,性能较高,使用相对麻烦。
- 名字建议大写字母开头,驼峰命名。
- 局部组件使用时,要做一个名字和组件间的映射对象,若不写映射,Vue底层也会尝试帮你做映射。
二、组件间传值与传值校验
1. 组件间传值
- props 选项是一个用于从父组件接收数据的数组或对象。
- 结合 v-bind 指令可以传递动态 prop 。
const app = Vue.createApp({ data() { return { message: 'hello' }; }, template: `<div> <demo v-bind:content='message' /> </div>` }); app.component('demo', { props: ['content'], template: `<div>{{content}}</div>` });
2. 传值校验
- 通过将 props 设置为对象,可以对传值进行校验。
- 可以校验 String、Number、Function、Boolean、Object、Array等类型。
- 如:使用 content: Number 校验传入的值是否为 Number 类型,若不是控制台中会抛出警告,但是不会报错。
app.component('demo', { props: { content: Number }, template: `<div>{{typeof content}}</div>` });
在 props 为对象时,还可以添加下列特殊选项:
- type:可以是下列原生构造函数中的一种:String、Number、Boolean、Array、Object、Date、Function、Symbol、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。
default:any 为该 prop 指定一个默认值。
required:Boolean 定义该 prop 是否是必填项。
- validator:Function 自定义验证函数会将该 prop 的值作为唯一的参数代入。如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。
props: { content: { type: Number, default: 123, required: true, validator: (num) => { return num > 1000 } } }
3. 传多个 prop
- 传多个 prop 时,可以将这些 prop 先存入一个对象中,再将这个对象一起传给子组件。
// v-bind='param' // 等价于 v-bind:a='param.a' v-bind:b='param.b' v-bind:c='param.c' v-bind:d='param.d' const app = Vue.createApp({ data() { return { param: { a: 'A', b: 'B', c: 'C', d: 'D' } }; }, template: `<div> <demo v-bind='param' /> </div>` }); app.component('demo', { props: ['a', 'b', 'c', 'd'], template: `<div>{{a}}--{{b}}--{{c}}--{{d}}</div>` });
4. 命名问题
- 通常属性传的时候,使用 content-abc 横线分隔命名,接受的时候,使用 contentAbc 驼峰命名。
const app = Vue.createApp({ data() { return { content: 'hello' }; }, template: `<div> <demo v-bind:content-abc='content' /> </div>` }); app.component('demo', { props: ['contentAbc'], template: `<div>{{contentAbc}}</div>` });
5. 单向数据流
- 单项数据流的概念:子组件可以使用父组件传递过来的数据,但是绝对不能修改传递过来的数据。
- 直接修改父组件传递过来的值会在控制台发出警告,且值也不会变化。
- 可以将父组件传递的值作为初始值,保存在子组件的data对象中进行更新操作。
const app = Vue.createApp({ data() { return { count: 1 }; }, template: `<div> <demo v-bind:count='count' /> </div>` }); app.component('demo', { props: ['count'], data() { return { myCount: this.count }; }, template: `<div @click='myCount++'>{{myCount}}</div>` });
三、Non-Props 属性是什么
- Non-Props 属性是指没有被 props 接受的属性。
- 它会被默认继承到子组件上,常用于给子组件添加 class、style 属性。
const app = Vue.createApp({ template: ` <div> <demo style='color: red' /> </div> ` }); app.component('demo', { template: `<div>hello world</div>` });
- 若不想子组件继承 Non-Props 属性,可以在子组件中添加:inheritattrs 选项
inheritAttrs: false
- 通过 $attrs 可以获取到这些 Non-Props 属性。
// 直接获取全部 Non-Props template: `<div v-bind='$attrs'>hello world</div>`
// 分别获取每一个 Non-Prop template: ` <div :style='$attrs.style'>hello world</div> <div :class='$attrs.class'>hello world</div> `
- 在生命周期函数中获取 Non-Props 属性。
mounted(){ console.log(this.$attrs); }
四、父子组件间如何通过事件进行通信
1. 不传参数
- 这里要用到实例方法 $emit 用于触发当前实例上的事件。
const app = Vue.createApp({ data(){ return { count: 1 } }, methods: { // 5. 执行handleAddOne函数count+1 handleAddOne(){ this.count += 1 } }, // 4. addOne事件触发,调用 handleAddOne template: ` <div> <demo :count='count' @addOne='handleAddOne'/> </div> ` }); app.component('demo', { props: ['count'], methods: { // 2. 执行 handleClick handleClick(){ // 3. $emit 会触发当前实例上的 addOne 事件 this.$emit('addOne') } }, // 1. 点击事件触发,调用 handleClick template: ` <div @click='handleClick'>{{count}}</div> ` });
2. 需要传递参数
- 如果需要还可以传递一些参数,因为 $emit 中附加参数都会传给监听器回调。
const app = Vue.createApp({ data() { return { count: 1 } }, methods: { handleAdd(n1, n2, n3) { this.count += n1 + n2 + n3; } }, template: ` <div> <demo :count='count' @add='handleAdd'/> </div> ` }); app.component('demo', { props: ['count'], methods: { handleClick() { this.$emit('add', 2, 3, 4) } }, template: ` <div @click='handleClick'>{{count}}</div> ` });
3. emits 选项的作用
- emits 可以是数组或对象,从组件触发自定义事件,emits 可以是简单的数组,也可以是对象,后者允许配置事件验证。
const app = Vue.createApp({ data() { return { count: 1 } }, methods: { handleAdd(count) { this.count = count; } }, template: `<div><demo :count='count' @add='handleAdd'/></div>` }); app.component('demo', { props: ['count'], // 数组的形式,注意 emits 的事件名最好和 $emit 的事件名一样 // 否则会在控制台打印警告信息 // emits: ['add'], // 对象的形式 emits: { // 在 $emit 传入的count小于等于5时,返回false控制台会打印警告信息。 // 大于5时,返回true,则不会打印警告信息 add: function (count) { if (count > 5) return true; return false } }, methods: { handleClick() { this.$emit('add', this.count + 1) } }, template: ` <div @click='handleClick'>{{count}}</div> ` });
4. 使用 v-model 实现父子组件通信
- 默认情况下,使用
modelValue
作为 prop 和update:modelValue
作为事件。const app = Vue.createApp({ data() { return { count: 1 } }, // 1. 通过 v-model 绑定 count 的值,并将其传递给子组件 template: `<demo v-model='count'/>` }); app.component('demo', { // 2. 子组件通过名为 modelValue 的属性进行接受 props: ['modelValue'], methods: { handleClick() { // 3. 通过传递的参数更新modelValue与count this.$emit('update:modelValue', this.modelValue + 1) } }, template: ` <div @click='handleClick'>{{modelValue}}</div> ` });
- 若想要使用其它的属性名,需要使用 v-model:one='count' 的写法。
const app = Vue.createApp({ data() { return { count: 1 } }, template: `<demo v-model:one='count'/>` }); app.component('demo', { props: ['one'], // 这里同样可以设置 emits 选项,同样要注意名字要一致 emits: ['update:one'], methods: { handleClick() { this.$emit('update:one', this.one + 1) } }, template: ` <div @click='handleClick'>{{one}}</div> ` });
- v-model 绑定多个属性 。
const app = Vue.createApp({ data() { return { count: 1, count1: 1 } }, template: `<demo v-model:one='count' v-model:two='count1'/>` }); app.component('demo', { props: ['one', 'two'], methods: { handleClick() { this.$emit('update:one', this.one + 1) }, handleClick1() { this.$emit('update:two', this.two + 1) } }, template: ` <div @click='handleClick'>{{one}}</div> <div @click='handleClick1'>{{two}}</div> ` });
- 自定义修饰符,注意这里只能使用modelValue的情况。
- 添加到组件
v-model
的修饰符将通过 modelModifiers prop 提供给组件,同时会对修饰符进行校验,没有传修饰符时返回空对象,确保程序不会报错。const app = Vue.createApp({ data() { return { count: 'a', } }, template: `<demo v-model.uppercase='count'/>` }); app.component('demo', { props: { modelValue: String, modelModifiers: { // 返回空对象外面需要加() default: () => ({}) } }, methods: { handleClick() { let newValue = this.modelValue + 'b'; if (this.modelModifiers.uppercase) { newValue = newValue.toUpperCase(); } this.$emit('update:modelValue', newValue) }, }, template: ` <div @click='handleClick'>{{modelValue}}</div> ` });
五、使用插槽和具名插槽解决组件内容传递问题
1. slot 插槽
<slot>
元素作为组件模板之中的内容分发插槽。<slot>
元素自身将被替换。- slot 中使用数据,作用域的问题:
- 父模板里调用的数据属性,使用的都是父模板里的数据。
- 子模版里调用的数据属性,使用的都是子模版里的数据。
const app = Vue.createApp({ data() { return { text: '提交', } }, template: ` <demo><button>{{text}}</button></demo> ` }); app.component('demo', { template: ` <div> <input /> <slot></slot> </div> ` });
- 若模板组件中没有添加内容,可以在插槽中设置默认值。
<demo></demo> app.component('demo', { template: ` <div> <input /> <slot><div>hello world</div></slot> </div> ` });
2. 具名插槽
- 想要有选择性的接受插槽中的内容,需要使用具名插槽。
- 通过 v-slot 指令可以设置插槽的名称,<slot>通过name属性接受需要插入的内容。
- 通常需要在插槽内容外包裹一个<template>标签。
- 注意:v-slot 后面用 “ : ” 而不是 “ = ” ,v-slot可以缩写为 “ # ”。
const app = Vue.createApp({ template: ` <layout> <template v-slot:header> <header>header</header> </template> <template v-slot:footer> <footer>footer</footer> </template> </layout> ` }); app.component('layout', { template: ` <div> <slot name='header'></slot> <main>main</main> <slot name='footer'></slot> </div> ` });
3. 作用域插槽
- 作用域插槽:可以让插槽内容能够访问子组件中才有的数据。
const app = Vue.createApp({ // v-slot='pList' 会接受所有的prop // 使用 v-slot={ item } 解构的方式可以接受特定的prop template: ` <list v-slot='pList'> <span>{{pList.item}}</span> </list> ` }); app.component('list', { template: ` <div> <slot v-for='item in 3' :item='item'></slot> </div> ` });
六、动态组件和异步组件
1. 动态组件
- 动态组件:根据数据的变化,结合 component 这个标签,来动态切换组件。
- component:渲染一个“元组件”为动态组件。依
is
的值,来决定哪个组件被渲染。const app = Vue.createApp({ data() { return { currentItem: 'input-item' } }, methods: { handleClick() { if (this.currentItem === 'input-item') { this.currentItem = 'common-item' } else { this.currentItem = 'input-item' } } }, template: ` <component :is='currentItem'></component> <button @click='handleClick'>切换</button> ` }); app.component('input-item', { template: ` <input /> ` }); app.component('common-item', { template: ` <div>hello</div> ` });
使用<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。<keep-alive> <component :is='currentItem'></component> </keep-alive>
2. 异步组件
- defineAsyncComponent:可以创建一个只有在需要时才会加载的异步组件。
- 参数为一个返回
Promise
的工厂函数。const app = Vue.createApp({ template: ` <div> <common-item /> <async-common-item /> </div> ` }); app.component('common-item', { template: `<div>hello</div>` }); app.component('async-common-item', Vue.defineAsyncComponent(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ template: `<div>this is an async component</div>` }) }, 2000) }) }));
七、基础语法知识点查缺补漏
1. v-once 指令
- v-once:不需要表达式,只渲染元素和组件一次。
const app = Vue.createApp({ data() { return { count: 1 } }, methods: { handleClick() { this.count++; console.log(this.count); } }, template: `<div @click='handleClick' v-once>{{ count }}</div>` });
2. 特殊属性 ref
ref
被用来给元素或子组件注册引用信息。- 引用信息将会被注册在父组件的
$refs
对象上。- 如果在普通的 DOM 元素上使用,引用指向的就是那个 DOM 元素;
- 如果用在子组件上,引用就指向组件实例。
使用 ref 和 $refs 获取 dom 节点:
- 注意:获取 dom 节点,需要在 mounted 生命周期之后(包括mounted)。
const app = Vue.createApp({ data() { return { count: 1 } }, mounted() { // console.log(this.$refs.count); // 获取到 dom 节点后,可以修改其中的内容 this.$refs.count.innerHTML = 'hello'; }, template: `<div ref='count' >{{ count }}</div>` });
使用 ref 和 $refs 获取子组件注册引用信息:
const app = Vue.createApp({ data() { return { count: 1 } }, mounted() { // 调用子组件的sayHello方法 this.$refs.item.sayHello(); }, template: `<item ref='item'/>` }); app.component('item', { methods: { sayHello() { console.log('hello'); } }, template: `<div>hello</div>` });
3. provide / inject 组合式API
- provide / inject :可以跨组件,进行多层组件之间的传递。
- 但是传递的值不是响应式的,是一次性的数据。
const app = Vue.createApp({ data() { return { count: 1 } }, methods: { handleClick() { console.log(this.count); this.count++; } }, provide() { return { count: this.count } }, template: `<item1 @click='handleClick'/>` }); app.component('item1', { template: `<item2 />` }); app.component('item2', { inject: ['count'], template: `<div>{{count}}</div>` });