Vue2->Vue3
如果有Vue2的基礎(chǔ),并在此基礎(chǔ)上學(xué)習(xí)Vue3,并不需要把完整的官網(wǎng)看完,我們只需要關(guān)注一些新功能和非兼容的變化即可進(jìn)行開(kāi)發(fā)。
Vue3變化
- 同一元素上使用的
v-if和v-for優(yōu)先級(jí)已更改,但不推薦同時(shí)使用v-if和v-for。 - 組件事件需要在
emits選項(xiàng)中聲明 -
destroyed生命周期選項(xiàng)被重命名為unmounted -
beforeDestroy生命周期選項(xiàng)被重命名為beforeUnmount - 自定義指令的API已更改為與組件生命周期一致
- 新增了三個(gè)組件:
Fragment支持多個(gè)根節(jié)點(diǎn)、Suspense可以在組件渲染之前的等待時(shí)間顯示指定內(nèi)容、Teleport可以讓子組件能夠在視覺(jué)上跳出父組件(如父組件overflow:hidden) - 新增指令
v-memo,可以緩存 html 模板,比如 v-for 列表不會(huì)變化的就緩存,簡(jiǎn)單說(shuō)就是用內(nèi)存換時(shí)間。 - 用
Proxy代替 Object.defineProperty 重構(gòu)了響應(yīng)式系統(tǒng),可以監(jiān)聽(tīng)到數(shù)組下標(biāo)變化,及對(duì)象新增屬性,因?yàn)楸O(jiān)聽(tīng)的不是對(duì)象屬性,而是對(duì)象本身,還可攔截 apply、has 等13種方法 - 重構(gòu)了虛擬 DOM,在編譯時(shí)會(huì)將事件緩存、將 slot 編譯為 lazy 函數(shù)、保存靜態(tài)節(jié)點(diǎn)直接復(fù)用(靜態(tài)提升)、以及添加靜態(tài)標(biāo)記、Diff 算法使用 最長(zhǎng)遞增子序列 優(yōu)化了對(duì)比流程,使得虛擬 DOM 生成速度提升
200% - 支持在
<style></style>里使用v-bind,給 CSS 綁定 JS 變量(color: v-bind(str)) - 新增
Composition API可以更好的邏輯復(fù)用和代碼組織,同一功能的代碼不至于像以前一樣太分散,雖然 Vue2 中可以用 mixin 來(lái)實(shí)現(xiàn)復(fù)用代碼,但也存在問(wèn)題,比如方法或?qū)傩悦麜?huì)沖突,代碼來(lái)源也不清楚等 - 全局函數(shù)
set和delete以及實(shí)例方法$set和$delete移除?;诖淼淖兓瘷z測(cè)已經(jīng)不再需要它們了 - 畢竟 Vue3 是用
TS寫(xiě)的,所以對(duì)TS的支持度更好 - Vue3 不兼容
IE11 -
$on,$off和$once實(shí)例方法已被移除,組件實(shí)例不再實(shí)現(xiàn)事件觸發(fā)接口。
三、組合式API
原有的組件選項(xiàng)(data、computed、methods、watch) 的方式來(lái)組織組件代碼通常是非常有效的,但是也存在一些不好的地方,例如把原有的關(guān)聯(lián)邏輯按照選項(xiàng)劃分開(kāi)來(lái),掩蓋了原有潛在的邏輯問(wèn)題,這個(gè)時(shí)候我們就必須要不斷地上下滾動(dòng)代碼來(lái)找到響應(yīng)的代碼塊來(lái)查找,這樣帶來(lái)了極大的不便,特別是一開(kāi)始沒(méi)有編寫(xiě)過(guò)這組件的人來(lái)說(shuō),這導(dǎo)致組件難以閱讀和理解。
所以針對(duì)上述的情況,Vue3提出了新的組織組件代碼的方式---組合式API。組合式API需要一個(gè)可以實(shí)際使用的地方,那就是setup。
setup的觸發(fā)時(shí)機(jī)是在組件創(chuàng)建之前執(zhí)行的。需要注意,在setup中你應(yīng)該避免使用this,因?yàn)檫@個(gè)時(shí)候this并不代表組件實(shí)例。setup的調(diào)用發(fā)生在data、computed和methods被解析之前,所以它們沒(méi)法在setup中被獲取。
當(dāng)然,我們依然可以在Vue3使用選項(xiàng)(Option API)的方式來(lái)組織代碼,這個(gè)和Vue2沒(méi)有區(qū)別,但是不建議這樣寫(xiě)。
Vue3.x組件的選項(xiàng)(Option API)寫(xiě)法(不建議)
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {},
mounted() {
console.log('生命周期mounted')
},
components: {},
methods: {},
watch: {},
computed: {}
});
</script>
Vue3.x組合式寫(xiě)法(推薦)
<script>
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
setup(){
let num = ref(0);
let fn = () => {};
onMounted(():void => {
console.log('生命周期mounted');
});
return {
num,
fn
}
}
})
</script>
下面例子代碼都是在Typescript的環(huán)境下進(jìn)行的,所以需要Typescript基礎(chǔ)。
1. 生命周期
通過(guò)在生命周期鉤子前面加上 “on” 來(lái)訪問(wèn)組件的生命周期鉤子。
下表包含如何在 Option API 和 setup() 內(nèi)部調(diào)用生命周期鉤子
| Option API | setup |
|---|---|
| beforeCreate | - |
| created | - |
| beforeMount | OnBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
| activated | onActivated |
| deactivated | onDeactivated |
因?yàn)?
setup是在beforeCreate和created生命周期鉤子前運(yùn)行的,所以不需要顯式地定義它們。換句話說(shuō),在這些鉤子中編寫(xiě)的任何代碼都應(yīng)該直接在setup函數(shù)中編寫(xiě)。
<template>
<div id="div">123</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
export default defineComponent({
setup() {
console.log("setup");
onMounted((): void => {
console.log("onMounted");
console.log(document.getElementById('div')?.innerHTML);
});
},
});
</script>
2. ref、reactive、toRefs響應(yīng)式和methods
Vue2.x默認(rèn)寫(xiě)在data的值,初始化的時(shí)候內(nèi)部會(huì)完成值的數(shù)據(jù)的響應(yīng)式(get、set綁定),但是Vue3要手動(dòng)調(diào)用內(nèi)置方法實(shí)現(xiàn),那么接下來(lái)看看常用的實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式方法都有哪些。
ref不僅可以用在數(shù)據(jù)的響應(yīng)式,還可以綁定
DOM元素
<template>
<div>
<div>{{ num }}</div>
<button @click="add1">num++</button>
<p>-------------------------------</p>
<div>{{ state.count }}</div>
<button @click="add2">state.count++</button>
<p>-------------------------------</p>
<div>{{ count }}</div>
<button @click="add3">count++</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs } from "vue";
export default defineComponent({
setup() {
interface ObjItf {
count: number;
}
// ref聲明響應(yīng)式數(shù)據(jù),用于聲明基本數(shù)據(jù)類(lèi)型
let num = ref<number>(1);
let obj = {
count: 1,
};
// reactive聲明響應(yīng)式數(shù)據(jù),用于聲明引用數(shù)據(jù)類(lèi)型
let state = reactive<ObjItf>(obj);
// toRefs解構(gòu)響應(yīng)式數(shù)據(jù)
let { count } = toRefs<ObjItf>(state);
const add1 = (): void => {
num.value++; // 注意通過(guò)ref聲明的變量,所以js要修改對(duì)應(yīng)的值是要通過(guò).value訪問(wèn)才可以,template模板不需要通過(guò).value訪問(wèn)
};
const add2 = (): void => {
state.count++; // 通過(guò)reactive聲明的遍歷,不需要通過(guò).value訪問(wèn)值
};
const add3 = (): void => {
count.value++; // 通過(guò)toRefs結(jié)構(gòu)的值和ref聲明的變量一樣,需要通過(guò).value訪問(wèn)其值
};
return {
num,
state,
count,
add1,
add2,
add3
};
},
});
</script>
注意:
- reactive可以傳遞基礎(chǔ)數(shù)據(jù)類(lèi)型和引用數(shù)據(jù)類(lèi)型,基礎(chǔ)數(shù)據(jù)類(lèi)型不會(huì)被包裝成響應(yīng)式數(shù)據(jù)
- reactive返回的響應(yīng)式數(shù)據(jù)本質(zhì)是Proxy對(duì)象,對(duì)象里面每一層都會(huì)被包裝成Proxy對(duì)象
- reactive返回的響應(yīng)式數(shù)據(jù)和原始的數(shù)據(jù)會(huì)相互影響
- ref可以傳遞基礎(chǔ)數(shù)據(jù)類(lèi)型和引用數(shù)據(jù)類(lèi)型。如果是基礎(chǔ)數(shù)據(jù)類(lèi)型,那么這個(gè)基礎(chǔ)數(shù)據(jù)值保存在返回的響應(yīng)式數(shù)據(jù)的
.value上;如果是對(duì)象,響應(yīng)式數(shù)據(jù)在.value上。 - ref本質(zhì)是將一個(gè)數(shù)據(jù)變成一個(gè)對(duì)象,這個(gè)對(duì)象具有響應(yīng)式特點(diǎn)
為什么需要toRefs和toRef?
和ref不一樣的是,toRef和toRefs整兩個(gè)方法,它們不創(chuàng)造響應(yīng)式,而是延續(xù)響應(yīng)式。創(chuàng)造響應(yīng)式一般由ref和reactive來(lái)解決,而toRef和toRefs則把對(duì)象的數(shù)據(jù)進(jìn)行分解和擴(kuò)散,其這個(gè)對(duì)象針對(duì)的是響應(yīng)式對(duì)象(reactive)而非普通對(duì)象。
3. watch
語(yǔ)法:watch(監(jiān)聽(tīng)源|[多個(gè)], (val, oldVal) => {}, {immediate?: false, deep: false})
watch寫(xiě)法上支持一個(gè)或者多個(gè)監(jiān)聽(tīng)源,這些監(jiān)聽(tīng)源必須只能是getter/effect函數(shù),ref數(shù)據(jù),reactive對(duì)象,或者是數(shù)組類(lèi)型
<template>
<div>
<div>{{ count }}</div>
<button @click="add">count++</button>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, watch } from "vue";
export default defineComponent({
setup() {
interface ObjItf {
count: number;
}
let obj = {
count: 1,
};
// reactive聲明響應(yīng)式數(shù)據(jù),用于聲明引用數(shù)據(jù)類(lèi)型
let state = reactive<ObjItf>(obj);
// toRefs解構(gòu)響應(yīng)式數(shù)據(jù)
let { count } = toRefs<ObjItf>(state);
const add = (): void => {
count.value++;
};
watch(count, (newVal: number, oldVal: number): void => {
console.log(newVal, oldVal);
});
return {
count,
add,
};
},
});
</script>
4. watchEffect
它立即執(zhí)行傳入的一個(gè)函數(shù),同時(shí)響應(yīng)式追蹤其依賴(lài),并在其依賴(lài)變更時(shí)重新運(yùn)行該函數(shù)。
<script>
let num = ref(0);
watchEffect(() => {
console.log(num.value);
});
setTimeout(() => {
num.value++;
}, 1000);
</script>
watch 和 watchEffect的區(qū)別
- 兩者都可以監(jiān)聽(tīng)
data屬性變化; -
watch需要明確監(jiān)聽(tīng)哪個(gè)屬性; - 而
watchEffect會(huì)根據(jù)其中的屬性,自動(dòng)監(jiān)聽(tīng)其變化。
5. computed
<template>
<div>{{ count }}</div>
<button @click="state.count++">state.count++</button>
</template>
<script lang="ts">
import { defineComponent, reactive, computed } from "vue";
export default defineComponent({
setup() {
interface ObjItf {
count: number;
}
let obj = {
count: 1,
arr: [1, 2, 3]
};
const state = reactive<ObjItf>(obj);
let count = computed((): number => {
return state.count;
});
return {
count,
state
};
},
});
</script>
6. 組件
全局組件:
const app = Vue.createApp({...})
app.component('my-component-name', {
/* ... */
})
局部組件:
子組件
<template>
<div>{{ aname }}</div>
</template>
<script lang="ts">
import { defineComponent, toRefs } from "vue";
export default defineComponent({
props: {
aname: {
type: String,
default: "張三",
},
},
emits: ["uname"],
setup(props, { emit }) {
let { aname } = toRefs(props);
const updateName = () => {
emit('uname', 'a改變后的名字')
};
return {
aname,
updateName,
};
},
});
</script>
父組件
<template>
<mychild :aname="aname" @uname="updateName"></mychild>
</template>
<script lang="ts">
import { defineComponent, toRefs, ref } from "vue";
import mychild from './mychild.vue';
export default defineComponent({
components: {
mychild
},
setup() {
let aname = ref('李四');
const updateName = (p: string) => {
aname.value = p;
}
return {
aname,
updateName
}
}
})
</script>
setup語(yǔ)法糖組件
子組件
<!-- -->
<template>
<div>{{ aname }}</div>
<button @click="updateName">修改姓名</button>
</template>
<script lang="ts" setup>
// setup語(yǔ)法糖下defineProps,defineEmits 不需要引入
let emit = defineEmits(["updatename"]);
let props = defineProps({
aname: {
type: String,
default: "李四",
},
});
const updateName = (): void => {
emit('updatename', '修改后的名字')
};
</script>
<style lang="less" scoped></style>
父組件
<!-- -->
<template>
<div></div>
<mychild :aname="aname" @updatename="updateName"></mychild>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import mychild from "./b.vue";
let aname = ref("張三");
let updateName = (name: string): void => {
aname.value = name;
};
</script>
<style lang="less" scoped></style>
7. v-model
子組件
<!-- -->
<template>
<div>{{ name }}{{ age }}</div>
<button @click="updateName">修改姓名</button>
</template>
<script lang="ts" setup>
let props = defineProps({
age: Number,
name: String,
});
let emits = defineEmits(["update:name", "update:age"]);
let updateName = () => {
emits("update:age", 30);
emits("update:name", "李四");
};
</script>
<style lang="less" scoped></style>
父組件
<!-- -->
<template>
<div></div>
<mychild v-model:name="state.name" v-model:age="state.age"></mychild>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import mychild from "./b.vue";
let info = {
name: '張三',
age: 20
}
let state = reactive(info);
</script>
<style lang="less" scoped></style>
8. 插槽
子組件
<!-- -->
<template>
<slot></slot>
<slot name="title"></slot>
<slot name="footer" :user="state.user" :d="state.d"></slot>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
let state = reactive({
user: { a: 1, b: 2 },
d: 1,
});
</script>
<style lang="less" scoped></style>
父組件
<!-- -->
<template>
<div></div>
<mychild>
<div>匿名插槽</div>
<template #title>
<div>具名插槽</div>
</template>
<template #footer="scope">
<div>作用域插槽{{ scope.user }} {{ scope.d }}</div>
</template>
</mychild>
</template>
<script lang="ts" setup>
import mychild from "./child.vue";
</script>
<style lang="less" scoped></style>
9. await支持
不必再配合 async 就可以直接使用 await 了,這種情況下,組件的 setup 會(huì)自動(dòng)變成 async setup 。
<script setup>
const post = await fetch('/api').then(() => {})
</script>
10. style scoped
<style scoped>
/* 修改第三方組件樣式 */
::v-deep(.foo) {}
/* 簡(jiǎn)寫(xiě) */
:deep(.foo) {}
/* 修改插槽內(nèi)容樣式 */
::v-slotted(.foo) {}
/* 簡(jiǎn)寫(xiě) */
:slotted(.foo) {}
/* 修改全局樣式 */
::v-global(.foo) {}
/* 簡(jiǎn)寫(xiě) */
:global(.foo) {}
</style>
11. teleport
這個(gè)組件可以把組件進(jìn)行傳送,to屬性就是要傳送的位置目標(biāo),用css選擇器
<template>
<div class="header"></div>
<teleport :to="target">
<p>哈哈哈哈哈</p>
</teleport>
<button @click="changeTarget">點(diǎn)擊切換</button>
<p>-------------------------------</p>
<div class="footer"></div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
let target = ref<string>(".header");
const changeTarget = (): void => {
target.value = ".footer";
};
</script>
<style lang="less" scoped></style>