6.组件间通信
组件有 分治 的特点,每个组件之间具有一定的独立性,但是在实际工作中使用组件的时候有互相之间传递数据的需求,此时就得考虑如何进行 组件间传值 的问题了。
完整案例:05_component/28_parent_child_component.html
父子组件
<!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>父子组件</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<my-child></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child'
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.1 Prop
学习:状态选项props 以及 实例属性 $attrs
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute
props 需要使用 props
选项来定义:
{
props: ['foo'],
created() {
// props 会暴露到 `this` 上
console.log(this.foo)
}
}
除了使用字符串数组来声明 prop 外,还可以使用对象的形式:
{
props: {
title: String,
likes: Number
}
}
对于以对象形式声明中的每个属性,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 的书写风格-短横线。
所有的 props 都遵循着单向绑定原则(
单项数据流
),props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。
6.1.1 父组件给子组件传值1
完整案例:05_component/29_parent_child_component_value1.html
<!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>父组件给子组件传值1</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<!--
父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
对象,数组,null,undefined,正则,需要使用绑定属性
-->
<my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<div>{{ msg }}</div>
<div>{{ flag }}</div>
<div>{{ num }}</div>
<div>{{ obj }}</div>
<div>{{ arr }}</div>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// 在定义子组件的地方,添加一个 props 选项
// 方式1
// props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
const Child = {
template: '#child',
props: ['msg', 'flag', 'num', 'obj', 'arr']
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
data () {
return {
msg: 'hello vue'
}
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
虽然上述案例已经完成了父组件给子组件传值,但是不够严谨
可能A负责父组件的编写,B负责了子组件的编写,容易造成 不知道 自定义的属性名的 数据类型
6.1.2 父组件给子组件传值2
完整案例:05_component/30_parent_child_component_value2.html
<!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>父组件给子组件传值2</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<!--
父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
对象,数组,null,undefined,正则,需要使用绑定属性
-->
<my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<div>{{ msg }}</div>
<div>{{ flag }}</div>
<div>{{ num }}</div>
<div>{{ obj }}</div>
<div>{{ arr }}</div>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// 在定义子组件的地方,添加一个 props 选项
// 方式1
// props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
// 方式2
// props 书写为一个对象,对象的key值为自定义的属性名,value值为 数据类型
const Child = {
template: '#child',
// props: ['msg', 'flag', 'num', 'obj', 'arr']
props: {
msg: String,
flag: Boolean,
num: Number,
obj: Object,
arr: Array
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
data () {
return {
msg: 'hello vue'
}
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
现在只能知道哪一个属性是哪一种数据类型,但是有时候我们可以不需要设置 自定义的属性(. )
<input />
<===><input type="text" />
6.1.3 父组件给子组件传值3
完整案例: 05_component/31_parent_child_component_value3.html
<!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>父组件给子组件传值3</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<!--
父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
对象,数组,null,undefined,正则,需要使用绑定属性
-->
<my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
<my-child :obj="{b:2}" ></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<div>{{ msg }}</div>
<div>{{ flag }}</div>
<div>{{ num }}</div>
<div>{{ obj }}</div>
<div>{{ arr }}</div>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// 在定义子组件的地方,添加一个 props 选项
// 方式1
// props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
// 方式2
// props 书写为一个对象,对象的key值为自定义的属性名,value值为 数据类型
// 方式3
// props 书写为一个对象
// 对象的key值为 自定义的属性名,
// value值又为一个对象
// 对象的key值为 type, 表示需要传递数据的 数据类型
// 对象的key值为 default,表示数据的默认值, 如果数据默认值为 对象和数组,需要通过函数返回
// 对象的key值为 required, 表示该数据是必须传递的项
// 对象的key值为 validator, 表示需要自定义验证规则
// 如果一个属性的值既可以是 String,也可以是number类型,通过 || 分割,或者数组书写
const Child = {
template: '#child',
// props: ['msg', 'flag', 'num', 'obj', 'arr']
// props: {
// msg: String,
// flag: Boolean,
// num: Number,
// obj: Object,
// arr: Array
// }
props: {
msg: {
type: String
},
flag: {
type: Boolean,
default: true
},
num: {
// type: Number || String,
// type: [Number, String],
type: Number,
default: 100000,
validator: (value) => {
return value > 20000
}
},
obj: {
required: true,
type: Object,
default () {
return { a: '1111' }
}
},
arr: {
type: Array,
default () {
return [4, 5, 6]
}
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
data () {
return {
msg: 'hello vue'
}
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.1.4 $attrs
一个包含了组件所有透传 attributes 的对象。
透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。
<my-button class="btn"></my-button>
子组件模板只有button,那么my-button的class直接透传给子组件
<button class="btn"></button>
默认情况下,若是单一根节点组件,$attrs
中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置 inheritAttrs
选项来显式地关闭该行为。
6.2 监听事件
学习:状态选项emits、实例方法 $emit
6.2.1 子组件给父组件传值- $emit
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件
$emit()
方法在组件实例上也同样以 this.$emit()
的形式可用
父组件可以通过 v-on
(缩写为 @
) 来监听事件
同样,组件的事件监听器也支持 .once
修饰符
像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
完整案例:05_component/32_child_parent_component_value1.html
<!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>子组件给父组件传值1</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<!--
父组件调用子组件的地方,绑定一个自定义的事件
该事件由父组件定义,默认参数就是子组件传递给父组件的值
-->
<my-child v-on:my-event="getData"></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<button @click="sendData">传值2000</button>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// 在子组件的某一个事件内部,通过 this.$emit('自定义事件名', 参数)完成传递
const Child = {
template: '#child',
mounted () {
this.$emit('my-event', 1000)
},
methods: {
sendData () {
this.$emit('my-event', 2000)
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
methods: {
getData (val) {
console.log(val)
}
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.2.2 子组件给父组件传值-声明触发的事件
组件要触发的事件可以显式地通过 emits
选项来声明:
{
emits: ['inFocus', 'submit']
}
这个 emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
{
emits: {
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
}
}
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit
的内容,返回一个布尔值来表明事件是否合法。
{
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 })
}
}
}
完整案例:05_component/33_child_parent_component_value2.html
<!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>子组件给父组件传值1</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<!--
父组件调用子组件的地方,绑定一个自定义的事件
该事件由父组件定义,默认参数就是子组件传递给父组件的值
-->
<my-child v-on:my-event="getData"></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<button @click="sendData">传值2000</button>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// 在子组件的某一个事件内部,通过 this.$emit('自定义事件名', 参数)完成传递
const Child = {
template: '#child',
// 声明触发的事件并且验证
// emits: ['my-event'], // 声明触发的事件
emits: { // 声明触发的事件并且验证
'my-event': (payload) => {
return payload > 500
}
},
mounted () {
this.$emit('my-event', 1000)
},
methods: {
sendData () {
this.$emit('my-event', 2000)
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
methods: {
getData (val) {
console.log(val)
}
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.2.3 自定义表单组件使用v-model
自定义事件可以用于开发支持 v-model
的自定义表单组件
<CustomInput>
组件内部需要做两件事:
- 将内部原生
input
元素的value
attribute 绑定到modelValue
prop - 输入新的值时在
input
元素上触发update:modelValue
事件
完整案例:05_component/34_custom_form_v-model.html
<!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>vue3-自定义组件使用 v-model</title>
</head>
<body>
<div id="app">
<my-input
type="text"
v-model="userName"
placeholder="用户名"
></my-input> {{ userName }}
<my-input
type="password"
v-model="password"
placeholder="密码"
></my-input> {{ password }}
</div>
</body>
<template id="input">
<div>
<input
:type="type"
:placeholder="placeholder"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
// v-model 用到自定义组件上,就相当于
// 父组件在调用子组件时 给子组件传了一个 叫 modelValue 的属性
// 同时 也相当于 父组件直接给子组件绑定了一个 叫 update:modelValue 的事件
// 在子组件模版中 将 input 的value的值赋值给 modelValue,
// 给input绑定一个事件,事件提交 update:modelValue
// vue2中自定义表单组件,父传给子 value 属性,事件为 input 事件
const Input = {
template: '#input',
emits: ['update:modelValue'],
props: {
type: {
type: String,
default: 'text'
},
placeholder: {
type: String
},
modelValue: {
type: String
}
}
}
Vue.createApp({
data () {
return {
userName: '',
password: ''
}
},
components: {
MyInput: Input
}
}).mount('#app')
</script>
</html>
另一种在组件内实现 v-model
的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get
方法需返回 modelValue
prop,而 set
方法需触发相应的事件:
const Input = {
template: `#input`,
props: ['modelValue'],
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
}
6.2.4 多个v-model的绑定
v-model
的参数
仅限于 vue3
完整案例:05_component/35_custom_form_v-model_params.html
<!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>一个组件使用多个v-model,仅限于vue3</title>
</head>
<body>
<div id="app">
<my-form
v-model:username = "username"
v-model:password = "password"
></my-form> {{ username }} -- {{ password }}
</div>
</body>
<template id="form">
<form>
<div>
<input type="text" placeholder="用户名" :value="username" @input="$emit('update:username', $event.target.value)"/>
</div>
<div>
<input type="password" placeholder="密码" :value="password" @input="$emit('update:password', $event.target.value)"/>
</div>
<button>登录</button>
</form>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Form = {
template: '#form',
emits: ['update:username', 'update:password'],
props: {
username: String,
password: String
}
}
Vue.createApp({
data () {
return {
username: '',
password: ''
}
},
components: {
MyForm: Form
}
}).mount('#app')
</script>
</html>
6.3 透传Attribute
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
6.3.1 attribute继承
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton>
组件,它的模板长这样:
<!-- <MyButton> 的模板 -->
<button>click me</button>
一个父组件使用了这个组件,并且传入了 class
:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click me</button>
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
6.3.2 对 class
和 style
的合并
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton>
组件的模板改成这样:
<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
则最后渲染出的 DOM 结果会变成:
<button class="btn large">click me</button>
6.3.3 v-on
监听器继承
同样的规则也适用于 v-on
事件监听器:
<MyButton @click="onClick" />
click
监听器会被添加到 <MyButton>
的根元素,即那个原生的 <button>
元素之上。当原生的 <button>
被点击,会触发父组件的 onClick
方法。同样的,如果原生 button
元素自身也通过 v-on
绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
6.3.4 禁用 Attributes 继承
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
。
最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs
选项为 false
,你可以完全控制透传进来的 attribute 被如何使用。
6.3.5 多根节点的 Attributes 继承
$attrs
被显式绑定
完整案例: 05_component/36_attribute_transmission.html
<!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>透传</title>
</head>
<body>
<div id="app">
{{ count }}
<my-button class="large" @click="count++"></my-button>
</div>
</body>
<script src="../lib/vue.global.js"></script>
<template id="btn">
<button class="btn">按钮</button>
</template>
<script>
Vue.createApp({
data () {
return {
count: 10
}
},
components: {
MyButton: { template: '#btn', inheritAttrs: false }
}
}).mount('#app')
</script>
</html>
以下案例仅供参考
<!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 >测试</div>
</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>
6.4 特殊Attribute -ref
学习:实例属性refs
用于注册模板引用。
ref
用于注册元素或子组件的引用。
使用选项式 API,引用将被注册在组件的 this.$refs
对象里
放在DOM元素上,获取DOM节点,放到组件上,获取子组件的实例,可以直接使用子组件的属性和方法
完整案例:05_component/37_attribute_ref.html
<!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>父子组件</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<my-child ref="child"></my-child>
<div ref="oDiv">DOM操作</div>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child',
data () {
return {
msg: 'child 在此'
}
},
methods: {
fn () {
console.log('子组件的方法被调用')
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
},
mounted () {
console.log(this.$refs)
console.log(this.$refs.child.msg)
this.$refs.child.fn()
console.log(this.$refs.oDiv.innerHTML)
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.5 $parent
当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为 null
。
完整案例05_component/38_parent.html
<!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>父子组件</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<my-child></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
{{ $parent.msg }}
<button @click="$parent.fn()">点击</button>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child'
}
const Parent = {
template: '#parent',
data () {
return {
msg: '父组件在此'
}
},
methods: {
fn () {
console.log('父组件的方法被调用')
}
},
components: {
'my-child': Child
}
}
Vue.createApp({
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.6$root
当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。
完整案例:05_component/39_root.html
<!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>父子组件</title>
</head>
<body>
<div id="app">
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件 - {{ $root.msg }}</h1>
<my-child></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件 - {{ $root.msg }}</h3>
<button @click="$root.fn()">按钮</button>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child'
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
}
}
Vue.createApp({
data () {
return {
msg: 'root'
}
},
methods: {
fn () {
console.log('root 被调用')
}
},
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
6.7 非父子组件传值
兄弟组件传值 - 中央事件总线传值 ---- vue2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVk6Gte8-1673405502848)(assets/image-20220916160921781.png)]
完整案例:05_component/40_brother_value-vue2.html
<!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>vue2兄弟组件传值</title>
</head>
<body>
<div id="app">
<my-content></my-content>
<my-footer></my-footer>
</div>
</body>
<template id="content">
<div>当前页面是:{{ page }}</div>
</template>
<template id="footer">
<ul>
<li @click="sendData('首页')">首页</li>
<li @click="sendData('分类')">分类</li>
<li @click="sendData('购物车')">购物车</li>
<li @click="sendData('我的')">我的</li>
</ul>
</template>
<script src="../lib/vue.js"></script>
<script>
// new Vue 实例作为中央事件总线 eventBus
// 通过 eventBus.$emit('自定义事件', 数据) 传值
// 通过 eventBus.$on('自定义事件', 回调函数)接收值,回调函数参数就是传递过来的值
const eventBus = new Vue()
const Content = {
template: '#content',
data () {
return {
page: '首页'
}
},
mounted () {
// 在此处接收数据
eventBus.$on('my-event', (val) => {
this.page = val
})
}
}
const Footer = {
template: '#footer',
methods: {
sendData (val) {
eventBus.$emit('my-event', val)
}
}
}
new Vue({
components: {
MyContent: Content,
MyFooter: Footer
}
}).$mount('#app')
</script>
</html>
vue3中没有明确的兄弟组件传值的方案,可以使用状态提升(找到这两个组件共同的父级组件,然后通过父与子之间的传值实现)
完整案例:05_component/41_brother_value-vue3.html
<!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>vue3兄弟组件传值-状态提升</title>
</head>
<body>
<div id="app">
<my-content :page="page"></my-content>
<my-footer @change-page="changePage"></my-footer>
</div>
</body>
<template id="content">
<div>当前页面是:{{ page }}</div>
</template>
<template id="footer">
<ul>
<li @click="sendData('首页')">首页</li>
<li @click="sendData('分类')">分类</li>
<li @click="sendData('购物车')">购物车</li>
<li @click="sendData('我的')">我的</li>
</ul>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Content = {
template: '#content',
props: {
page: String
}
}
const Footer = {
template: '#footer',
methods: {
sendData (val) {
this.$emit('change-page', val)
}
}
}
Vue.createApp({
data () {
return {
page: '首页'
}
},
methods: {
changePage (val) {
this.page = val
}
},
components: {
MyContent: Content,
MyFooter: Footer
}
}).mount('#app')
</script>
</html>
7.插槽
组件的最大特性就是 重用 ,而用好插槽能大大提高组件的可重用能力。
**插槽的作用:**父组件向子组件传递内容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BSuWuCaV-1673405502849)(assets/34.png)]
通俗的来讲,插槽无非就是在 子组件 中挖个坑,坑里面放什么东西由 父组件 决定。
插槽类型有:
- 单个(匿名)插槽
- 具名插槽
- 作用域插槽
7.1 - 插槽内容与插口
在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
这里有一个 <FancyButton>
组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton>
的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZfykBWj-1673405502849)(assets/slots.dbdaf1e8.png)]
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
完整案例:06_slot/42_slot.html
<!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>插槽</title>
</head>
<body>
<div id="app">
<!-- 自定义组件标签调用
默认情况下 内部代码是无法被解析的
可以在定义组件的模版时,通过 slot 标签显示内容
-->
<fancy-button>点击</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 FancyButton = {
template: '#btn'
}
Vue.createApp({
components: {
FancyButton
}
}).mount('#app')
</script>
</html>
7.2渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }}
插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:
父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
完整案例:06_slot/43_slot_render_scope.html
<!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>插槽</title>
</head>
<body>
<div id="app">
<!-- 自定义组件标签调用
默认情况下 内部代码是无法被解析的
可以在定义组件的模版时,通过 slot 标签显示内容
-->
<span>{{ msg }}</span>
<fancy-button>{{ msg }}</fancy-button>
</div>
</body>
<template id="btn">
<button>
按钮 - <slot></slot>
</button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const FancyButton = {
template: '#btn',
data () {
return {
msg: '注册'
}
}
}
Vue.createApp({
data () {
return {
msg: '登录'
}
},
components: {
FancyButton
}
}).mount('#app')
</script>
</html>
7.3默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton>
组件:
<button type="submit">
<slot></slot>
</button>
如果我们想在父组件没有提供任何插槽内容时在 <button>
内渲染“Submit”,只需要将“Submit”写在 <slot>
标签之间来作为默认内容:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
现在,当我们在父组件中使用 <SubmitButton>
且没有提供任何插槽内容时:
<SubmitButton />
“Submit”将会被作为默认内容渲染:
<button type="submit">Submit</button>
但如果我们提供了插槽内容:
<SubmitButton>Save</SubmitButton>
那么被显式提供的内容会取代默认内容:
<button type="submit">Save</button>
完整案例:06_slot/44_slot_default.html
<!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>插槽</title>
</head>
<body>
<div id="app">
<!-- 自定义组件标签调用
默认情况下 内部代码是无法被解析的
可以在定义组件的模版时,通过 slot 标签显示内容
-->
<fancy-button>点击</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 FancyButton = {
template: '#btn'
}
Vue.createApp({
components: {
FancyButton
}
}).mount('#app')
</script>
</html>
7.4具名插槽(v-slot属性,#简写)
有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout>
组件中,有如下模板:
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
对于这种场景,<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
在父组件中使用 <BaseLayout>
时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写#
,因此<template v-slot:header>
可以简写为<template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ji7v0Okf-1673405502850)(assets/named-slots.ebb7b207.png)]
完整案例:06_slot/45_slot_name.html
<!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>具名插槽</title>
</head>
<body>
<div id="app">
<base-layout>
<!-- vue2可以这样写 -->
<!-- <div slot="header">首页头部</div>
<div>首页内容</div>
<div slot="footer">首页底部</div> -->
<!-- vue3 使用 v-slot指令 -->
<template v-slot:header>
<div>首页头部</div>
</template>
<div>首页内容</div>
<template v-slot:footer>
<div >首页底部</div>
</template>
</base-layout>
<base-layout>
<!-- v-slot:header 可简写为 #header -->
<template #header>
<div>分类头部</div>
</template>
<!-- 默认内容要写 template 就写 v-slot:default 或者 #default -->
<template #default>
<div>分类内容</div>
</template>
<template #footer>
<div>分类底部</div>
</template>
</base-layout>
</div>
</body>
<template id="layout">
<div class="container">
<header class="header">
<slot name="header">头部</slot>
</header>
<div class="content">
<slot>内容</slot>
</div>
<footer class="footer">
<slot name="footer">底部</slot>
</footer>
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const BaseLayout = {
template: '#layout'
}
Vue.createApp({
components: {
BaseLayout
}
}).mount('#app')
</script>
</html>
7.5动态插槽名 - 了解
动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
注意这里的表达式和动态指令参数受相同的语法限制。
完整案例:06_slot/46_dynamic_slot_name.html
<!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="dan"> 1 动态插槽名 子组件默认值 1</slot>
<br />
<slot name="shaung"> 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 ? 'shaung': 'dan'
}
}
})
app.mount('#app')
</script>
</html>
7.6作用域插槽-了解
在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cxFkquO-1673405514873)(null)]
完整案例:06_slot/47_scope_slot.html
<!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>插槽</title>
</head>
<body>
<div id="app">
<!--
插槽的内容如果既想要访问父组件域内数据 msg,也想要访问子组件域内的数据 str num
在定义插槽的时候给slot 添加了 str 以及 num的自定义属性
在调用组件的时候 通过 v-slot 给组件添加 slotProps 的值
该值将包含子组件域内的数据,以对象的形式存在
-->
<fancy-button v-slot="slotProps">
{{ msg }} -- {{ slotProps.str }} -- {{ slotProps.num }}
</fancy-button>
</div>
</body>
<template id="btn">
<button>
按钮 - <slot :str="str" :num="10000"></slot>
</button>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const FancyButton = {
template: '#btn',
data () {
return {
str: '注册'
}
}
}
Vue.createApp({
data () {
return {
msg: '登录'
}
},
components: {
FancyButton
}
}).mount('#app')
</script>
</html>
7.7 $slots - 了解
一个表示父组件所传入插槽的对象。
通常用于手写渲染函数,但也可用于检测是否存在插槽。
每一个插槽都在 this.$slots
上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default
。
如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。
在渲染函数中,可以通过 this.$slots 来访问插槽:
完整案例:06_slot/48_$slot.html
<!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 class="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>
vue2渲染函数参照
<!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>vue2渲染函数</title>
</head>
<body>
<div id="app">
<my-com></my-com>
</div>
</body>
<template id="com">
<div>com</div>
</template>
<template id="test">
<div>test</div>
</template>
<script src="../lib/vue.js"></script>
<script>
const Com = {
template: '#com'
}
const Test = {
template: '#test'
}
// new Vue({
// el: '#app',
// components: {
// MyCom: Com
// }
// })
// new Vue({
// components: {
// MyCom: Com
// }
// }).$mount('#app')
new Vue({
// render: (h) => h(Com)
render: (createElement) => createElement('div', {}, [ createElement(Com), createElement(Test)])
}).$mount('#app')
</script>
</html>
8.依赖注入
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3CbFygiU-1673405502851)(assets/prop-drilling.11201220.png)]
注意,虽然这里的 <Footer>
组件可能根本不关心这些 props,但为了使 <DeepChild>
能访问到它们,仍然需要定义并向下传递。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”,显然是我们希望尽量避免的情况。
provide
和 inject
可以帮助我们解决这一问题。 [1] 一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCLa1ypq-1673405502851)(assets/provide-inject.3e0505e4.png)]
8.1 provide
要为组件后代提供数据,需要使用到 provide
选项:
{
provide: {
message: 'hello!'
}
}
对于 provide
对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。
如果我们需要提供依赖当前组件实例的状态 (比如那些由 data()
定义的数据属性),那么可以以函数形式使用 provide
:
{
data() {
return {
message: 'hello!'
}
},
provide() {
// 使用函数的形式,可以访问到 `this`
return {
message: this.message
}
}
}
这不会使注入保持响应性(比如祖先组件中有一个count的状态,祖先组件修改完状态,后代组件默认的值没有响应式的改变)
8.2 inject
要注入上层组件提供的数据,需使用 inject
选项来声明:
{
inject: ['message'],
created() {
console.log(this.message) // injected value
}
}
注入会在组件自身的状态之前被解析,因此你可以在 data()
中访问到注入的属性:
{
inject: ['message'],
data() {
return {
// 基于注入值的初始数据
fullMessage: this.message
}
}
}
当以数组形式使用
inject
,注入的属性会以同名的 key 暴露到组件实例上。在上面的例子中,提供的属性名为"message"
,注入后以this.message
的形式暴露。访问的本地属性名和注入名是相同的。如果我们想要用一个不同的本地属性名注入该属性,我们需要在
inject
选项的属性上使用对象的形式:{ inject: { /* 本地属性名 */ localMessage: { from: /* 注入来源名 */ 'message' } } }
这里,组件本地化了原注入名
"message"
所提供的的属性,并将其暴露为this.localMessage
。默认情况下,
inject
假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:
{ // 当声明注入的默认值时 // 必须使用对象形式 inject: { message: { from: 'message', // 当与原注入名同名时,这个属性是可选的 default: 'default value' }, user: { // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例 // 需要独立数据的,请使用工厂函数 default: () => ({ name: 'John' }) } } }
完整案例:07_provide/49_provide_inject.html
<!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>依赖注入</title>
</head>
<body>
<div id="app">
<button @click="count++">{{count}}</button>
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<my-child></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<!-- {{ msg }} -- {{ count }} -->
{{ mymsg }} -- {{ mycount }} -- {{ mytest }}
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child',
// inject: ['msg', 'count']
inject: {
mymsg: {
from: 'msg'
},
mycount: {
from: 'count'
},
mytest: {
default: '传家宝'
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
}
}
Vue.createApp({
data () {
return {
message: 'hello',
count: 100
}
},
provide () {
// 使用函数的形式,可以访问到 `this`
return {
msg: this.message,
count: this.count
}
},
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
发现以上案例在count值发生改变时没有更新后代数据
8.3 配合响应性 computed()
为保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性
完整案例:07_provide/50_provide_inject_computed_vue3.html
<!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>依赖注入</title>
</head>
<body>
<div id="app">
<button @click="count++">{{count}}</button>
<my-parent></my-parent>
</div>
</body>
<template id="parent">
<div>
<h1>父组件</h1>
<my-child></my-child>
</div>
</template>
<template id="child">
<div>
<h3>子组件</h3>
<!-- {{ msg }} -- {{ count }} -->
{{ mymsg }} -- {{ mycount }} -- {{ mytest }}
</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Child = {
template: '#child',
// inject: ['msg', 'count']
inject: {
mymsg: {
from: 'msg'
},
mycount: {
from: 'count'
},
mytest: {
default: '传家宝'
}
}
}
const Parent = {
template: '#parent',
components: {
'my-child': Child
}
}
const { computed } = Vue
Vue.createApp({
data () {
return {
message: 'hello',
count: 100
}
},
provide () {
// 使用函数的形式,可以访问到 `this`
return {
msg: this.message,
count: computed(() => this.count) // 确保响应式
}
},
components: {
'my-parent': Parent
}
}).mount('#app')
</script>
</html>
测试得知vue2中也是如此处理数据
9.动态组件
有些场景会需要在两个组件间来回切换(Tab切换)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hETBTrCW-1673405502852)(assets/image-20220919113443278.png)]
9.1特殊 Attribute—is
用于绑定动态组件。
<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>
完整案例:08_dynamic/51_dynamic_component.html
<!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>动态组件</title>
</head>
<body>
<div id="app">
<ul>
<li @click="currentTab='Home'">首页</li>
<li @click="currentTab='Kind'">分类</li>
<li @click="currentTab='Cart'">购物车</li>
<li @click="currentTab='User'">我的</li>
</ul>
<component :is="currentTab"></component>
</div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
const Home = {
template: `
<div>
首页
</div>
`
}
const Kind = {
template: `
<div>
分类
</div>
`
}
const Cart = {
template: `
<div>
购物车
</div>
`
}
const User = {
template: `
<div>
我的
</div>
`
}
Vue.createApp({
data () {
return {
currentTab: 'Home'
}
},
components: {
Home,
Kind,
Cart,
User
}
}).mount('#app')
</script>
</html>
如果此时给每个组件加入一个输入框,输入内容切换组件查看效果,发现切换回来数据不在
9.2 <KeepAlive>
组件
缓存包裹在其中的动态切换组件
<KeepAlive>
包裹动态组件时,会缓存不活跃的组件实例,而不是销毁它们。
任何时候都只能有一个活跃组件实例作为 <KeepAlive>
的直接子节点。
完整案例:08_dynamic/52_keep-alive.html
<!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>动态组件</title>
</head>
<body>
<div id="app">
<ul>
<li @click="currentTab='Home'">首页</li>
<li @click="currentTab='Kind'">分类</li>
<li @click="currentTab='Cart'">购物车</li>
<li @click="currentTab='User'">我的</li>
</ul>
<!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 -->
<!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 -->
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
</div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
const Home = {
template: `
<div>
首页 <input placeholder="首页"/>
</div>
`,
created () { console.log('Home created') },
mounted () { console.log('Home mounted') },
unmounted () { console.log('Home unmounted') }
}
const Kind = {
template: `
<div>
分类 <input placeholder="分类"/>
</div>
`,
created () { console.log('Kind created') },
mounted () { console.log('Kind mounted') },
unmounted () { console.log('Kind unmounted') }
}
const Cart = {
template: `
<div>
购物车 <input placeholder="购物车"/>
</div>
`,
created () { console.log('Cart created') },
mounted () { console.log('Cart mounted') },
unmounted () { console.log('Cart unmounted') }
}
const User = {
template: `
<div>
我的 <input placeholder="我的"/>
</div>
`,
created () { console.log('User created') },
mounted () { console.log('User mounted') },
unmounted () { console.log('User unmounted') }
}
Vue.createApp({
data () {
return {
currentTab: 'Home'
}
},
components: {
Home,
Kind,
Cart,
User
}
}).mount('#app')
</script>
</html>
当一个组件在
<KeepAlive>
中被切换时,它的activated
和deactivated
生命周期钩子将被调用,用来替代mounted
和unmounted
。这适用于<KeepAlive>
的直接子节点及其所有子孙节点。
9.3activated、deactivated钩子
完整案例:08_dynamic/53_activated_deacvidated.html
<!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>动态组件</title>
</head>
<body>
<div id="app">
<ul>
<li @click="currentTab='Home'">首页</li>
<li @click="currentTab='Kind'">分类</li>
<li @click="currentTab='Cart'">购物车</li>
<li @click="currentTab='User'">我的</li>
</ul>
<!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 -->
<!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 -->
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
</div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
const Home = {
template: `
<div>
首页 <input placeholder="首页"/>
</div>
`,
created () { console.log('Home created') },
mounted () { console.log('Home mounted') },
unmounted () { console.log('Home unmounted') },
activated () { console.log('Home 显示')},
deactivated () { console.log('Home 隐藏')}
}
const Kind = {
template: `
<div>
分类 <input placeholder="分类"/>
</div>
`,
created () { console.log('Kind created') },
mounted () { console.log('Kind mounted') },
unmounted () { console.log('Kind unmounted') },
activated () { console.log('Kind 显示')},
deactivated () { console.log('Kind 隐藏')}
}
const Cart = {
template: `
<div>
购物车 <input placeholder="购物车"/>
</div>
`,
created () { console.log('Cart created') },
mounted () { console.log('Cart mounted') },
unmounted () { console.log('Cart unmounted') },
activated () { console.log('Cart 显示')},
deactivated () { console.log('Cart 隐藏')}
}
const User = {
template: `
<div>
我的 <input placeholder="我的"/>
</div>
`,
created () { console.log('User created') },
mounted () { console.log('User mounted') },
unmounted () { console.log('User unmounted') },
activated () { console.log('User 显示')},
deactivated () { console.log('User 隐藏')}
}
Vue.createApp({
data () {
return {
currentTab: 'Home'
}
},
components: {
Home,
Kind,
Cart,
User
}
}).mount('#app')
</script>
</html>
要不不缓存,要缓存都缓存了,这样不好
使用
include
/exclude
可以设置哪些组件被缓存,使用max
可以设定最多缓存多少个<!-- 用逗号分隔的字符串,中间不要家空格 --> <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>
组件如果想要条件性地被
KeepAlive
缓存,就必须显式声明一个name
选项。完整案例:
08_dynamic/54_keep_alive_include.html
<!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>动态组件</title> </head> <body> <div id="app"> <ul> <li @click="currentTab='Home'">首页</li> <li @click="currentTab='Kind'">分类</li> <li @click="currentTab='Cart'">购物车</li> <li @click="currentTab='User'">我的</li> </ul> <!-- 动态组件默认切换时 执行的是组件的 销毁 和 重新创建 --> <!-- 可以使用 KeepAlive 保留组件的状态,避免组件的重新渲染 --> <!-- 字符串逗号分隔,千万不要加空格 --> <!-- <keep-alive include="home,user"> <component :is="currentTab"></component> </keep-alive> --> <!-- 正则 --> <!-- <keep-alive :include="/home|user/"> <component :is="currentTab"></component> </keep-alive> --> <!-- 数组 --> <keep-alive :include="['home', 'user']"> <component :is="currentTab"></component> </keep-alive> </div> </body> <script src="../lib/vue.global.js"></script> <script> const Home = { name: 'home', template: ` <div> 首页 <input placeholder="首页"/> </div> `, created () { console.log('Home created') }, mounted () { console.log('Home mounted') }, unmounted () { console.log('Home unmounted') }, activated () { console.log('Home 显示')}, deactivated () { console.log('Home 隐藏')} } const Kind = { name: 'kind', template: ` <div> 分类 <input placeholder="分类"/> </div> `, created () { console.log('Kind created') }, mounted () { console.log('Kind mounted') }, unmounted () { console.log('Kind unmounted') }, activated () { console.log('Kind 显示')}, deactivated () { console.log('Kind 隐藏')} } const Cart = { name: 'cart', template: ` <div> 购物车 <input placeholder="购物车"/> </div> `, created () { console.log('Cart created') }, mounted () { console.log('Cart mounted') }, unmounted () { console.log('Cart unmounted') }, activated () { console.log('Cart 显示')}, deactivated () { console.log('Cart 隐藏')} } const User = { name: 'user', template: ` <div> 我的 <input placeholder="我的"/> </div> `, created () { console.log('User created') }, mounted () { console.log('User mounted') }, unmounted () { console.log('User unmounted') }, activated () { console.log('User 显示')}, deactivated () { console.log('User 隐藏')} } Vue.createApp({ data () { return { currentTab: 'Home' } }, components: { Home, Kind, Cart, User } }).mount('#app') </script> </html>
9.4<component>
元素
一个用于渲染动态组件或元素的“元组件”
完整案例:08_dynamic/55_component_element.html
<!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>
也可以渲染组件
9.5 DOM 模板解析注意事项(is=“vue:xxx”)-了解
当 is
attribute 用于原生 HTML 元素时,它将被当作 Customized built-in element,其为原生 web 平台的特性。
但是,在这种用例中,你可能需要 Vue 用其组件来替换原生元素,如 DOM 模板解析注意事项所述。你可以在 is
attribute 的值中加上 vue:
前缀,这样 Vue 就会把该元素渲染为 Vue 组件(my-row-component
为自定义组件):
<table>
<tr is="vue:my-row-component"></tr>
</table>
完整案例:08_dynamic/56_DOM模板解析注意事项.html
<!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>
</head>
<body>
<div id="app">
<table>
<tr>
<th>序号</th>
<th>姓名</th>
</tr>
<!-- <my-tr></my-tr> -->
<tr is="vue:my-tr"></tr>
</table>
</div>
</body>
<template id="tr">
<tr>
<td>1</td>
<td>吴大勋</td>
</tr>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Tr = {
template: '#tr'
}
Vue.createApp({
components: {
MyTr: Tr
}
}).mount('#app')
</script>
</html>
注意不要使用绑定属性
10.异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能
10.1 全局API
学习:defineAsyncComponent()
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
完整案例:09_async/57_defineAsyncComponent.html
<!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>异步组件</title>
</head>
<body>
<div id="app">
<my-test></my-test>
<my-com></my-com>
</div>
</body>
<template id="com">
<div>异步加载组件</div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
const Com = {
template: '#com'
}
const MyCom = Vue.defineAsyncComponent(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(Com)
}, 3000);
})
})
Vue.createApp({
components: {
MyTest: Com,
MyCom
}
}).mount('#app')
</script>
</html>
10.2加载函数
学习:() => import()
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
以后讲解项目时可以用到,需要在脚手架环境中使用(单文件组件中使用
)
10.3 <Suspense>
组件
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
vue3中新增的
完整案例:09_async/58_Suspense.html
<!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>异步组件</title>
</head>
<body>
<div id="app">
<my-test></my-test>
<!-- 组件未加载完毕,显示 正在加载... 加载完毕 显示组件 -->
<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 Com = {
template: '#com'
}
const MyCom = Vue.defineAsyncComponent(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(Com)
}, 3000);
})
})
Vue.createApp({
components: {
MyTest: Com,
MyCom
}
}).mount('#app')
</script>
</html>
后期可以和
Transition
,KeepAlive
,路由
等结合使用