1. vue3的生命周期
vue3的生命周期一般有2種形式寫法,一種是基于vue2的options API的寫法,一種是vue3特有的Composition API
options API的生命周期
基本同vue2的生命周期基礎(chǔ),只是為了與生命周期beforeCreate和created對(duì)應(yīng),將beforeDestroy和destroyed更名為beforeUnmount和unmounted,使用方法同vue2
<template>
<p>生命周期</p>
<p>{{msg}}</p>
<button @click="changeMsg">修改值</button>
<button @click="toHome">跳轉(zhuǎn)頁面</button>
</template>
<script>
import { useRouter } from 'vue-router'
export default {
name: 'lifeCycles',
data() {
return {
msg: 'hello vue3'
}
},
setup() {
console.log('setup')
// 在`setup`函數(shù)中創(chuàng)建`router`對(duì)象,相當(dāng)于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的生命周期鉤子函數(shù)是寫在setup函數(shù)中的,它所有生命周期是在vue2生命周期名字前加on,且必須先導(dǎo)入才可使用
在這種寫法中,是沒有onBeforeCreate和onCreated周期的,setup等同于(或者說是介于)這兩個(gè)生命周期
<template>
<p>composition API生命周期</p>
<p>{{msg}}</p>
<button @click="changeMsg">修改值</button>
<button @click="toHome">跳轉(zhuǎn)頁面</button>
</template>
<script>
import { useRouter } from 'vue-router'
// 必須先導(dǎo)入生命周期
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`函數(shù)中創(chuàng)建`router`對(duì)象,相當(dāng)于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帶來了什么:
- 更好的代碼組織
- 更好的邏輯復(fù)用,避免
mixins混入時(shí)帶來的命名沖突和維護(hù)困難問題 - 更好的類型推導(dǎo)
options API
使用options API,當(dāng)代碼很多時(shí),即當(dāng)data, watch, methods等有很多內(nèi)容時(shí),業(yè)務(wù)邏輯比較復(fù)雜,我們需要修改一部分時(shí),可能要分別到data/methods/模板中對(duì)應(yīng)修改,可能需要我們?cè)陧撁娣磸?fù)橫跳來修改,邏輯塊會(huì)比較散亂。
Composition API
Composition API則會(huì)將業(yè)務(wù)相關(guān)的部分整合到一起,作為一個(gè)模塊,當(dāng)要修改,統(tǒng)一到一處修改,代碼看起來會(huì)更有條理

它包含的內(nèi)容包括:
- reactive
- ref相關(guān)(ref, toRef, toRefs,后面會(huì)具體介紹)
- readonly
- watch和watchEffect
- setup
- 生命周期鉤子函數(shù)
兩者的選擇:
- 不建議共用,否則容易引起混亂(思路、組織方式、寫法都不太一樣)
- 小型項(xiàng)目,業(yè)務(wù)邏輯簡(jiǎn)單的,建議用
options API,對(duì)新手也比較友好 - 中大型項(xiàng)目、邏輯復(fù)雜,建議使用
Composition API
Composition API它屬于高階技巧,不是基礎(chǔ)必會(huì)的,有一定的學(xué)習(xí)成本,是為了解決復(fù)雜業(yè)務(wù)邏輯而設(shè)計(jì),就像hooks在React中的地位一樣
3. 如何理解ref,toRef,toRefs
ref
- 通過
ref方式創(chuàng)建響應(yīng)式的值類型,并賦予初始值,并通過.value的方式獲取值或修改值 - 通過
reactive方式創(chuàng)建響應(yīng)式的引用類型,并賦予初始值,修改和獲取方式同普通對(duì)象一樣 - 除了以上兩種用法,還可以使用
ref來聲明dom元素,也就是類似vue2中的用法
<template>
<p>ref demo</p>
<p>{{nameRef}}今年{{ageRef}}歲了</p>
<p>他喜歡{{hobbies.type}}</p>
</template>
<script>
// 導(dǎo)入ref, reactive, onMounted
<template>
<p>ref demo</p>
<p>{{nameRef}}今年{{ageRef}}歲了</p>
<p>他喜歡{{hobbies.type}}</p>
<p ref="eleRef">我是refTemplate使用方式的內(nèi)容</p>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'ref',
setup() {
// ref
const ageRef = ref(3); // ref創(chuàng)建響應(yīng)式的值類型,并賦予初始值
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的區(qū)別在于,vue2使用this.$refs['eleRef']方式獲取dom,這里通過eleRef.value方式獲取
console.log(eleRef.value) // dom
console.log(eleRef.value.innerHTML) // 我是refTemplate使用方式的內(nèi)容
})
// 注意,這些對(duì)象都要return出去,否則就不是響應(yīng)式
return {
ageRef,
nameRef,
hobbies,
eleRef
}
}
}
</script>

PS: 小建議,
ref定義的數(shù)據(jù)可以加個(gè)Ref后綴,這樣就能區(qū)分ref和reactive定義的變量了
toRef
看定義有點(diǎn)繞
- 針對(duì)一個(gè)響應(yīng)式對(duì)象(
reactive封裝)的屬性prop - 通過
toRef創(chuàng)建一個(gè)ref對(duì)象,這個(gè)ref對(duì)象和reactive對(duì)象的某屬性兩者保持引用關(guān)系
注意,如果toRef是通過普通對(duì)象來生成ref對(duì)象,那么普通對(duì)象和ref對(duì)象都將不是響應(yīng)式的
<template>
<p>toRef demo</p>
<p>小花今年 - {{ageRef}}歲 - {{state.age}}歲</p>
</template>
<script>
import { toRef, reactive } from 'vue'
export default {
name: 'toRef',
setup() {
// 定義一個(gè)響應(yīng)式的reactive引用對(duì)象
const state = reactive({
name: '小花',
age: 3
})
// 如果是普通對(duì)象,使用toRef,那么它們將都不是響應(yīng)式的
// 也就是說ageRef和state都不是響應(yīng)式
// const state = {
// name: '小花',
// age: 3
// }
// 通過toRef創(chuàng)建一個(gè)響應(yīng)值類型ageRef, 這個(gè)ageRef和state.age屬性保持雙向引用
const ageRef = toRef(state, 'age')
// 修改state.age值時(shí),ageRef也會(huì)跟著改
setTimeout(() => {
state.age = 25
}, 1000)
// 修改ageRef值時(shí),state.age也會(huì)跟著改
setTimeout(() => {
ageRef.value = 30
}, 2000)
return {
state, ageRef
}
}
}
</script>



toRefs
- 將響應(yīng)式對(duì)象(
reactive)的所有屬性prop,轉(zhuǎn)換為對(duì)應(yīng)prop名字的ref對(duì)象 - 兩者保持引用關(guān)系
<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() {
// 定義一個(gè)響應(yīng)式的reactive引用對(duì)象
const state = reactive({
name: '小花',
age: 3
})
// 相當(dāng)于
// const age = toRef(state, 'age')
// const name = toRef(state, 'name')
// const stateAsRefs = { age, name }
const stateAsRefs = toRefs(state)
// 修改state.age值時(shí),就會(huì)映射到ref類型的age上
setTimeout(() => {
state.age = 25
}, 1000)
// return stateAsRefs 等同于:
// const { age: age, name: name } = stateAsRefs
// return { age, name }
return stateAsRefs
}
}
</script>
應(yīng)用:
當(dāng)使用composition API時(shí),抽象出一個(gè)模塊,使用toRefs返回響應(yīng)式對(duì)象,這樣,在接收的時(shí)候,我們就可以使用解構(gòu)的方式獲取到對(duì)象里面的內(nèi)容,這也是比較符合我們常用的方式
// 封裝一個(gè)模塊,使用toRefs導(dǎo)出對(duì)象
export function useFeature() {
const state = reactive({
x: 1,
y: 2
})
// ...
return toRefs(state)
}
// 導(dǎo)入時(shí),可以使用解構(gòu)方式
import { useFeature } from './features'
export default {
setup() {
// 可以在不丟失響應(yīng)式的情況下解構(gòu)
const { x, y } = useFeature()
return { x, y }
}
}
ref, toRef, toRefs 使用小結(jié):
- 用
reactive做對(duì)象的響應(yīng)式,用ref做值類型的響應(yīng)式 - setup中返回
toRefs(state),或toRef(state, prop) -
ref變量命名建議用xxxRef - 合成函數(shù)返回響應(yīng)式對(duì)象時(shí),使用
toRefs
為什么需要 ref ?
- 如果沒有
ref,普通的值類型定義,沒法做響應(yīng)式 -
computed,setup,合成函數(shù),都有可能返回值類型,要保證其返回是響應(yīng)式的 - 如果vue不定義
ref,用戶可能會(huì)自己造ref,反而更加混亂
為什么需要.value ?
-
ref是一個(gè)對(duì)象(保證響應(yīng)式),value用來存儲(chǔ)值 - 通過
.value屬性的get和set實(shí)現(xiàn)響應(yīng)式 - 用于模板、
reactive時(shí),不需要.value,這是因?yàn)?code>vue編譯會(huì)自動(dòng)識(shí)別,其他情況則需要使用
為什么需要 toRef 和 toRefs ?
- 目的:為了不丟失響應(yīng)式的情況下,把對(duì)象數(shù)據(jù)分散、擴(kuò)散(或者說是解構(gòu))
- 前提:針對(duì)的是響應(yīng)式對(duì)象(reactive封裝的對(duì)象)
- 本質(zhì):不創(chuàng)建響應(yīng)式(創(chuàng)建是ref和reactive的事),而是延續(xù)響應(yīng)式
4. watch和watchEffect的區(qū)別
-
watch和watchEffect都可以監(jiān)聽data的變化 -
watch需要指定監(jiān)聽的屬性,默認(rèn)初始時(shí)不會(huì)觸發(fā),如果初始要觸發(fā),需要配置immediate: true -
watchEffect是不需要指定監(jiān)聽的屬性,而是自動(dòng)監(jiān)聽其用到的屬性,它初始化時(shí),一定會(huì)執(zhí)行一次,這是為了收集要監(jiān)聽的屬性
<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
})
// 監(jiān)聽ref變量
watch(numberRef, (newVal, oldVal) => {
console.log('watch:', newVal, oldVal);
// watch: 1000 undefined
// watch: 200 1000
}, {
immediate: true // 第三個(gè)參數(shù)可選,是一些配置項(xiàng),immediate表示初始化時(shí)就執(zhí)行監(jiān)聽
})
setTimeout(() => {
numberRef.value = 200
}, 1000)
// 監(jiān)聽對(duì)象
watch(
// 第一參數(shù)是監(jiān)聽對(duì)象,如果是對(duì)象需要使用函數(shù)返回形式
() => state.age,
// 第二個(gè)參數(shù)是監(jiān)聽的變化值
(newVal, oldVal) => {
console.log('watch:', newVal, oldVal);
// watch: 3 undefined
// watch: 18 3
},
// 第三個(gè)參數(shù)是配置項(xiàng)
{
immediate: true, // 初始變化就監(jiān)聽
deep: true // 深度監(jiān)聽
}
)
setTimeout(() => {
state.age = 18
}, 1000)
return {
numberRef,
...toRefs(state)
}
}
}
</script>

// watchEffect監(jiān)聽
watchEffect(() => {
console.log('watchEffect');
console.log(numberRef.value);
console.log(state.age);
})

5. 在setup中怎么獲取組件實(shí)例
- 在
setup和其它compostion API中沒有this - 如果一定要獲取,要使用
getCurrentInstance獲取,并且在掛載后才可獲取數(shù)據(jù) - 如果是
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來獲取組件實(shí)例
// 且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升級(jí)了哪些重要的功能
參考官網(wǎng)升遷指南
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向父組件發(fā)出事件 - 在子組件中,需要
emits聲明向父組件發(fā)出的事件集合
<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>
<!-- 可以同時(shí)觸發(fā)多個(gè)事件 -->
<button @click="one($event), two($event)">觸發(fā)多事件</button>
</template>
fragment
vue2只允許template中只有一個(gè)元素,如果多個(gè)元素,必須用一個(gè)元素包裹
vue3則允許template中可以直接有多個(gè)元素,這樣就可以減少dom層級(jí)
移除.sync
vue2中的.sync
vue2中使用.sync是對(duì)以下語句的語法糖,父組件通過v-bind:xxx.sync='xxx'來向子組件說明這個(gè)屬性是雙向綁定的,子組件中通過$emit('update:xxx', newVal)來更新這個(gè)值
<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方式說明這個(gè)屬性是雙向綁定的,子組件中通過$emit('update:xxx', newVal)來更新這個(gè)值
<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>
異步組件的導(dǎo)入方式不一樣
vue2: child: () => import('child.vue')
vue3:需要defineAsyncComponent導(dǎo)入,child: defineAsyncComponent(() => import('child.vue'))
teleport
teleport將我們的模板移動(dòng)到DOM 中 Vue app之外的其他位置,比如可以使用teleport標(biāo)簽將組件在body層
<template>
<p>這是放在當(dāng)前組件下的內(nèi)容</p>
<teleport to="body">
<p>假設(shè)這是個(gè)彈窗,直接放到body下</p>
</teleport>
</template>
