Vue3.3指北
- Vue3
- 1、组件基础
- 1.1、全局组件
- 1.2、局部组件
- 1.3、组件的命名
- 1.4、组件的数据存放
- 1.5、组件标签化
- 2、父组件向子组件传递数据
- 2.1、props
- 2.2、动态props
- 2.3、props传数组
- 2.4、props传对象
- 2.4.1、默认值和必传值
- 3、子组件向父组件传递数据
- 4、父子组件互相访问
- 4.1、父组件访问子组件 - $refs
- 5、插槽
- 5.1、普通插槽
- 5.2、具名插槽
- 5.3、渲染作用域
- 5.4、作用域插槽
- 6、动态组件和异步组件
- 6.1、动态组件
- 6.1.1、在动态组件上使用 keep-alive
- 6.2、异步组件
- 7、组件的生命周期
- 7.1、在选项式API中使用Vue生命周期钩子
- 7.2、在组合式API中使用Vue3生命周期钩子
- 7.3、将Vue2的生命周期钩子代码更新到Vue3
- 7.4、深入了解每个生命周期钩子
- 7.4.1、beforeCreate() – 选项 API
- 7.4.2、created() – 选项式 API
- 7.4.3、setup() - 组合式API
- 7.4.4、beforeMount() 和 onBeforeMount()
- 7.4.5、mounted() 和onMounted()
- 7.4.6、beforeUpdate() 和onBeforeUpdate()
- 7.4.7、updated() 和onUpdated()
- 7.4.8、beforeUnmount() 和 onBeforeUnmounted()
- 7.4.9、unmounted() 和 onUnmounted()
- 7.4.10、activated() and onActivated()
- 7.4.11、deactivated() 和 onDeactivated()
视频参考教程: 2021年Vue3.0全家桶全系列精讲
随笔记源码: 逍遥的人儿 / KuangStudyVue3
const app = Vue.createApp({...})
// component-a 为组件名
app.component('component-a', {
// 配置项
})
// component-b 为组件名
app.component('component-b', {
// 配置项
})
<div id="app">
<component-a>component-a>
<component-b>component-b>
div>
示例:
<div id="app">
<component-a>component-a>
div>
<script>
// 创建一个Vue 应用
const app = Vue.createApp({})
// 定义一个名为 component-a 的新全局组件
app.component('component-a', {
template: '自定义组件!
'
})
app.mount('#app')
script>
// 局部注册组件ComponentA
const ComponentA = {
/* ... */
}
// 局部注册组件ComponentB
const ComponentB = {
/* ... */
}
components
选项中定义你想要使用的组件:const app = Vue.createApp({
components: {
'ComponentA': ComponentA,
'ComponentA': ComponentB
}
})
<div id="app">
<ComponentA>ComponentA>
<ComponentB>ComponentB>
div>
示例:
<div id="app">
<runoob-a>runoob-a>
div>
<script>
var runoobA = {
template: '自定义组件!
'
}
const app = Vue.createApp({
components: {
'runoob-a': runoobA
}
})
app.mount('#app')
script>
- 全局组件:在整个Vue实例中都可以被调用
- 局部组件:只能在当前组件中被使用
组件命名分为两种,短横线式和大驼峰式:
<div id="app">
<my-component-name>my-component-name>
div>
<script>
// 局部定义组件my-component-name
const my-component-name = {
template: '自定义组件!
'
}
const app = Vue.createApp({
components: {
// 注册组件
'my-component-name': my-component-name
}
})
app.mount('#app')
script>
<div id="app">
<MyComponentName>MyComponentName>
div>
<script>
// 局部定义组件runoobA
const MyComponentName = {
template: '自定义组件!
'
}
const app = Vue.createApp({
components: {
// 注册组件
'MyComponentName': MyComponentName
}
})
app.mount('#app')
script>
注意:
- 我们直接在DOM(即非字符串的模板)中使用时只有短横线法是有效的。在后面CLI调用两种方法都可以
问题:组件可以访问Vue实例数据吗?
组件对象也有 data 、 methods
// 注册一个局部组件
const Counter = {
data() {
return {
count: 0
}
},
template: `
`,
methods: {}
}
template
模块写法不够清晰,如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。
<div id="app">
<Counter>Counter>
div>
<template id="mycount">
<button @click="count++">你点击了{{count}}次button>
template>
<script src="../js/vue.js">script>
<script>
// 定义一个局部组件
const Counter = {
data() {
return {
count: 0
}
},
template: '#mycount'
}
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
msg: '你好,Vue3!'
}
},
// 组件选项
components: {
// 注册局部组件
'Counter': Counter,
}
});
// 挂载vue实例
app.mount('#app');
script>
组件中,使用选项 props
来声明需要从父级接收到的数据
<div id="app">
<site-name title="Google">site-name>
div>
<script>
const app = Vue.createApp({})
// 全局注册组件
app.component('site-name', {
// 使用 props 接收从父级传来的数据
props: ['title'],
template: `{{ title }}
`
})
app.mount('#app')
script>
可以用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件:
<div id="app">
<site-name :title="title" :message="message">site-name>
div>
<script>
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
title: 'Google',
message: ['西','贝','秦']
}
}
});
// 全局注册组件
app.component('site-name', {
// 使用 props 接收从父级传来的数据
props: ['title','message'],
template: `
{{ title }}
- {{index}} -- {{item}}
`
})
app.mount('#app');
script>
props
的值有两种方式:
方式一传递字符串数组代码如下:
<div id="app">
<site-name :title="title" :message="message">site-name>
div>
<script>
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
title: 'Google',
message: ['西','贝','秦']
}
}
});
// 全局注册组件
app.component('site-name', {
// 使用 props 接收从父级传来的数据
props: ['title','message'],
template: `
{{ title }}
- {{index}} -- {{item}}
`
})
app.mount('#app');
script>
在前面,我们的 props
选项是使用一个数组
除了数组之外,我们也可以使用对象,当需要对props
进行类型等验证时,就需要对象写法了
语法如下:
Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
示例如下:
<div id="app">
<site-name :title="title" :message="message">site-name>
div>
<script>
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
title: 'Google',
message: ['西','贝','秦']
}
}
});
// 全局注册组件
app.component('site-name', {
// 使用 props 接收从父级传来的数据
props: {
// 类型限制
title: String, //限制父组件传的是字符串类型
message: Array,// 限制父组件传的是数组类型
},
template: `
{{ title }}
- {{index}} -- {{item}}
`
})
app.mount('#app');
script>
type
: 限制的类型default
: 如果没有传值,给一个默认值
required
: 必须的,即意味着这个值是必须要传递的,不传就报错示例:
<div id="app">
<site-name :title="title" :message="message">site-name>
div>
<script>
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
title: 'Google',
message: ['西','贝','秦']
}
}
});
// 全局注册组件
app.component('site-name', {
// 使用 props 接收从父级传来的数据
props: {
// 类型限制
title: {
type: String, // 类型限制为 String
default: '默认Title', // 如果没有传值,则给一个默认值
required: true // required 必须的,即意味着这个值是必须要传递的,不传就报错
},
// 类型是对象或者数组时, 默认值必须是一个函数
message: {
type: Array, // 类型限制为 Array
default() {
return [];
},
},
template: `
{{ title }}
- {{index}} -- {{item}}
`
})
app.mount('#app');
script>
props
用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件去。自定义事件的流程:
$emit()
来发射事件v-on
来监听子组件事件示例:我们使用子组件发射事件来触发父组件的 appClick 函数,并传递参数秦晓
<div id="app">
<Box @box-click="appClick">Box>
div>
<script>
// 注册局部组件
const Box = {
methods: {
btnClick(){
// btnClick函数发射事件:第一个参数是自定义事件的名称,第二个参数是自定义事件的参数
this.$emit('boxClick','秦晓');
}
},
template: `
`
}
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
message: '',
}
},
components: {
Box,
},
methods: {
// 接收自定义事件传的参数
appClick(item) {
console.log('父组件函数被触发');
console.log(item);
}
}
});
app.mount('#app');
script>
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问根组件
$children(Vue3.x已经废弃)
或 $refs
$parent
(一般不用)$refs
和 ref
指令通常是一起使用的,可以用于获取 dom 元素或组件实例查找范围 → 当前组件内(更精确稳定)
ref
给某一个子组件绑定一个特定的 ID
<div ref="chartRef">我是渲染图表的容器div>
this.$refs.ID
就可以访问到该组件了
this.$refs.ID.xx
就可以拿到该组件里面的属性数据了mounted () {
console.log(this.$refs.chartRef)
}
示例:
我们给子组件使用
ref="box1"
绑定ID,在父组件里面使用this.$refs.box1
就可以拿到该组件,接着使用this.$refs.box1.msg
拿到该组件的msg
属性数据
<div id="app">
<Box ref="box1">Box>
<button @click="getChildComponent">访问子组件button>
div>
<script>
// 注册局部组件
const Box = {
data(){
return {
msg: '春风十里'
}
},
methods: {
btnClick(){
alert('点击了按钮')
}
},
template: `
`
}
// 创建Vue的实例对象
const app = Vue.createApp({
data(){
return {
message: '秦晓',
}
},
components: {
Box,
},
methods: {
// 父组件可访问子组件
getChildComponent(){
// this.$refs.box1 相当于拿到了子组件
// this.$refs.box1.msg 就是拿到了子组件里面的 msg 数据
// this.$refs.box1.btnClick 就是拿到了子组件里面的 btnClick 方法
console.log(this.$refs.box1.msg);
}
}
});
app.mount('#app');
script>
就可以为子组件开启一个插槽<!--子组件模板-->
<template id="box">
<slot>默认内容</slot>
</template>
<div id="app">
<Box>
<button>按钮button>
Box>
div>
<div>
div>
标签当子组件的功能复杂时,子组件的插槽可能并非是一个。
如何给插槽起名字呢?
<slot name="header">头部的内容slot>
template
标签,并且增加 v-slot:name
属性即可<template v-slot:header>
<button>我是头部button>
template>
示例:
<template id="box">
<div>
<header>
<slot name="header">头部的内容slot>
header>
<main>
<slot name="main">主要的内容slot>
main>
<footer>
<slot name="footer">尾部的内容slot>
footer>
div>
template>
<div id="app">
<Box>
<template v-slot:header>
<button>我是头部button>
template>
<template v-slot:main>
<input type="text" placeholder="我是主要内容" />
template>
<template v-slot:footer>
template>
Box>
div>
假设:isShow
属性包含在组件中,也包含在 Vue 实例中。
<div id="app">
<Box v-show="isShow">Box>
div>
提问:既然子组件和父组件(Vue实例)都有
isShow
属性,那么使用哪个呢?
- 回答:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在级作用域内编译
- 我们在使用
的时候,整个组件的使用过程相当于在父组件中出现的
- 那么它的作用域就是父组件,使用的属性也是属于父组件的属性
- 因此 ,
isShow
使用的是 Vue 实例中的属性,而不是子组件的属性
一句话总结:父组件替换插槽的标签,但是内容由子组件来提供
作用域插槽语法如下:
:data
动态绑定数据
<template id="box">
<slot :data="nameArr">slot>
template>
template
标签,并附带属性 v-slot:default="slotProps"
接收数据,slotProps.data
就是我们的``nameArr`数据
<div id="app">
<Box>
<template v-slot:default="slotProps">template>
Box>
div>
示例:
<div id="app">
<Box>
<template v-slot:default="slotProps">
<span>{{slotProps.data.join('---')}}span>
template>
Box>
div>
<template id="box">
<slot :data="nameArr">
<ul>
<li v-for="name in nameArr">{{name}}li>
ul>
slot>
template>
上述我们使用子组件,虽然在父级作用域内编译,但是通过动态数据绑定拿到了子级作用域的值,这样就实现了子组件在父级作用域内编译,但是拿到了子级作用域的值
动态组件,就是实现动态切换的组件:它的用途是可以动态绑定我们的组件,根据数据不同更换不同的组件。
<component>
用来动态地挂载不同的组件 使用 is
属性来选择要挂载的组件
<component v-bind:is="currentTabComponent">component>
currentTabComponent
可以包括:
示例:
is
属性绑定已注册组件的名字<div id="app">
<button @click="changeView('A')">切换到组件Abutton>
<button @click="changeView('B')">切换到组件Bbutton>
<button @click="changeView('C')">切换到组件Cbutton>
<component :is="currentView">component>
div>
<script>
let vm = new createApp({
data: {
currentView: 'comA'
},
components: {
comA: {
template: '组件A'
},
comB: {
template: '组件B'
},
comC: {
template: '组件C'
}
},
methods: {
changeView(component) {
this.currentView = 'com' + component;
}
}
});
script>
我们上方在一个多标签的界面中使用 is
属性来切换不同的组件,当在这些组件之间切换的时候,我们有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
失活问题:动态组件有一个问题就是不会缓存,也称为失活,假使现在有这样一个情况:我们有一个切换按钮可以在输入框和按钮之间切换,当我们给输入框输入数据,然后切换为按钮,再切换为输入框,发现我们之前输入的数据被清空了
解决方法是给 component
标签外加一个 keep-alive
:
<keep-alive>
<component :is="currentTabComponent">component>
keep-alive>
当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载;这样的做得目的可以很好的提高用户体验。
defineAsyncComponent
,这个方法可以传递两种类型的参数,分别是函数类型和对象类型语法如下:
// 注册一个全局异步组件
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: 'I am async!'
})
}, 1000)
})
上面的例子,采用 setTimeout
模拟异步获取组件,真实情况,甚至可以通过ajax请求组件编译之后的template
,然后调用 resolve
方法;如果加载失败,可以调用 reject
方法
大部分情况下,我们的组件都是单独分割成一个 .vue
文件,那么我们可以这么做:
Vue.component('async-webpack-example', function (resolve) {
require(['./my-async-component'], resolve)
})
示例:
// 定义异步组件
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
// 创建Vue应用程序
const app = createApp({
// 在模板中使用异步组件
template: `
异步组件示例
`
});
// 挂载应用程序
app.mount('#app');
defineAsyncComponent
函数定义了一个异步组件AsyncComponent
,它通过动态导入./AsyncComponent.vue
模块来异步加载组件Suspense
组件包裹异步组件,它负责在异步组件加载期间显示一个加载状态。一旦异步组件加载完成,它将被渲染并替换Suspense
组件。Vue 3 生命周期完整指南 - 掘金 (juejin.cn)
使用 选项API,生命周期钩子是被暴露 Vue实例上的选项。我们不需要导入任何东西,只需要调用这个方法并为这个生命周期钩子编写代码。
mounted()
和updated()
生命周期钩子,可以这么写// 选项 API
<script>
export default {
mounted() {
console.log('mounted!')
},
updated() {
console.log('updated!')
}
}
</script>
在组合API中,我们需要将生命周期钩子导入到项目中,才能使用,这有助于保持项目的轻量性。
// 组合 API
import { onMounted } from 'vue'
除了beforecate
和created
(它们被setup
方法本身所取代),我们可以在setup
方法中访问的API生命周期钩子有9个选项:
onBeforeMount
– 在挂载开始之前被调用onMounted
– 组件挂载时调用onBeforeUpdate
– 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器onUpdated
– 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子onBeforeUnmount
– 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的onUnmounted
– 卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载onActivated
– 被 keep-alive
缓存的组件激活时调用onDeactivated
– 被 keep-alive
缓存的组件停用时调用onErrorCaptured
– 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false
以阻止该错误继续向上传播示例:
// 组合 API
<script>
import { onMounted } from 'vue'
export default {
setup () {
onMounted(() => {
console.log('mounted in the composition api!')
})
}
}
</script>
beforeCreate
-> 使用 setup()
created
-> 使用 setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeDestroy
-> onBeforeUnmount
destroyed
-> onUnmounted
errorCaptured
-> onErrorCaptured
选项式API | 组合式API |
---|---|
beforeCreate/created | setup |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
update | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
由于创建的挂钩是用于初始化所有响应数据和事件的事物,因此beforeCreate
无法访问组件的任何响应数据和事件。
// 选项 API
export default {
data() {
return {
val: 'hello'
}
},
beforeCreate() {
console.log('Value of val is: ' + this.val)
}
}
val
的输出值是 undefined
,因为尚未初始化数据,我们也不能在这调用组件方法。
如果我们要在组件创建时访问组件的数据和事件,可以把上面的 beforeCreate
用 created
代替。
// 选项API
export default {
data() {
return {
val: 'hello'
}
},
created() {
console.log('Value of val is: ' + this.val)
}
}
其输出为Value of val is: hello
,因为我们已经初始化了数据。
对于使用 组合API 的 Vue3 生命周期钩子,使用setup()
方法替换beforecate
和created
。这意味着,在这些方法中放入的任何代码现在都只在setup
方法中。
// 组合API
import { ref } from 'vue'
export default {
setup() {
const val = ref('hello')
console.log('Value of val is: ' + val.value)
return {
val
}
}
}
在组件DOM实际渲染安装之前调用。在这一步中,根元素还不存在。
this.$els
来访问。ref
// 选项 API
export default {
beforeMount() {
console.log(this.$el)
}
}
组合式API中使用 ref
:
// 组合 API
<template>
<div ref='root'>
Hello World
</div>
</template>
import { ref, onBeforeMount } from 'vue'
export default {
setup() {
const root = ref(null)
onBeforeMount(() => {
console.log(root.value)
})
return {
root
}
},
// 兼容选项式API
beforeMount() {
console.log(this.$el)
}
}
因为app.$el
还没有创建,所以输出将是undefined
。
在组件的第一次渲染后调用,该元素现在可用,允许直接DOM访问。
this.$el
来访问我们的DOMrefs
来访问Vue生命周期钩子中的DOMimport { ref, onMounted } from 'vue'
export default {
setup() { /* 组合 API */
const root = ref(null)
onMounted(() => {
console.log(root.value)
})
return {
root
}
},
mounted() { /* 选项 API */
console.log(this.$el)
}
}
beforeUpdate
对于跟踪对组件的编辑次数,甚至跟踪创建“撤消”功能的操作很有用
DOM更新后,updated
的方法即会调用。
<template>
<div>
<p>{{val}} | 编辑 {{ count }} 次p>
<button @click='val = Math.random(0, 100)'>点击改变button>
div>
template>
选项式API:
export default {
data() {
return {
val: 0
}
},
beforeUpdate() {
console.log("beforeUpdate() val: " + this.val)
},
updated() {
console.log("updated() val: " + this.val
}
}
组合式API:
import { ref, onBeforeUpdate, onUpdated } from 'vue'
export default {
setup () {
const count = ref(0)
const val = ref(0)
onBeforeUpdate(() => {
count.value++;
console.log("beforeUpdate");
})
onUpdated(() => {
console.log("updated() val: " + val.value)
})
return {
count, val
}
}
}
这些方法很有用,但是对于更多场景,我们需要使用的watch
方法检测这些数据更改。 watch
之所以好用,是因为它给出了更改后的数据的旧值和新值。
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
在 选项 API中,删除事件侦听器的示例如下所示:
// 选项 API
export default {
mounted() {
console.log('mount')
window.addEventListener('resize', this.someMethod);
},
beforeUnmount() {
console.log('unmount')
window.removeEventListener('resize', this.someMethod);
},
methods: {
someMethod() {
// do smth
}
}
}
// 组合API
import { onMounted, onBeforeUnmount } from 'vue'
export default {
setup () {
const someMethod = () => {
// do smth
}
onMounted(() => {
console.log('mount')
window.addEventListener('resize', someMethod);
})
onBeforeUnmount(() => {
console.log('unmount')
window.removeEventListener('resize', someMethod);
})
}
}
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
import { onUnmounted } from 'vue'
export default {
setup () { /* 组合 API */
onUnmounted(() => {
console.log('unmounted')
})
},
unmounted() { /* 选项 API */
console.log('unmounted')
}
}
keep-alive
缓存的组件激活时调用。keep-alive
组件来管理不同的选项卡视图,每次在选项卡之间切换时,当前选项卡将运行这个 activated
钩子。<template>
<div>
<span @click='tabName = "Tab1"'>Tab 1 span>
<span @click='tabName = "Tab2"'>Tab 2span>
<keep-alive>
<component :is='tabName' class='tab-area'/>
keep-alive>
div>
template>
<script>
import Tab1 from './Tab1.vue'
import Tab2 from './Tab2.vue'
import { ref } from 'vue'
export default {
components: {
Tab1,
Tab2
},
setup () { /* 组合 API */
const tabName = ref('Tab1')
return {
tabName
}
}
}
script>
在Tab1.vue组件内部,我们可以像这样访问activated
钩子。
<template>
<div>
<h2>Tab 1h2>
<input type='text' placeholder='this content will persist!'/>
div>
template>
<script>
import { onActivated } from 'vue'
export default {
setup() {
onActivated(() => {
console.log('Tab 1 Activated')
})
}
}
script>
keep-alive
缓存的组件停用时调用。import { onActivated, onDeactivated } from 'vue'
export default {
setup() {
onActivated(() => {
console.log('Tab 1 Activated')
})
onDeactivated(() => {
console.log('Tab 1 Deactivated')
})
}
}
现在,当我们在选项卡之间切换时,每个动态组件的状态都将被缓存和保存。