1. vue3的生命周期
vue3
的生命周期一般有2种形式写法,一种是基于vue2
的options API
的写法,一种是vue3
特有的Composition API
options API的生命周期
基本同vue2
的生命周期基础,只是为了与生命周期beforeCreate
和created
对应,将beforeDestroy
和destroyed
更名为beforeUnmount
和unmounted
,使用方法同vue2
<template>
<p>生命周期</p>
<p>{{msg}}</p>
<button @click="changeMsg">修改值</button>
<button @click="toHome">跳转页面</button>
</template>
<script>
import { useRouter } from 'vue-router'
export default {
name: 'lifeCycles',
data() {
return {
msg: 'hello vue3'
}
},
setup() {
console.log('setup')
// 在`setup`函数中创建`router`对象,相当于vue2中的`this.router`
const router = useRouter()
const toHome = () => {
router.push({
path: '/'
})
}
return {toHome}
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
},
beforeUpdate() {
console.log('beforeUpdate');
},
updated() {
console.log('updated');
},
// 由vue2 beforeDestroy改名
beforeUnmount() {
console.log('beforeUnmount');
},
// 由vue2 destroyed改名
unmounted() {
console.log('unmounted');
},
methods: {
changeMsg() {
this.msg = 'after changed'
}
}
}
</script>
composition API的生命周期
composition API
的生命周期钩子函数是写在setup
函数中的,它所有生命周期是在vue2生命周期名字前加on
,且必须先导入才可使用
在这种写法中,是没有onBeforeCreate
和onCreated
周期的,setup
等同于(或者说是介于)这两个生命周期
<template>
<p>composition API生命周期</p>
<p>{{msg}}</p>
<button @click="changeMsg">修改值</button>
<button @click="toHome">跳转页面</button>
</template>
<script>
import { useRouter } from 'vue-router'
// 必须先导入生命周期
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
name: 'lifeCycles',
data() {
return {
msg: 'hello vue3'
}
},
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount');
})
onMounted(() => {
console.log('onMounted');
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
})
onUpdated(() => {
console.log('onUpdated');
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
})
onUnmounted(() => {
console.log('onUnmounted');
})
// 在`setup`函数中创建`router`对象,相当于vue2中的`this.router`
const router = useRouter()
const toHome = () => {
router.push({
path: '/'
})
}
return {toHome}
},
methods: {
changeMsg() {
this.msg = 'after changed'
}
}
}
</script>
2. 如何理解 Composition API 和 options API ?
Composition API
带来了什么:
- 更好的代码组织
- 更好的逻辑复用,避免
mixins
混入时带来的命名冲突和维护困难问题 - 更好的类型推导
options API
使用options API
,当代码很多时,即当data, watch, methods
等有很多内容时,业务逻辑比较复杂,我们需要修改一部分时,可能要分别到data/methods/模板
中对应修改,可能需要我们在页面反复横跳来修改,逻辑块会比较散乱。
Composition API
Composition API
则会将业务相关的部分整合到一起,作为一个模块,当要修改,统一到一处修改,代码看起来会更有条理
它包含的内容包括:
- reactive
- ref相关(ref, toRef, toRefs,后面会具体介绍)
- readonly
- watch和watchEffect
- setup
- 生命周期钩子函数
两者的选择:
- 不建议共用,否则容易引起混乱(思路、组织方式、写法都不太一样)
- 小型项目,业务逻辑简单的,建议用
options API
,对新手也比较友好 - 中大型项目、逻辑复杂,建议使用
Composition API
Composition API
它属于高阶技巧,不是基础必会的,有一定的学习成本,是为了解决复杂业务逻辑而设计,就像hooks
在React
中的地位一样
3. 如何理解ref,toRef,toRefs
ref
- 通过
ref
方式创建响应式的值类型,并赋予初始值,并通过.value
的方式获取值或修改值 - 通过
reactive
方式创建响应式的引用类型,并赋予初始值,修改和获取方式同普通对象一样 - 除了以上两种用法,还可以使用
ref
来声明dom
元素,也就是类似vue2
中的用法
<template>
<p>ref demo</p>
<p>{{nameRef}}今年{{ageRef}}岁了</p>
<p>他喜欢{{hobbies.type}}</p>
</template>
<script>
// 导入ref, reactive, onMounted
<template>
<p>ref demo</p>
<p>{{nameRef}}今年{{ageRef}}岁了</p>
<p>他喜欢{{hobbies.type}}</p>
<p ref="eleRef">我是refTemplate使用方式的内容</p>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'ref',
setup() {
// ref
const ageRef = ref(3); // ref创建响应式的值类型,并赋予初始值
const nameRef = ref('小花')
console.log(ageRef.value) // 通过.value方式获取值
ageRef.value = 18 // 通过.value方式修改值
// reactive
const hobbies = reactive({
type: 'basketball'
})
console.log(hobbies.type) // basketball,通过obj[key]方式获取值
hobbies.type = 'aaaaa' // 通过obj[key]=xxx方式修改值
// refTemplate
const eleRef = ref(null)
onMounted(() => {
// 跟vue2的区别在于,vue2使用this.$refs['eleRef']方式获取dom,这里通过eleRef.value方式获取
console.log(eleRef.value) // dom
console.log(eleRef.value.innerHTML) // 我是refTemplate使用方式的内容
})
// 注意,这些对象都要return出去,否则就不是响应式
return {
ageRef,
nameRef,
hobbies,
eleRef
}
}
}
</script>
PS: 小建议,
ref
定义的数据可以加个Ref
后缀,这样就能区分ref
和reactive
定义的变量了
toRef
看定义有点绕
- 针对一个响应式对象(
reactive
封装)的属性prop
- 通过
toRef
创建一个ref
对象,这个ref
对象和reactive
对象的某属性两者保持引用关系
注意,如果toRef是通过普通对象来生成ref
对象,那么普通对象和ref
对象都将不是响应式的
<template>
<p>toRef demo</p>
<p>小花今年 - {{ageRef}}岁 - {{state.age}}岁</p>
</template>
<script>
import { toRef, reactive } from 'vue'
export default {
name: 'toRef',
setup() {
// 定义一个响应式的reactive引用对象
const state = reactive({
name: '小花',
age: 3
})
// 如果是普通对象,使用toRef,那么它们将都不是响应式的
// 也就是说ageRef和state都不是响应式
// const state = {
// name: '小花',
// age: 3
// }
// 通过toRef创建一个响应值类型ageRef, 这个ageRef和state.age属性保持双向引用
const ageRef = toRef(state, 'age')
// 修改state.age值时,ageRef也会跟着改
setTimeout(() => {
state.age = 25
}, 1000)
// 修改ageRef值时,state.age也会跟着改
setTimeout(() => {
ageRef.value = 30
}, 2000)
return {
state, ageRef
}
}
}
</script>
toRefs
- 将响应式对象(
reactive
)的所有属性prop
,转换为对应prop
名字的ref
对象 - 两者保持引用关系
<template>
<p>toRef demo</p>
<!-- 这样,模板中就不用写state.name, state.age了,直接写name和age即可 -->
<p>{{name}}今年 - {{age}}岁</p>
</template>
<script>
import { toRefs, reactive } from 'vue'
export default {
name: 'toRef',
setup() {
// 定义一个响应式的reactive引用对象
const state = reactive({
name: '小花',
age: 3
})
// 相当于
// const age = toRef(state, 'age')
// const name = toRef(state, 'name')
// const stateAsRefs = { age, name }
const stateAsRefs = toRefs(state)
// 修改state.age值时,就会映射到ref类型的age上
setTimeout(() => {
state.age = 25
}, 1000)
// return stateAsRefs 等同于:
// const { age: age, name: name } = stateAsRefs
// return { age, name }
return stateAsRefs
}
}
</script>
应用:
当使用composition API时,抽象出一个模块,使用toRefs
返回响应式对象,这样,在接收的时候,我们就可以使用解构的方式获取到对象里面的内容,这也是比较符合我们常用的方式
// 封装一个模块,使用toRefs导出对象
export function useFeature() {
const state = reactive({
x: 1,
y: 2
})
// ...
return toRefs(state)
}
// 导入时,可以使用解构方式
import { useFeature } from './features'
export default {
setup() {
// 可以在不丢失响应式的情况下解构
const { x, y } = useFeature()
return { x, y }
}
}
ref, toRef, toRefs 使用小结:
- 用
reactive
做对象的响应式,用ref
做值类型的响应式 - setup中返回
toRefs(state)
,或toRef(state, prop)
-
ref
变量命名建议用xxxRef
- 合成函数返回响应式对象时,使用
toRefs
为什么需要 ref ?
- 如果没有
ref
,普通的值类型定义,没法做响应式 -
computed,setup,合成函数
,都有可能返回值类型,要保证其返回是响应式的 - 如果vue不定义
ref
,用户可能会自己造ref
,反而更加混乱
为什么需要.value ?
-
ref
是一个对象(保证响应式),value
用来存储值 - 通过
.value
属性的get
和set
实现响应式 - 用于模板、
reactive
时,不需要.value
,这是因为vue
编译会自动识别,其他情况则需要使用
为什么需要 toRef 和 toRefs ?
- 目的:为了不丢失响应式的情况下,把对象数据分散、扩散(或者说是解构)
- 前提:针对的是响应式对象(reactive封装的对象)
- 本质:不创建响应式(创建是ref和reactive的事),而是延续响应式
4. watch和watchEffect的区别
-
watch
和watchEffect
都可以监听data
的变化 -
watch
需要指定监听的属性,默认初始时不会触发,如果初始要触发,需要配置immediate: true
-
watchEffect
是不需要指定监听的属性,而是自动监听其用到的属性,它初始化时,一定会执行一次,这是为了收集要监听的属性
<template>
<p>watch 的使用</p>
<p>numberRef: {{numberRef}}</p>
<p>{{name}}-{{age}}</p>
</template>
<script>
import { ref, reactive, toRefs, watch, watchEffect } from 'vue'
export default {
name:'Watch',
setup() {
const numberRef = ref(1000)
const state = reactive({
name: '小花',
age: 3
})
// 监听ref变量
watch(numberRef, (newVal, oldVal) => {
console.log('watch:', newVal, oldVal);
// watch: 1000 undefined
// watch: 200 1000
}, {
immediate: true // 第三个参数可选,是一些配置项,immediate表示初始化时就执行监听
})
setTimeout(() => {
numberRef.value = 200
}, 1000)
// 监听对象
watch(
// 第一参数是监听对象,如果是对象需要使用函数返回形式
() => state.age,
// 第二个参数是监听的变化值
(newVal, oldVal) => {
console.log('watch:', newVal, oldVal);
// watch: 3 undefined
// watch: 18 3
},
// 第三个参数是配置项
{
immediate: true, // 初始变化就监听
deep: true // 深度监听
}
)
setTimeout(() => {
state.age = 18
}, 1000)
return {
numberRef,
...toRefs(state)
}
}
}
</script>
// watchEffect监听
watchEffect(() => {
console.log('watchEffect');
console.log(numberRef.value);
console.log(state.age);
})
5. 在setup中怎么获取组件实例
- 在
setup
和其它compostion API
中没有this
- 如果一定要获取,要使用
getCurrentInstance
获取,并且在挂载后才可获取数据 - 如果是
options API
,则可以像vue2
一样正常使用this
<template>
<p>get instance</p>
</template>
<script>
import { getCurrentInstance, onMounted } from 'vue'
export default {
name: 'GetInstance',
data() {
return {
x: 1,
y: 2
}
},
// composition API
// 没有this,需要getCurrentInstance来获取组件实例
// 且setup本身是beforeCreate和Created生命周期间的钩子,拿不到data,所以要在onMounted中获取
setup() {
console.log('this', this); // this undefined
const instance = getCurrentInstance()
console.log('instance', instance.data.x); // instance undefined
onMounted(() => {
console.log('instance', instance.data.x); // instance 1
})
},
// options API
mounted() {
console.log(this.x); // 1
}
}
</script>
6. vue3升级了哪些重要的功能
参考官网升迁指南
createApp
// vue2
const app = new Vue({/*options*/})
Vue.use(/*...*/)
Vue.mixin(/*...*/)
Vue.component(/*...*/)
Vue.directive(/*...*/)
// vue3
const app = Vue.createApp({/*options*/})
app.use(/*...*/)
app.mixin(/*...*/)
app.component(/*...*/)
app.directive(/*...*/)
emits属性
- 在
setup
中可以使用emit
向父组件发出事件 - 在子组件中,需要
emits
声明向父组件发出的事件集合
<template>
parent
<Child msg="hello" @log="log" />
</template>
<script>
import Child from './child.vue'
export default {
name: 'emits',
components:{
Child
},
methods: {
log() {
console.log('child emit me!')
}
}
}
</script>
<!-- Child.vue -->
<script>
export default {
name: 'child',
props: {
msg: {
type: String
}
},
emits: ['log'], // 需要声明接收的父组件传递的方法
// 在setup方法中,可以使用emit方法与父组件通信
setup(props, {emit}) {
emit('log')
},
methods: {
one(e) {
console.log('one');
},
two(e) {
console.log('two');
}
}
}
</script>
多事件处理
<template>
<!-- 可以同时触发多个事件 -->
<button @click="one($event), two($event)">触发多事件</button>
</template>
fragment
vue2
只允许template
中只有一个元素,如果多个元素,必须用一个元素包裹
vue3
则允许template
中可以直接有多个元素,这样就可以减少dom
层级
移除.sync
vue2中的.sync
vue2
中使用.sync
是对以下语句的语法糖,父组件通过v-bind:xxx.sync='xxx'
来向子组件说明这个属性是双向绑定的,子组件中通过$emit('update:xxx', newVal)
来更新这个值
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>
<!-- sync语法糖 -->
<text-document v-bind:title.sync="doc.title"></text-document>
在vue3
中,废除了.sync
的写法,换成一种更具有语义的写法v-model:xxx
,在父组件中使用v-model:xxx
方式说明这个属性是双向绑定的,子组件中通过$emit('update:xxx', newVal)
来更新这个值
<template>
<p>{{name}}-{{age}}</p>
<UserInfo
v-model:name="name"
v-model:age="age"
/>
</template>
<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './userInfo.vue'
export default {
name: 'vmodel',
components: {
UserInfo
},
setup() {
const userInfo = reactive({
name: '小花',
age: 3
})
return toRefs(userInfo)
}
}
</script>
<!-- userInfo.vue -->
<template>
<input type="text" :value="name" @input="$emit('update:name', $event.target.value)">
<input type="text" :value="age" @input="$emit('update:age', $event.target.value)">
</template>
<script>
export default {
props: {
name: {
type: String
},
age: {
type: String
}
}
}
</script>
异步组件的导入方式不一样
vue2
: child: () => import('child.vue')
vue3
:需要defineAsyncComponent
导入,child: defineAsyncComponent(() => import('child.vue'))
teleport
teleport
将我们的模板移动到DOM
中 Vue app
之外的其他位置,比如可以使用teleport
标签将组件在body
层
<template>
<p>这是放在当前组件下的内容</p>
<teleport to="body">
<p>假设这是个弹窗,直接放到body下</p>
</teleport>
</template>