赞
踩
- <span style="background-color:#f8f8f8"><span style="color:#333333">1. 组件基础
- 2. 组件通讯
- 3. 插槽slot
- 4. 依赖出入
- 5. 动态组件
- 6. 异步组件</span></span>
3.1.1 什么是组件?为什么要用组件?
组件: 实现应用中局部功能代码和资源的集合。
组件的优点: 帮我们解决依赖关系混乱,不好维护的难题。提高代码的复用率。
3.1.2 什么是组件化?理解组件化
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
3.1.3 如何封装一个vue组件
目标:理解一般思路即可
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。
先构建组件的模板
定义组件
注册组件
使用组件
3.2.1 构建模板、定义组件。
下面是定义一个组件的代码。
<span style="background-color:#f8f8f8"><span style="color:#333333">注意:组件里面的选项api基本上和 createApp() 的选项api一致,所以我们在组件里面也可以用 data,computed,methods ...; const Header = { // template 是模板,里面存放组件的html代码片段。 template: '<header>头部-{{msg}} - {{ reverseMsg }}</header>', // 可以写任意的属于vue的选项 data () { return { msg: 'hello header' } }, computed: { reverseMsg () { return this.msg.split('').reverse().join('') } } }</span></span>
通过上面的代码我们可以发现,如果是比较复杂的 html 片段我们直接写在 template 模板里面,显然有点不合适。有没有更简单的方式呢?请看下面的代码。
- <span style="background-color:#f8f8f8"><span style="color:#333333"> 特别注意:这段代码不要写在 vue节点作用域内部,不然是不起作用的。
-
- <template id="header">
- <header>头部-{{msg}} - {{ reverseMsg }}</header>
- </template></span></span>
- <span style="background-color:#f8f8f8"><span style="color:#333333"> const Header = {
- // template 是模板,里面存放组件的html代码片段。
- template: '#header',
- // 可以写任意的属于vue的选项
- data () {
- return {
- msg: 'hello header'
- }
- },
- computed: {
- reverseMsg () {
- return this.msg.split('').reverse().join('')
- }
- }
- }</span></span>
3.2.2 全局注册组件
我们可以使用 Vue 应用实例的 app.component()
方法,让组件在当前 Vue 应用中全局可用
- <span style="background-color:#f8f8f8"><span style="color:#333333">import { createApp } from 'vue'
-
- const app = createApp({})
-
- app.component(
- // 注册的名字
- 'MyComponent',
- // 组件的实现
- {
- /* ... */
- }
- )</span></span>
app.component()
方法可以被链式调用:
- <span style="background-color:#f8f8f8"><span style="color:#333333">app
- .component('ComponentA', ComponentA)
- .component('ComponentB', ComponentB)
- .component('ComponentC', ComponentC)</span></span>
全局注册的组件可以在此应用的任意组件的模板中使用
- <span style="background-color:#f8f8f8"><span style="color:#333333"><!-- 这在当前应用的任意组件中都可用 -->
- <ComponentA/>
- <ComponentB/>
- <ComponentC/></span></span>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用
完整案例:24_component_vue3.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>24_全局注册组件</title> </head> <body> <div id="app"> <!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 --> <!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。 这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。 这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 --> <my-header></my-header> <!-- <MyHeader></MyHeader> --> </div> </body> <!-- 01 定义组件的模板 --> <template id="header"> <header>头部-{{msg}} - {{ reverseMsg }}</header> </template> <script src="lib/vue.global.js"></script> <script> // 02.定义组件 - 首字母大写 const Header = { template: '#header', // 绑定页面的模板 --- 必不可少 // 可以写任意的属于vue的选项 data () { return { msg: 'hello header' } }, computed: { reverseMsg () { return this.msg.split('').reverse().join('') } } } const { createApp } = Vue const app = createApp({ }) // 03.全局注册组件 --- app.mount('#app') 之前 // app.component('MyHeader', Header) // 大驼峰式 app.component('my-header', Header) // 短横线式 app.mount('#app') </script> </html></span></span>
vue2全局注册组件
完整案例:25_component_vue2.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>25_vue2全局注册组件</title> </head> <body> <div id="app"> <!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 --> <!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。 这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。 这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 --> <my-header></my-header> <!-- <MyHeader></MyHeader> --> </div> </body> <!-- 01 定义组件的模板 --> <template id="header"> <header>头部-{{msg}} - {{ reverseMsg }}</header> </template> <script src="lib/vue.js"></script> <script> // 02.定义组件 - 首字母大写 const Header = { template: '#header', // 绑定页面的模板 --- 必不可少 // 可以写任意的属于vue的选项 data () { // vue2中的所有的组件的 data 必须是函数 return { msg: 'hello header' } }, computed: { reverseMsg () { return this.msg.split('').reverse().join('') } } } // 03.全局注册组件 --- new Vue 实例 之前 // Vue.component('MyHeader', Header) // 大驼峰式 Vue.component('my-header', Header) // 短横线式 new Vue({}).$mount('#app') </script> </html></span></span>
3.2.2 局部注册组件
全局注册虽然很方便,但有以下几个问题:
全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
局部注册需要使用 components
选项
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。
完整案例:26_components_vue3.html
vue3局部注册组件
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>26_vue3 局部注册组件</title> </head> <body> <div id="app"> <!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 --> <!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。 这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。 这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 --> <my-header></my-header> <!-- <MyHeader></MyHeader> --> </div> </body> <!-- 01 定义组件的模板 --> <template id="header"> <header>头部-{{msg}} - {{ reverseMsg }}</header> </template> <script src="lib/vue.global.js"></script> <script> // 02.定义组件 - 首字母大写 const Header = { template: '#header', // 绑定页面的模板 --- 必不可少 // 可以写任意的属于vue的选项 data () { return { msg: 'hello header' } }, computed: { reverseMsg () { return this.msg.split('').reverse().join('') } } } const { createApp } = Vue const app = createApp({ components: { // 03 局部注册组件 // 'my-header': Header MyHeader: Header } }) app.mount('#app') </script> </html></span></span>
完整案例:27_components_vue2.html
vue2局部注册组件
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>27_vue2局部注册组件</title> </head> <body> <div id="app"> <!-- 04 使用组件 大驼峰式 短横线式 在html文件中只能使用 短横线式 --> <!-- 为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。 这意味着一个以 `MyComponent` 为名注册的组件,在模板中可以通过 `<MyComponent>` 或 `<my-component>` 引用。 这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。 --> <my-header></my-header> <!-- <MyHeader></MyHeader> --> </div> </body> <!-- 01 定义组件的模板 --> <template id="header"> <header>头部-{{msg}} - {{ reverseMsg }}</header> </template> <script src="lib/vue.js"></script> <script> // 02.定义组件 - 首字母大写 const Header = { template: '#header', // 绑定页面的模板 --- 必不可少 // 可以写任意的属于vue的选项 data () { // vue2中的所有的组件的 data 必须是函数 return { msg: 'hello header' } }, computed: { reverseMsg () { return this.msg.split('').reverse().join('') } } } new Vue({ components: { // 03 局部注册组件 // MyHeader: Header 'my-header': Header } }).$mount('#app') </script> </html></span></span>
局部注册的组件在后代组件中并*不*可用
3.2.3 组件使用注意事项
以上案例使用 PascalCase 作为组件名的注册格式,这是因为:
PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
<PascalCase />
在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板中可以通过 <MyComponent>
或 <my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>28_父子组件</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 </div> </template> <script src='https://unpkg.com/vue@3/dist/vue.global.js'></script> <script> // 定义子组件 const Child = { template: '#child' } // 定义父组件 const Parent = { template: '#parent', components: { MyChild: Child // 在父组件中 注册 子组件 } } const { createApp } = Vue const app = createApp({ components: {// 在根组件Root中注册"父组件" MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
组件间的关系:
父---子
兄---弟
爷---孙
组件有 分治 的特点,每个组件之间具有一定的独立性,以防止数据混乱,但是在实际工作中使用组件的时候有互相之间传递数据的需求,此时就得考虑如何进行 组件间传值 的问题了。
完整案例:28_parent_child_component.html
父子组件
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>28_父子组件</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 -- {{msg}} <my-child></my-child> </div> </template> <template id="child"> <div> <!-- 子组件也想用父组件的数据 ,可以吗? --> 我是子组件 -- {{msg}} </div> </template> <script src='https://unpkg.com/vue@3/dist/vue.global.js'></script> <script> const Child = { template: '#child' } const Parent = { template: '#parent', data(){ //组件实例 也 有保存自己数据的data return { msg:'hello' // 父组件的数据 } }, components: { MyChild: Child } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
通过以上代码发现,组件不能直接访问 Vue根实例中的 data,而且即使可以访问,如果将所有的数据都放在 Vue 根实例中,Vue根实例就会变的非常臃肿。
3.4.1 Prop
学习:状态选项props 以及 实例属性 $attrs
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute。
3.4.2 基本语法
父组件通过自定义属性 传递props
- <span style="background-color:#f8f8f8"><span style="color:#333333">1、给 prop 传入一个静态的值:
- <video-item title="哈哈"></video-item>
- 2、动态的值,可以配合v-bind指令进行传递
- <video-item :title="title"></video-item></span></span>
子组件中通过props选项 接收值
<span style="background-color:#f8f8f8"><span style="color:#333333">1、字符串数组声明props { props: ['foo'], created() { // props 会暴露到 `this` 上 console.log(this.foo) } } 2、对象的形式声明 { props: { title: String, likes: Number } }</span></span>
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>29_父组件给子组件传值 方式1 - 数组</title> <style> #app{ width: 300px; height: 220px; border: 1px solid #000; } .child{ width: 200px; height: 150px; margin: 30px; border: 1px solid #f00; } </style> </head> <body> <div id="app"> 我是父组件 <my-child :movies="movies"></my-child> </div> </body> <template id="child"> <div class="child"> 我是子组件 <ul> <li v-for="item in movies" :key="item">{{item}}</li> </ul> </div> </template> <script src='https://unpkg.com/vue@3/dist/vue.global.js'></script> <script> const Child = { props: ['movies'], template: '#child' } const { createApp } = Vue const app = createApp({ data() { return { movies:['西游记','水浒传','三国演义','白蛇传'] } }, components: { MyChild: Child } }) app.mount('#app') </script> </html></span></span>
对于以对象形式声明中的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number
类型,则可使用 Number
构造函数作为其声明的值。
如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。
虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 模板时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式
<my-com :likeNum="100"></my-com>
===><my-com :like-num="100"></my-com>
对于组件名我们推荐使用 PascalCase,因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格-短横线。
完整案例:29_parent_child_component_value1.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>29_父组件给子组件传值 方式1 - 数组</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <!-- 父组件调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,对象,数组,null,undefined,需要使用绑定属性 --> <my-child :msg="msg" :flag="true" :num="100" :obj="{a: 1, b: 2}" :arr="['a', 'b', 'c']"></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ msg }} - {{ flag }} - {{ num }} - {{ obj }} - {{ arr }} </div> </template> <script src="lib/vue.global.js"></script> <script> // 在定义子组件的地方,添加props选项, // props的数据类型为数组,数组的元素即为 自定义的属性名, 之后就可以直接在子组件中通过 自定义的属性名 使用 传递的值 const Child = { props: ['msg', 'flag', 'num', 'obj', 'arr'], template: '#child' } const Parent = { template: '#parent', components: { MyChild: Child }, data () { return { msg: 'hello parent' } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
虽然上述案例已经完成了父组件给子组件传值,但是不够严谨
可能A负责父组件的编写,B负责了子
组件的编写,容易造成 不知道 自定义的属性名的 数据类型
3.4.3 数据类型验证
用来对props
传递进来的数据进行类型验证:可以设定类型、默认值、是否必填、自定义校验规则等。
验证的 type 类型可以是:String
Number
Boolean
Object
Array
Function
Date
Symbol
任何自定义构造函数
或上述内容组成的数组
需要注意的是传递的数据值是null
和 undefined
时,会通过任何类型验证。
案例:指定props数据类型:30_parent_child_component_value2.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>30_父组件给子组件传值 方式2 - 对象-数据类型</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <!-- 父组件调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,对象,数组,null,undefined,需要使用绑定属性 --> <my-child :msg="msg" :flag="true" :num="100" :obj="{a: 1, b: 2}" :arr="['a', 'b', 'c']"></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ msg }} - {{ flag }} - {{ num }} - {{ obj }} - {{ arr }} </div> </template> <script src="lib/vue.global.js"></script> <script> // 方式2:props的数据类型为对象,对象的key值为自定义的属性名,value值为数据类型(不会阻碍程序运行,警告代码不严谨) // 如果一个自定义的属性的值既可以是 String 类型,也可以是 Number类型 key: String | Number const Child = { props: { msg: String, flag: Boolean, num: String | Number, obj: Object, arr: Array }, template: '#child' } const Parent = { template: '#parent', components: { MyChild: Child }, data () { return { msg: 'hello parent' } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
现在只能知道哪一个属性是哪一种数据类型,但是有时候我们可以不需要设置 自定义的属性(. )
<input />
<===><input type="text" />
3.4.4 自定义校验规则
完整案例: 31_parent_child_component_value3.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>31_父组件给子组件传值 方式3 - 对象-数据类型-默认值</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <!-- 父组件调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,对象,数组,null,undefined,需要使用绑定属性 --> <my-child :msg="msg" :flag="true" :num="100" :obj="{a: 1, b: 2}" :arr="['a', 'b', 'c']"></my-child> <my-child :flag="true" :num="2000" :arr="['aa']"></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ msg }} - {{ flag }} - {{ num }} - {{ obj }} - {{ arr }} </div> </template> <script src="lib/vue.global.js"></script> <script> // 方式3: props的数据类型为对象,对象的key值为自定义的属性名,value值为 一个新的对象 // 对象的key值可以为 type, 那么value值就为 数据类型 // 对象的key值可以为 required, 那么value值就为 true / false --- 该属性是不是必须传递,即使有默认值如果未传递还是要报警告 // 对象的key值可以为 default, 那么value值就为 默认值 -- 如果属性的值是对象和数组,那么默认值设置为函数返回即可 // 对象的key值可以为 validator 函数 ,可以自定义验证规则,但是不能验证多属性 const Child = { props: { msg: { type: String, required: true, default: 'hello vue', validator (val) { return val.length > 20 } }, flag: Boolean, num: { // type: Number | String, type: Number, default: 3000, validator (val) { // 猜测:不能验证多类型数据 Right-hand side of 'instanceof' is not an object return val > 2000 } }, obj: { type: Object, default () { return { a: 3, b: 4}} }, arr: { type: Array, default () { return ['1', '2', '3', '4']}, validator (val) { return val.length > 2 } } }, template: '#child' } const Parent = { template: '#parent', components: { MyChild: Child }, data () { return { msg: 'hello parent' } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
3.4.5 prop的单向数据流
子组件内部不能改变props!!
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- props:['num'],
- method:{
- inc(){
- console.log(this.num) // 可以访问
- this.num++ // 不能改变
- }
- }
- }</span></span>
所有的 props 都遵循着单向绑定原则(
单项数据流
),props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
目的:这避免子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
比如在父级和子级分别有一个点击增加一个数据值的按钮,点击父级按钮两边的数据都会改变,点击子级按钮只有子级的改变,但是不管怎么变都是假象,父级一变他也跟着变了(乖乖听爸爸的话)。所以子级是不能自己更改数据的,而且更改了Vue也会报错。
但是有特殊情况,prop是数组或者对象时,更改子级的数据父级也会随着更改,可见prop传的是同一个引用。引用不可以变,但是引用内的值可以变。怎么避免,无法避免,只能是说别更改就对了
3.4.6 $attrs
如果不使用props接收传递过来的值,那么数据就会存在于$attrs中
如果使用了props接收传递过来的值,那么数据就会存在与$props中
$attrs 的用法;
- <span style="background-color:#f8f8f8"><span style="color:#333333"><template id="child">
- <div>
- 我是子组件 {{$attrs}} // $attrs 是一个属性对象集合,注意:这里的属性不包含 class 和 style
- </div>
- </template></span></span>
学习:状态选项emits、实例方法 $emit
作用:用于子组件给父组件传值,主要利用自定义事件
3.5.1 $emit 基础语法
自定义事件流程:
在子组件中,通过 $emit()
来触发事件
在父组件中,通过 v-on
来监听子组件事件,同样,组件的事件监听器也支持 .once
修饰符
像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>32_子组件给父组件传值</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <!-- 在父组件调用子组件的地方,绑定一个自定义的事件,该事件由父组件实现,默认参数即为子组件传递给父组件的值 --> <my-child @my-event="getData"></my-child> </div> </template> <template id="child"> <div> 我是子组件 <button @click="$emit('my-event', 2000)">传2000</button> <button @click="sendData(3000)">传3000</button> </div> </template> <script src="lib/vue.global.js"></script> <script> // 在子组件的某一个事件内部,通过 this.$emit('自定义的事件名',传递的参数)完成子组件给父组件传值,内联事件处理器中不需要写this const Child = { template: '#child', mounted () { this.$emit('my-event', 1000) }, methods: { sendData (num) { this.$emit('my-event', num) } } } const Parent = { template: '#parent', components: { MyChild: Child }, methods: { getData (val) { console.log('接收到子组件的数据:' + val) } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
3.5.2 声明触发的事件
组件要触发的事件还可以显式地通过 emits 选项来声明:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- emits: ['inFocus', 'submit']
- }</span></span>
这个 emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- emits: {
- submit(payload) {
- // 通过返回值为 `true` 还是为 `false` 来判断
- // 验证是否通过
- }
- }
- }</span></span>
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit
的内容,返回一个布尔值来表明事件是否合法。
<span style="background-color:#f8f8f8"><span style="color:#333333">{ emits: { // 没有校验 click: null, // 校验 submit 事件 submit: ({ email, password }) => { if (email && password) { return true } else { console.warn('Invalid submit event payload!') return false } } }, methods: { submitForm(email, password) { this.$emit('submit', { email, password }) } } }</span></span>
完整案例:33_child_parent_component_value2.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>33_子组件给父组件传值-声明触发的事件并且验证</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <!-- 在父组件调用子组件的地方,绑定一个自定义的事件,该事件由父组件实现,默认参数即为子组件传递给父组件的值 --> <my-child @my-event="getData"></my-child> </div> </template> <template id="child"> <div> 我是子组件 <button @click="$emit('my-event', 2000)">传2000</button> <button @click="sendData(3000)">传3000</button> </div> </template> <script src="lib/vue.global.js"></script> <script> // 在子组件的某一个事件内部,通过 this.$emit('自定义的事件名',传递的参数)完成子组件给父组件传值,内联事件处理器中不需要写this const Child = { template: '#child', // emits: ['my-event'], emits: { 'my-event': function (payload) { console.log(payload) return payload > 2000 } }, mounted () { this.$emit('my-event', 1000) }, methods: { sendData (num) { this.$emit('my-event', num) } } } const Parent = { template: '#parent', components: { MyChild: Child }, methods: { getData (val) { console.log('接收到子组件的数据:' + val) } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
自定义事件可以用于开发支持 v-model
的自定义表单组件
完整案例:34_custom_form_v-model.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>34_自定义表单组件配合 v-model</title> </head> <body> <div id="app"> <my-input v-model="userName" placeholder="用户名"></my-input> {{ userName }} <my-input type="password" v-model="password" placeholder="密码"></my-input>{{ password }} <my-button @my-click="submit"></my-button> </div> </body> <template id="input"> <input :type="type" :placeholder="placeholder" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <template id="btn"> <!-- 实际上执行2次,因为子组件模板中只有button --> <!-- <button @click="$emit('click')">提交</button> --> <button @click="$emit('my-click')">提交</button> </template> <script src="lib/vue.global.js"></script> <script> const Input = { template: '#input', props: { type: { type: String, default: 'text' }, placeholder: String, modelValue: String // 自定义表单组件 v-model 内部 为 modelValue 属性和 input事件 }, emits: ['update:modelValue'] } const Button = { template: '#btn' } const { createApp } = Vue const app = createApp({ components: { MyInput: Input, MyButton: Button }, data () { return { userName: '', password: '' } }, methods: { submit () { console.log({ userName: this.userName, password: this.password }) } } }) app.mount('#app') </script> </html></span></span>
另一种在组件内实现 v-model
的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get
方法需返回 modelValue
prop,而 set
方法需触发相应的事件:
- <span style="background-color:#f8f8f8"><span style="color:#333333">const Input = {
- template: `#input`,
- props: ['modelValue'],
- emits: ['update:modelValue'],
- computed: {
- value: {
- get() {
- return this.modelValue
- },
- set(value) {
- this.$emit('update:modelValue', value)
- }
- }
- }
- }</span></span>
3.6.1 多个v-model的绑定
仅限于 vue3
完整案例:35_custom_form_v-model_params.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>35_多个 v-model 绑定</title> </head> <body> <div id="app"> <!-- <my-form v-model:username="username" v-model:password="password"></my-form> --> <my-custom v-model:username="username" v-model:password="password"></my-custom> <my-button @my-click="submit"></my-button> </div> </body> <template id="custom"> <div> <input type="text" placeholder="用户名" :value="username" @input="$emit('update:username', $event.target.value)"> <input type="password" placeholder="密码" :value="password" @input="$emit('update:password', $event.target.value)"> </div> </template> <template id="btn"> <!-- 实际上执行2次,因为子组件模板中只有button --> <!-- <button @click="$emit('click')">提交</button> --> <button @click="$emit('my-click')">提交</button> </template> <script src="lib/vue.global.js"></script> <script> const Custom = { template: '#custom', props: { username: String, password: String }, emits: ['update:username', 'update:password'] } const Button = { template: '#btn' } const { createApp } = Vue const app = createApp({ components: { MyCustom: Custom, MyButton: Button }, data () { return { username: '', password: '' } }, methods: { submit () { console.log({ username: this.username, password: this.password }) } } }) app.mount('#app') </script> </html></span></span>
“透传 attribute”指的是由父组件传递给一个子组件,却没有被该子组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
3.7.1 attribute继承
当子组件标签上传递了一个未在Prop内注册的数据时,该数据特性会被添加到子组件的根元素上,变成组件根标签的行间属性。
- <span style="background-color:#f8f8f8"><span style="color:#333333"><my-button class="btn"></my-button>
-
- 子组件模板只有<button>按钮</button>,那么my-button的class直接透传给子组件
-
- <button class="btn"></button></span></span>
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
3.7.2 替换已有的属性
- <span style="background-color:#f8f8f8"><span style="color:#333333">父组件使用子组件标签的时候传入一个未注册的type属性:
- <my-cmp type="text"></my-cmp>
- 渲染结果:最终传入的 type="text" 就会替换掉 type="date" 并把它破坏!
- <input type="date"></span></span>
3.7.3 class
和 style
的合并
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton>
组件的模板改成这样:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><!-- <MyButton> 的模板 -->
- <button class="btn">click me</button></span></span>
则最后渲染出的 DOM 结果会变成:
<span style="background-color:#f8f8f8"><span style="color:#333333"><button class="btn large">click me</button></span></span>
3.7.4v-on
监听器继承
同样的规则也适用于 v-on
事件监听器:
<span style="background-color:#f8f8f8"><span style="color:#333333"><MyButton @click="onClick" /></span></span>
click
监听器会被添加到 <MyButton>
的根元素,即那个原生的 <button>
元素之上。当原生的 <button>
被点击,会触发父组件的 onClick
方法。同样的,如果原生 button
元素自身也通过 v-on
绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
3.7.5inheritAttrs
禁用继承
如果不希望组件的根元素继承父组件传递的未注册的特性,那么可以在组件选项中设置 inheritAttrs: false
。
最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs
选项为 false
,你可以完全控制透传进来的 attribute 被如何使用。
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- template: '#parent',
- inheritAttrs: false,
- // ...
- })</span></span>
注意:inheritAttrs: false 选项不会影响 style 和 class 的绑定。
3.7.6 多根节点的 Attributes 继承
$attrs
被显式绑定
希望这个属性被添加到其他元素上(默认是添加到根元素),可以配合实例的 $attrs 属性使用
$attrs 是根组件传递给子组件所有传递属性的集合,是一个对象,key 为传递的特性名,value 为传递特性值。
<span style="background-color:#f8f8f8"><span style="color:#333333">console.log(this.$attrs)//{ class:"my-cmp" }</span></span>
所以,可以使用 inheritAttrs: false
和 $attrs
相互配合,就可以手动设置这些特性被加到哪个元素上。
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- inheritAttrs: false,
- props: ['label', 'value'],
- template: `
- <label>
- {{ label }}
- <input
- v-bind="$attrs" //未注册的属性
- v-bind:value="value"
- v-on:input="$emit('input', $event.target.value)"
- >
- </label>`,
- })</span></span>
完整案例: 36_attribute_transmission.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>36_透传attribute</title> <style> .btn { border: 0; padding: 10px 20px; } .btn-success { background-color: rgb(29, 198, 29); color: #fff; } .btn-danger { background-color: rgb(218, 23, 39); color: #fff; } .btn-primary { background-color: rgb(75, 104, 236); color: #fff; } </style> </head> <body> <div id="app"> <my-button type="a" class="btn-success" @click="print('success')" @my-event="myalert(1)"></my-button> <my-button type="b" class="btn-danger" @click="print('danger')" @my-event="myalert(2)"></my-button> <my-button type="c" class="btn-primary" @click="print('primary')" @my-event="myalert(3)"></my-button> </div> </body> <template id="button"> <!-- Attributes 继承(class 与style合并, v-on事件继承) --> <!-- <button class="btn">按钮</button> --> <!-- 深层组件继承 --> <base-button></base-button> </template> <template id="base"> <button class="btn" v-bind="$attrs" >按钮</button> <div >测试</button> </template> <script src="lib/vue.global.js"></script> <script> const Base = { template: '#base', mounted () { // 在js中访问透传的 attributes console.log('2', this.$attrs) }, // inheritAttrs: false // 不想要一个组件自动地继承 attribute,你可以在组件选项中设置 } const Button = { template: '#button', components: { BaseButton: Base }, mounted () { console.log('1', this.$attrs) }, } const { createApp } = Vue const app = createApp({ components: { MyButton: Button }, methods: { print (msg) { console.log(msg) } }, myalert (num) { alert(num) } }) app.mount('#app') </script> </html></span></span>
学习:实例属性refs
用于注册模板引用。
ref
用于注册元素或子组件的引用。
使用选项式 API,引用将被注册在组件的 this.$refs
对象里
放在DOM元素上,获取DOM节点,放到组件上,获取子组件的实例,可以直接使用子组件的属性和方法
完整案例:37_attribute_ref.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>37_特殊的attribute——ref</title> </head> <body> <div id="app"> <!-- ref用到自定义组件,可以获取到子组件的实例 --> <my-child ref="childRef"></my-child> <!-- 原生jsDOM操作 --> <input type="text" id="userName" @input="getJsData"> <!-- ref用到DOM元素上,可以获取到当前的DOM元素 --> <input type="text" ref="password" @input="getRefData"> </div> </body> <template id="child"> <div> <h1>使用ref获取子组件的实例</h1> </div> </template> <script src="lib/vue.global.js"></script> <script> const Child = { template: '#child', data () { return { count: 10 } }, computed: { doubleCount () { return this.count * 2 } }, methods: { print () { console.log(this.count) } } } const { createApp } = Vue const app = createApp({ components: { MyChild: Child }, mounted () { console.log(this) // 父组件可以通过 ref 直接获取到子组件的实例的属性和方法等 console.log(this.$refs.childRef.count) console.log(this.$refs.childRef.doubleCount) this.$refs.childRef.print() }, methods: { getJsData () { console.log(document.getElementById('userName').value) }, getRefData () { console.log(this.$refs.password.value) } } }) app.mount('#app') </script> </html></span></span>
当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为 null
。
完整案例38_parent.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>38_parent</title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ $parent.msg }} <button @click="$parent.print()">执行</button> <button @click="test">执行</button> </div> </template> <script src="lib/vue.global.js"></script> <script> const Child = { template: '#child', methods: { test () { this.$parent.print() } } } const Parent = { template: '#parent', components: { MyChild: Child }, data () { return { msg: 'hello parent' } }, methods: { print () { console.log(this.msg) } } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent } }) app.mount('#app') </script> </html></span></span>
当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。
完整案例:39_root.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>39_root </title> </head> <body> <div id="app"> <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 --- {{ $root.count }} <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 --- {{ $root.count }} <button @click="$root.add()">加</button> </div> </template> <script src="lib/vue.global.js"></script> <script> const Child = { template: '#child', created () { console.log('child', this.$root) } } const Parent = { template: '#parent', components: { MyChild: Child }, created () { console.log('parent', this.$root) } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent }, data () { return { count: 100 } }, methods: { add () { this.count++ } } }) app.mount('#app') </script> </html></span></span>
兄弟组件传值 - 中央事件总线传值 ---- vue2
完整案例:40_brother_value-vue2.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>40_vue2 兄弟组件传值</title> </head> <body> <div id="app"> <my-content></my-content> <my-footer></my-footer> </div> </body> <script src="lib/vue.js"></script> <script> const bus = new Vue() // 中央事件总线传值 const Content = { template: `<div>{{ type }}</div>`, data () { return { type: '首页' } }, mounted () { bus.$on('change-type', (val) => { this.type = val }) } } const Footer = { template: ` <footer> <ul> <li @click="changeType('首页')">首页</li> <li @click="changeType('分类')">分类</li> <li @click="changeType('购物车')">购物车</li> <li @click="changeType('我的')">我的</li> </ul> </footer> `, methods: { changeType (type) { bus.$emit('change-type', type) } } } new Vue({ components: { MyContent: Content, MyFooter: Footer } }).$mount('#app') </script> </html></span></span>
vue3中没有明确的兄弟组件传值的方案,可以使用状态提升(找到这两个组件共同的父级组件,然后通过父与子之间的传值实现)
完整案例:41_brother_value-vue3.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>41_vue3 兄弟组件传值-状态提升</title> </head> <body> <div id="app"> <my-content :type="type" ></my-content> <my-footer @change-type="getVal"></my-footer> </div> </body> <script src="lib/vue.global.js"></script> <script> const Content = { props: ['type'], template: `<div>{{ type }}</div>`, } const Footer = { template: ` <footer> <ul> <li @click="changeType('首页')">首页</li> <li @click="changeType('分类')">分类</li> <li @click="changeType('购物车')">购物车</li> <li @click="changeType('我的')">我的</li> </ul> </footer> `, methods: { changeType (type) { this.$emit('change-type', type) } } } Vue.createApp({ components: { MyContent: Content, MyFooter: Footer }, data () { return { type: '首页' } }, methods: { getVal (type) { this.type = type } } }).mount('#app') </script> </html></span></span>
后期脚手架中可以使用 一个库 -> mitt
。
为什么要使用插槽?我们可以看看下面的图,通过分析我们看一下怎么解决这个问题。
A页面图
B页面图
C页面图
组件的最大特性就是 重用 ,而用好插槽能大大提高组件的可重用能力。
插槽的作用:父组件向子组件传递内容。
通俗的来讲,插槽无非就是在 子组件 中挖个坑,坑里面放什么东西由 父组件 决定。
插槽类型有:
单个(匿名)插槽
具名插槽
作用域插槽
3.12.1 插槽内容与插口<slot>
在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
这里有一个 <FancyButton>
组件,可以像这样使用:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><FancyButton>
- <h1>Click me!</h1> <!-- 插槽内容 -->
- </FancyButton></span></span>
而 <FancyButton>
的模板是这样的:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><button class="fancy-btn">
- <slot></slot> <!-- 插槽出口 -->
- </button></span></span>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
<span style="background-color:#f8f8f8"><span style="color:#333333"><button class="fancy-btn">Click me!</button></span></span>
完整案例:42_slot.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>42_插槽</title> </head> <body> <div id="app"> <fancy-button> click me <!-- 插槽内容 --> </fancy-button> <fancy-button> 登录 <!-- 插槽内容 --> </fancy-button> <fancy-button> 注册 <!-- 插槽内容 --> </fancy-button> </div> </body> <template id="btn"> <button> <slot></slot> <!-- 插槽出口 --> </button> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Button = { template: '#btn' } const app = createApp({ components: { FancyButton: Button } }) app.mount('#app') </script> </html></span></span>
3.12.2 渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><span>{{ message }}</span>
- <FancyButton>{{ message }}</FancyButton></span></span>
这里的两个 {{ message }}
插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
完整案例:43_slot_render_scope.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>43_插槽渲染作用域</title> </head> <body> <div id="app"> <div>{{ message }}</div> <fancy-button> {{ message }}<!-- 插槽内容 --> </fancy-button> </div> </body> <template id="btn"> <button> <slot></slot> <!-- 插槽出口 --> </button> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Button = { template: '#btn' } const app = createApp({ components: { FancyButton: Button }, data () { return { message: 'hello slot render scope!' } } }) app.mount('#app') </script> </html></span></span>
3.12.3 默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton>
组件:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><button type="submit">
- <slot></slot>
- </button></span></span>
如果我们想在父组件没有提供任何插槽内容时在 <button>
内渲染“Submit”,只需要将“Submit”写在 <slot>
标签之间来作为默认内容:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><button type="submit">
- <slot>
- Submit <!-- 默认内容 -->
- </slot>
- </button></span></span>
现在,当我们在父组件中使用 <SubmitButton>
且没有提供任何插槽内容时:
<span style="background-color:#f8f8f8"><span style="color:#333333"><SubmitButton /></span></span>
“Submit”将会被作为默认内容渲染:
<span style="background-color:#f8f8f8"><span style="color:#333333"><button type="submit">Submit</button></span></span>
但如果我们提供了插槽内容:
<span style="background-color:#f8f8f8"><span style="color:#333333"><SubmitButton>Save</SubmitButton></span></span>
那么被显式提供的内容会取代默认内容:
<span style="background-color:#f8f8f8"><span style="color:#333333"><button type="submit">Save</button></span></span>
完整案例:44_slot_default.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>44_插槽默认内容</title> </head> <body> <div id="app"> <fancy-button> click me <!-- 插槽内容 --> </fancy-button> <fancy-button> 登录 <!-- 插槽内容 --> </fancy-button> <fancy-button></fancy-button> </div> </body> <template id="btn"> <button> <slot>按钮</slot> <!-- 插槽出口,给slot内写入内容作为插槽的默认值 --> </button> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Button = { template: '#btn' } const app = createApp({ components: { FancyButton: Button } }) app.mount('#app') </script> </html></span></span>
3.12.4 具名插槽(v-slot属性,#简写)
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout>
组件中,有如下模板:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><div class="container">
- <header>
- <!-- 标题内容放这里 -->
- </header>
- <main>
- <!-- 主要内容放这里 -->
- </main>
- <footer>
- <!-- 底部内容放这里 -->
- </footer>
- </div></span></span>
对于这种场景,<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><div class="container">
- <header>
- <slot name="header"></slot>
- </header>
- <main>
- <slot></slot>
- </main>
- <footer>
- <slot name="footer"></slot>
- </footer>
- </div></span></span>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
在父组件中使用 <BaseLayout>
时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><BaseLayout>
- <template v-slot:header>
- <!-- header 插槽的内容放这里 -->
- </template>
- </BaseLayout></span></span>
v-slot
有对应的简写#
,因此<template v-slot:header>
可以简写为<template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
完整案例:45_slot_name.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>45_具名插槽</title> </head> <body> <div id="app"> <base-layout> <template v-slot:header>首页头部</template> <template v-slot:default>首页内容</template> <template v-slot:footer>首页底部</template> </base-layout> <hr /> <base-layout> <template v-slot:default>分类内容</template> </base-layout> <hr /> <base-layout> <template #header>我的头部</template> <template #default>我的内容</template> <template #footer>我的底部</template> </base-layout> <!-- vue2.7 版本以前,现在不会走具名插槽 --> <base-layout> <div slot="header">vue2.6头部</div> <div slot="default">vue2.6内容</div> <div slot="footer">vue2.6底部</div> </base-layout> </div> </body> <template id="layout"> <div class="container"> <header class="header"> <slot name="header">header</slot> </header> <div class="content"> <!-- 不写name时,相当于 写了name="default" --> <slot>content</slot> </div> <footer class="footer"> <slot name="footer">footer</slot> </footer> </div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Layout = { template: '#layout' } const app = createApp({ components: { BaseLayout: Layout } }) app.mount('#app') </script> </html></span></span>
3.12.5 动态插槽名
动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名:
- <span style="background-color:#f8f8f8"><span style="color:#333333"><base-layout>
- <template v-slot:[dynamicSlotName]>
- ...
- </template>
-
- <!-- 缩写为 -->
- <template #[dynamicSlotName]>
- ...
- </template>
- </base-layout></span></span>
注意这里的表达式和动态指令参数受相同的语法限制。
完整案例:46_dynamic_slot_name.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>46_动态插槽名</title> </head> <body> <div id="app"> <button @click="count++">{{ count }}</button> <my-com> <template v-slot:[type]> {{ type }} 父组件默认值 </template> </my-com> </div> </body> <template id="com"> <div > <slot name="dynFirst"> 1 动态插槽名 子组件默认值 1</slot> <br /> <slot name="dynLast"> 2 动态插槽名 子组件默认值 2</slot> </div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Com = { template: '#com' } const app = createApp({ components: { MyCom: Com }, data () { return { count: 0 } }, computed: { type () { return this.count % 2 === 0 ? 'dynLast': 'dynFirst' } } }) app.mount('#app') </script> </html></span></span>
3.12.6 作用域插槽
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
完整案例:47_scope_slot.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>47_作用域插槽(配合具名插槽)</title> </head> <body> <div id="app"> <my-com> <!-- <template v-slot:test="testProps">作用域插槽以及具名插槽-{{testProps.message}}- {{ testProps.count }}</template> --> <!-- <template #test="testProps">作用域插槽以及具名插槽-{{testProps.message}}- {{ testProps.count }}</template> --> <template #test="{ message, count }">作用域插槽以及具名插槽-{{message}}- {{ count }}</template> </my-com> <my-com1> <!-- <template v-slot="slotProps">作用域插槽-{{slotProps.message}}- {{ slotProps.count }}</template> --> <!-- <template v-slot="{ message, count }">作用域插槽-{{message}}- {{ count }}</template> --> <!-- <template #="{ message, count }">作用域插槽-{{message}}- {{ count }}</template> --> <template #default="{ message, count }">作用域插槽-{{message}}- {{ count }}</template> </my-com1> </div> </body> <template id="com"> <div> <!-- 一旦使用了具名插槽,在使用作用域插槽时 v-slot:具名="属性名" --> <slot name="test" :message="message" :count="1"></slot> </div> </template> <template id="com1"> <div> <!-- 没有使用具名插槽,在使用作用域插槽时 v-slot="属性名" --> <slot :message="message" :count="1"></slot> </div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp } = Vue const Com = { template: '#com', data () { return { message: 'hello msg' } } } const Com1 = { template: '#com1', data () { return { message: 'hello msg1' } } } const app = createApp({ components: { MyCom: Com, MyCom1: Com1 } }) app.mount('#app') </script> </html></span></span>
3.12.7 $slots
一个表示父组件所传入插槽的对象。
通常用于手写渲染函数,但也可用于检测是否存在插槽。
每一个插槽都在 this.$slots
上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default
。
如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。
在渲染函数中,可以通过 this.$slots 来访问插槽:
完整案例:48_$slot.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>48_$slots渲染函数</title> </head> <body> <div id="app"> <my-com> <template #default>1111</template> <template #footer>2222</template> </my-com> </div> </body> <template id="com"> <div><slot>默认值</slot></div> <div><slot name="footer">底部默认值</slot></div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp, h } = Vue // h 代表创建一个元素 createElement const Com = { // template: '#com' render () { console.log(this.$slots) return [ h('div', { class: 'content'}, this.$slots.default()), // <div calss="content"><slot></slot></div> h('div', { class: 'footer'}, this.$slots.footer()) // <div class="footer"><slot name="footer"></slot></div> ] } } const app = createApp({ components: { MyCom: Com } }) app.mount('#app') </script> </html></span></span>
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
注意,虽然这里的 <Footer>
组件可能根本不关心这些 props,但为了使 <DeepChild>
能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
provide
和 inject
可以帮助我们解决这一问题。 [1] 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
3.13.1 provide
要为组件后代提供数据,需要使用到 provide 选项:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- provide: {
- message: 'hello!'
- }
- }</span></span>
对于 provide
对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。
如果我们需要提供依赖当前组件实例的状态 (比如那些由 data()
定义的数据属性),那么可以以函数形式使用 provide
:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- data() {
- return {
- message: 'hello!'
- }
- },
- provide() {
- // 使用函数的形式,可以访问到 `this`
- return {
- message: this.message
- }
- }
- }</span></span>
这不会使注入保持响应性(比如祖先组件中有一个count的状态,祖先组件修改完状态,后代组件默认的值没有响应式的改变)
3.13.2 inject
要注入上层组件提供的数据,需使用 inject 选项来声明:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- inject: ['message'],
- created() {
- console.log(this.message) // injected value
- }
- }</span></span>
注入会在组件自身的状态之前被解析,因此你可以在 data()
中访问到注入的属性:
- <span style="background-color:#f8f8f8"><span style="color:#333333">{
- inject: ['message'],
- data() {
- return {
- // 基于注入值的初始数据
- fullMessage: this.message
- }
- }
- }</span></span>
当以数组形式使用
inject
,注入的属性会以同名的 key 暴露到组件实例上。在上面的例子中,提供的属性名为"message"
,注入后以this.message
的形式暴露。访问的本地属性名和注入名是相同的。如果我们想要用一个不同的本地属性名注入该属性,我们需要在
inject
选项的属性上使用对象的形式:
<span style="background-color:#f8f8f8">{ inject: { /* 本地属性名 */ localMessage: { from: /* 注入来源名 */ 'message' } } }</span>这里,组件本地化了原注入名
"message"
所提供的的属性,并将其暴露为this.localMessage
。默认情况下,
inject
假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:
<span style="background-color:#f8f8f8">{ // 当声明注入的默认值时 // 必须使用对象形式 inject: { message: { from: 'message', // 当与原注入名同名时,这个属性是可选的 default: 'default value' }, user: { // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例 // 需要独立数据的,请使用工厂函数 default: () => ({ name: 'John' }) } } }</span>
完整案例:49_provide_inject.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>49_依赖注入</title> </head> <body> <div id="app"> <button @click="count++">加1</button> {{ count }} <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 - {{ message }} <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ myMessage }} - {{ count }} </div> </template> <script src="lib/vue.global.js"></script> <script> const Child = { template: '#child', inject: { myMessage: { // 替换本地名字, from: 'message' }, count: { default: 100 } } } const Parent = { template: '#parent', inject: ['message'], components: { MyChild: Child } } const { createApp } = Vue const app = createApp({ components: { MyParent: Parent }, data () { return { message: '传家宝', count: 1 } }, provide () { return { message: this.message, count: this.count } } }) app.mount('#app') </script> </html></span></span>
发现以上案例在count值发生改变时没有更新后代数据
3.13.3 配合响应性 computed()
为保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性
完整案例:50_provide_inject_computed_vue3.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>50_依赖注入配合响应式</title> </head> <body> <div id="app"> <button @click="count++">加1</button> {{ count }} <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 - {{ message }} <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ myMessage }} - {{ count }} </div> </template> <script src="lib/vue.global.js"></script> <script> const Child = { template: '#child', inject: { myMessage: { // 替换本地名字, from: 'message' }, count: { default: 100 } } } const Parent = { template: '#parent', inject: ['message'], components: { MyChild: Child } } const { createApp, computed } = Vue const app = createApp({ components: { MyParent: Parent }, data () { return { message: '传家宝', count: 1 } }, provide () { return { message: this.message, // count: this.count count: computed(() => this.count) // 响应式关键 } } }) app.mount('#app') </script> </html></span></span>
测试得知vue2中也是如此处理数据
50_provide_inject_computed_vue2.html
<span style="background-color:#f8f8f8"><!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>50_依赖注入配合响应式</title> </head> <body> <div id="app"> <button @click="count++">加1</button> {{ count }} <my-parent></my-parent> </div> </body> <template id="parent"> <div> 我是父组件 - {{ message }} <my-child></my-child> </div> </template> <template id="child"> <div> 我是子组件 - {{ myMessage }} - {{ count }} </div> </template> <script src="lib/vue.js"></script> <script> const Child = { template: '#child', inject: { myMessage: { // 替换本地名字, from: 'message' }, count: { default: 100 } } } const Parent = { template: '#parent', inject: ['message'], components: { MyChild: Child } } // console.log(Vue.computed) const { computed } = Vue new Vue({ components: { MyParent: Parent }, data: { message: '传家宝', count: 1 }, provide() { return { message: this.message, // count: this.count count: computed(() => this.count) // 响应式关键 } } }).$mount('#app') </script> </html></span>
有些场景会需要在两个组件间来回切换(Tab切换)
3.14.1 特殊 Attribute—is
用于绑定动态组件。
- <span style="background-color:#f8f8f8"><span style="color:#333333"><!-- currentTab 改变时组件也改变 -->
- <component :is="currentTab"></component></span></span>
完整案例:51_dynamic_component.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>51_动态组件</title> </head> <body> <div id="app"> <ul> <li @click="currentTab='Home'">首页</li> <li @click="currentTab='Kind'">分类</li> <li @click="currentTab='Cart'">购物车</li> </ul> <!-- 动态组件 --> <component :is="currentTab"></component> </div> </body> <script src="lib/vue.global.js"></script> <script> const Home = { template: `<div> 首页 <input /> </div>` } const Kind = { template: `<div> 分类 <input /> </div>` } const Cart = { template: `<div> 购物车 <input /> </div>` } Vue.createApp({ components: { Home, Kind, Cart }, data () { return { currentTab: 'Home' } } }).mount('#app') </script> </html></span></span>
如果此时给每个组件加入一个输入框,输入内容切换组件查看效果,发现切换回来数据不在
3.14.2 <KeepAlive>组件
缓存包裹在其中的动态切换组件
<KeepAlive>
包裹动态组件时,会缓存不活跃的组件实例,而不是销毁它们。
任何时候都只能有一个活跃组件实例作为 <KeepAlive>
的直接子节点。
完整案例:52_keep-alive.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>52_keep-alive</title> </head> <body> <div id="app"> <ul> <li @click="currentTab='Home'">首页</li> <li @click="currentTab='Kind'">分类</li> <li @click="currentTab='Cart'">购物车</li> </ul> <!-- 动态组件 --> <!-- 实际上is属性的值为组件的名字 --> <!-- 可以通过 keep-alive 保留组件的状态,避免组件的销毁和重建 --> <keep-alive> <component :is="currentTab"></component> </keep-alive> </div> </body> <script src="lib/vue.global.js"></script> <script> const Home = { template: `<div> 首页 <input /> </div>` } const Kind = { template: `<div> 分类 <input /> </div>` } const Cart = { template: `<div> 购物车 <input /> </div>` } Vue.createApp({ components: { Home, Kind, Cart }, data () { return { currentTab: 'Home' } } }).mount('#app') </script> </html></span></span>
当一个组件在
<KeepAlive>
中被切换时,它的activated
和deactivated
生命周期钩子将被调用,用来替代mounted
和unmounted
。这适用于<KeepAlive>
的直接子节点及其所有子孙节点。
3.14.3 activated、deactivated钩子
完整案例:53_activated_deacvidated.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>53_keep-alive钩子函数</title> </head> <body> <div id="app"> <ul> <li @click="currentTab='Home'">首页</li> <li @click="currentTab='Kind'">分类</li> <li @click="currentTab='Cart'">购物车</li> </ul> <!-- 动态组件 --> <!-- 实际上is属性的值为组件的名字 --> <!-- 可以通过 keep-alive 保留组件的状态,避免组件的销毁和重建 --> <keep-alive> <component :is="currentTab"></component> </keep-alive> </div> </body> <script src="lib/vue.global.js"></script> <script> // 假设首页面需要保留组件的状态,跳转到新的页面再回到首页时,要确保数据的请求的及时性时效性 // 以前 mounted 中请求了数据,使用keepalive时再回到首页 mounted 并没有执行 // 如果要获取最新的数据,请在activated中获取 // 假设首页长列表,点击某一项可以进入详情,返回首页之后,还希望滚动条在原来的位置 // 以前销毁组件时记录滚动条位置,是应用keep-alive之后,在deacvidated中记录滚动条位置 const Home = { template: `<div> 首页 <input /> </div>`, created () { console.log('首页 created') }, mounted () { console.log('首页 mounted') }, unmounted () { console.log('首页 unmounted') }, activated () { console.log('首页 activated') }, deactivated () { console.log('首页 deactivated') } } const Kind = { template: `<div> 分类 <input /> </div>`, created () { console.log('分类 created') }, mounted () { console.log('分类 mounted') }, unmounted () { console.log('分类 unmounted') }, activated () { console.log('分类 activated') }, deactivated () { console.log('分类 deactivated') } } const Cart = { template: `<div> 购物车 <input /> </div>`, created () { console.log('购物车 created') }, mounted () { console.log('购物车 mounted') }, unmounted () { console.log('购物车 unmounted') }, activated () { console.log('购物车 activated') }, deactivated () { console.log('购物车 deactivated') } } Vue.createApp({ components: { Home, Kind, Cart }, data () { return { currentTab: 'Home' } } }).mount('#app') </script> </html></span></span>
要不不缓存,要缓存都缓存了,这样不好
使用
include
/exclude
可以设置哪些组件被缓存,使用max
可以设定最多缓存多少个
<span style="background-color:#f8f8f8"><!-- 用逗号分隔的字符串 --> <KeepAlive include="a,b"> <component :is="view"></component> </KeepAlive> <!-- 正则表达式 (使用 `v-bind`) --> <KeepAlive :include="/a|b/"> <component :is="view"></component> </KeepAlive> <!-- 数组 (使用 `v-bind`) --> <KeepAlive :include="['a', 'b']"> <component :is="view"></component> </KeepAlive></span>组件如果想要条件性地被
KeepAlive
缓存,就必须显式声明一个name
选项。完整案例:
54_keep_alive_include.html
<span style="background-color:#f8f8f8"><!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>54_选择性缓存</title> </head> <body> <div id="app"> <ul> <li @click="currentTab='Home'">首页</li> <li @click="currentTab='Kind'">分类</li> <li @click="currentTab='Cart'">购物车</li> </ul> <!-- 动态组件 --> <!-- 实际上is属性的值为组件的名字 --> <!-- 可以通过 keep-alive 保留组件的状态,避免组件的销毁和重建 --> <!-- 用逗号分隔的字符串 注意不要加空格 --> <!-- <keep-alive include="home,kind"> --> <!-- 正则表达式 (使用 `v-bind`) 使用绑定属性 --> <!-- <keep-alive :include="/home|kind/"> --> <!-- 数组 (使用 `v-bind`) --> <keep-alive :include="['home', 'kind']"> <component :is="currentTab"></component> </keep-alive> </div> </body> <script src="lib/vue.global.js"></script> <script> // 假设首页面需要保留组件的状态,跳转到新的页面再回到首页时,要确保数据的请求的及时性时效性 // 以前 mounted 中请求了数据,使用keepalive时再回到首页 mounted 并没有执行 // 如果要获取最新的数据,请在activated中获取 // 假设首页长列表,点击某一项可以进入详情,返回首页之后,还希望滚动条在原来的位置 // 以前销毁组件时记录滚动条位置,是应用keep-alive之后,在deacvidated中记录滚动条位置 const Home = { name: 'home', template: `<div> 首页 <input /> </div>`, created() { console.log('首页 created') }, mounted() { console.log('首页 mounted') }, unmounted() { console.log('首页 unmounted') }, activated() { console.log('首页 activated') }, deactivated() { console.log('首页 deactivated') } } const Kind = { name: 'kind', template: `<div> 分类 <input /> </div>`, created() { console.log('分类 created') }, mounted() { console.log('分类 mounted') }, unmounted() { console.log('分类 unmounted') }, activated() { console.log('分类 activated') }, deactivated() { console.log('分类 deactivated') } } const Cart = { name: 'cart', template: `<div> 购物车 <input /> </div>`, created() { console.log('购物车 created') }, mounted() { console.log('购物车 mounted') }, unmounted() { console.log('购物车 unmounted') }, activated() { console.log('购物车 activated') }, deactivated() { console.log('购物车 deactivated') } } Vue.createApp({ components: { Home, Kind, Cart }, data() { return { currentTab: 'Home' } } }).mount('#app') </script> </html></span>
最大缓存实例数
我们可以通过传入 max
prop 来限制可被缓存的最大组件实例数。<KeepAlive>
的行为在指定了 max
后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
- <span style="background-color:#f8f8f8"><span style="color:#333333"><KeepAlive :max="10">
- <component :is="activeComponent" />
- </KeepAlive></span></span>
<component>
元素一个用于渲染动态组件或元素的“元组件”
完整案例:55_component_element.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>55_component元素</title> </head> <body> <div id="app"> <input type="checkbox" v-model="flag" /> <!-- 条件为真渲染为 a 标签,否则为 span 标签 --> <component :is="flag ? 'a' : 'span'">你好</component> <component :is="flag ? 'my-com1' : 'my-com2'"></component> </div> </body> <script src="lib/vue.global.js"></script> <script> const Com1 = { template: `<div>com1</div>` } const Com2 = { template: `<div>com2</div>` } Vue.createApp({ components: { MyCom1: Com1, MyCom2: Com2 }, data () { return { flag: false } } }).mount('#app') </script> </html></span></span>
也可以渲染组件
当 is
attribute 用于原生 HTML 元素时,它将被当作 Customized built-in element,其为原生 web 平台的特性。
但是,在这种用例中,你可能需要 Vue 用其组件来替换原生元素,如 DOM 模板解析注意事项所述。你可以在 is
attribute 的值中加上 vue:
前缀,这样 Vue 就会把该元素渲染为 Vue 组件(my-row-component
为自定义组件):
- <span style="background-color:#f8f8f8"><span style="color:#333333"><table>
- <tr is="vue:my-row-component"></tr>
- </table></span></span>
完整案例:56_DOM模板解析注意事项.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>56_DOM模板解析注意事项</title> </head> <body> <div id="app"> <table> <tr> <td>序号</td> <td>姓名</td> <td>密码</td> </tr> <!-- <my-tr :list="list"></my-tr> --> <!-- Vue 用其组件来替换原生元素 --> <tr is="vue:my-tr" :list="list"></tr> </table> </div> </body> <template id="tr"> <tr v-for="item of list" :key="item.id"> <td>{{ item.id }}</td> <td>{{ item.name }}</td> <td>{{ item.password }}</td> </tr> </template> <script src="lib/vue.global.js"></script> <script> const Tr = { props: ['list'], template: '#tr' } Vue.createApp({ components: { MyTr: Tr }, data () { return { list: [ { id: 1, name: '张三', password: '123' }, { id: 2, name: '李四', password: '234' }, { id: 3, name: '王五', password: '345' } ] } } }).mount('#app') </script> </html></span></span>
注意不要使用绑定属性
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能
3.17.1 全局API
学习:defineAsyncComponent()
- <span style="background-color:#f8f8f8"><span style="color:#333333">import { defineAsyncComponent } from 'vue'
-
- const AsyncComp = defineAsyncComponent(() => {
- return new Promise((resolve, reject) => {
- // ...从服务器获取组件
- resolve(/* 获取到的组件 */)
- })
- })
- // ... 像使用其他一般组件一样使用 `AsyncComp`</span></span>
完整案例:57_defineAsyncComponent.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>57_定义异步组件</title> </head> <body> <div id="app"> <my-test></my-test> <hr/> <my-com></my-com> </div> </body> <template id="com"> <div>异步加载组件</div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp, defineAsyncComponent } = Vue const Com = { template: '#com' } const MyCom = defineAsyncComponent(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(Com) }, 3000) }) }) createApp({ components: { MyCom, MyTest: Com } }).mount('#app') </script> </html></span></span>
3.17.2 加载函数
学习:() => import()
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件
- <span style="background-color:#f8f8f8"><span style="color:#333333">import { defineAsyncComponent } from 'vue'
-
- const AsyncComp = defineAsyncComponent(() =>
- import('./components/MyComponent.vue')
- )</span></span>
以后讲解项目时可以用到,需要在脚手架环境中使用(单文件组件中使用
)
3.18.3 <Suspense>
组件
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
vue3中新增的
完整案例:58_Suspense.html
<span style="background-color:#f8f8f8"><span style="color:#333333"><!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>58_Suspense</title> </head> <body> <div id="app"> <my-test></my-test> <hr/> <!-- Suspense内部需要一个根组件 --> <Suspense> <!-- 异步加载组件 --> <my-com></my-com> <!-- 加载状态 --> <template #fallback> 加载中... </template> </Suspense> </div> </body> <template id="com"> <div>异步加载组件</div> </template> <script src="lib/vue.global.js"></script> <script> const { createApp, defineAsyncComponent } = Vue const Com = { template: '#com' } const MyCom = defineAsyncComponent(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(Com) }, 3000) }) }) createApp({ components: { MyCom, MyTest: Com } }).mount('#app') </script> </html></span></span>
后期可以和
Transition
,KeepAlive
,路由
等结合使用
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。