這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 vue3 於 2020 年 09 月 18 日正式發佈,2022 年 2 月 7 日 vue3 成為新的預設版本 距離 vue3 正式發佈已經過去兩年有餘, 成為預設版本也過去大半年了,以前還能說是對新技術、新特性的觀望,而現在面試都直問 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
vue3 於 2020 年 09 月 18 日正式發佈,2022 年 2 月 7 日 vue3 成為新的預設版本
距離 vue3 正式發佈已經過去兩年有餘, 成為預設版本也過去大半年了,以前還能說是對新技術、新特性的觀望,而現在面試都直問 vue3 源碼了。
我想,不管什麼原因,是時候學習 vue3 了
所以這次我也順便把學習的過程記錄下來,算個總結,也便於日後的梳理。
前置介紹
在 vue3.2 中,我們只需在script標簽中添加setup。就可以做到,組件只需引入不用註冊,屬性和方法也不用 return 才能於 template 中使用,也不用寫setup函數,也不用寫export default ,甚至是自定義指令也可以在我們的template中自動獲得。
本次我們的學習也將在 setup 語法糖下進行。
環境搭建
npm init vue@latest
使用工具
<script setup lang="ts"> + VSCode + Volar
安裝 Volar 後,註意禁用 vetur
好的,準備工作已經完成,下麵我們開始進入到 vue3 setup 的正式學習
ref 和 reactive
- ref: 用來給基本數據類型綁定響應式數據,訪問時需要通過 .value 的形式, tamplate 會自動解析,不需要 .value
- reactive: 用來給 複雜數據類型 綁定響應式數據,直接訪問即可
ref其實也是內部調用 reactive 來實現的
<template> <div> <p>{{title}}</p> <h4>{{userInfo}}</h4> </div> </template> <script setup lang="ts"> import { ref, reactive } from "vue"; type Person = { name: string; age: number; gender?: string; }; const title = ref<string>("彼時彼刻,恰如此時此刻"); const userInfo = reactive<Person>({ name: '樹哥', age: 18 }) </script>
toRef、toRefs、toRaw
toRef
toRef 如果原始對象是非響應式的,數據會變,但不會更新視圖
<template> <div> <button @click="change">按鈕</button> {{state}} </div> </template> <script setup lang="ts"> import { reactive, toRef } from 'vue' const obj = { name: '樹哥', age: 18 } const state = toRef(obj, 'age') const change = () => { state.value++ console.log('obj:',obj,'state:', state); } </script>
可以看到,點擊按鈕,當原始對象是非響應式時,使用toRef 的數據改變,但是試圖並沒有更新
<template> <div> <button @click="change">按鈕</button> {{state}} </div> </template> <script setup lang="ts"> import { reactive, toRef } from 'vue' const obj = reactive({ name: '樹哥', age: 18 }) const state = toRef(obj, 'age') const change = () => { state.value++ console.log('obj:', obj, 'state:', state); } </script>
當我們把 obj 用 reactive 包裹,再使用 toRef,點擊按鈕時,可以看到視圖和數據都變了
toRef返回的值是否具有響應性取決於被解構的對象本身是否具有響應性。響應式數據經過toRef返回的值仍具有響應性,非響應式數據經過toRef返回的值仍沒有響應性。
toRefs
toRefs相當於對對象內每個屬性調用toRef,toRefs返回的對象內的屬性使用時需要加.value,主要是方便我們解構使用
<template> <div> <button @click="change">按鈕</button> name--{{name}}---age{{age}} </div> </template> <script setup lang="ts"> import { reactive, toRefs } from 'vue' const obj = reactive({ name: '樹哥', age: 18 }) let { name, age } = toRefs(obj) const change = () => { age.value++ name.value = '張麻子' console.log('obj:', obj); console.log('name:', name); console.log('age:', age); } </script>
簡單理解就是批量版的toRef,(其源碼實現也正是通過對象迴圈調用了toRef)
toRaw
將響應式對象修改為普通對象
<template> <div> <button @click="change">按鈕</button> {{data}} </div> </template> <script setup lang="ts"> import { reactive, toRaw } from 'vue' const obj = reactive({ name: '樹哥', age: 18 }) const data = toRaw(obj) const change = () => { data.age = 19 console.log('obj:', obj, 'data:', data); } </script>
數據能變化,視圖不變化(失去響應式)
computed
<template> <div> <p>{{title}}</p> <h4>{{userInfo}}</h4> <h1>{{add}}</h1> </div> </template> <script setup lang="ts"> import { ref, reactive,computed } from "vue"; const count = ref(0) // 推導得到的類型:ComputedRef<number> const add = computed(() => count.value +1) </script>
watch
vue3 watch 的作用和 Vue2 中的 watch 作用是一樣的,他們都是用來監聽響應式狀態發生變化的,當響應式狀態發生變化時,就會觸發一個回調函數。
watch(data,()=>{},{})
-
參數一,監聽的數據
-
參數二,數據改變時觸發的回調函數(newVal,oldVal)
-
參數三,options配置項,為一個對象
-
1、監聽ref定義的一個響應式數據
<script setup lang="ts"> import { ref, watch } from "vue"; const str = ref('彼時彼刻') //3s後改變str的值 setTimeout(() => { str.value = '恰如此時此刻' }, 3000) watch(str, (newV, oldV) => { console.log(newV, oldV) //恰如此時此刻 彼時彼刻 }) </script>
- 2、監聽多個ref
這時候寫法變為數組的形式
<script setup lang="ts"> import { ref, watch } from "vue"; let name = ref('樹哥') let age = ref(18) //3s後改變值 setTimeout(() => { name.value = '我叫樹哥' age.value = 19 }, 3000) watch([name, age], (newV, oldV) => { console.log(newV, oldV) // ['我叫樹哥', 19] ['樹哥', 18] }) </script>
- 3、監聽Reactive定義的響應式對象
<script setup lang="ts"> import { reactive, watch } from "vue"; let info = reactive({ name: '樹哥', age: 18 }) //3s後改變值 setTimeout(() => { info.age = 19 }, 3000) watch(info, (newV, oldV) => { console.log(newV, oldV) }) </script>
當 watch 監聽的是一個響應式對象時,會隱式地創建一個深層偵聽器,即該響應式對象裡面的任何屬性發生變化,都會觸發監聽函數中的回調函數。即當 watch 監聽的是一個響應式對象時,預設開啟 deep:true
- 4、監聽reactive 定義響應式對象的單一屬性
錯誤寫法:
<script setup lang="ts"> import { reactive, watch } from "vue"; let info = reactive({ name: '樹哥', age: 18 }) //3s後改變值 setTimeout(() => { info.age = 19 }, 3000) watch(info.age, (newV, oldV) => { console.log(newV, oldV) }) </script>
可以看到控制台出現警告
[Vue warn]: Invalid watch source: 18 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. at <Index> at <App>
如果我們非要監聽響應式對象中的某個屬性,我們可以使用 getter 函數的形式,即將watch第一個參數修改成一個回調函數的形式
正確寫法:
// 其他不變 watch(()=>info.age, (newV, oldV) => { console.log(newV, oldV) // 19 18 })
- 5、監聽reactive定義的 引用數據
<script setup lang="ts"> import { reactive, watch } from "vue"; let info = reactive({ name: '張麻子', age: 18, obj: { str: '彼時彼刻,恰如此時此刻' } }) //3s後改變s值 setTimeout(() => { info.obj.str = 'to be or not to be' }, 3000) // 需要自己開啟 deep:true深度監聽,不然不發觸發 watch 的回調函數 watch(() => info.obj, (newV, oldV) => { console.log(newV, oldV) }, { deep: true }) </script>
WatchEffect
會立即執行傳入的一個函數,同時響應式追蹤其依賴,併在其依賴變更時重新運行該函數。(有點像計算屬性)
如果用到 a 就只會監聽 a, 就是用到幾個監聽幾個 而且是非惰性,會預設調用一次
<script setup lang="ts"> import { ref, watchEffect } from "vue"; let num = ref(0) //3s後改變值 setTimeout(() => { num.value++ }, 3000) watchEffect(() => { console.log('num 值改變:', num.value) }) </script>
可以在控制臺上看到,第一次進入頁面時,列印出num 值改變:0
,三秒後,再次列印num 值改變:1
- 停止監聽
當 watchEffect 在組件的 setup() 函數或生命周期鉤子被調用時,偵聽器會被鏈接到該組件的生命周期,併在組件卸載時自動停止。
但是我們採用非同步的方式創建了一個監聽器,這個時候監聽器沒有與當前組件綁定,所以即使組件銷毀了,監聽器依然存在。
這個時候我們可以顯式調用停止監聽
<script setup lang="ts"> import { watchEffect } from 'vue' // 它會自動停止 watchEffect(() => {}) // ...這個則不會! setTimeout(() => { watchEffect(() => {}) }, 100) const stop = watchEffect(() => { /* ... */ }) // 顯式調用 stop() </script>
- 清除副作用(onInvalidate)
watchEffect 的第一個參數——effect函數——可以接收一個參數:叫onInvalidate,也是一個函數,用於清除 effect 產生的副作用
就是在觸發監聽之前會調用一個函數可以處理你的邏輯,例如防抖
import { ref, watchEffect } from "vue"; let num = ref(0) //3s後改變值 setTimeout(() => { num.value++ }, 3000) watchEffect((onInvalidate) => { console.log(num.value) onInvalidate(() => { console.log('執行'); }); })
控制台依次輸出:0 => 執行 => 1
- 配置選項
watchEffect的第二個參數,用來定義副作用刷新時機,可以作為一個調試器來使用
flush (更新時機):
- 1、pre:組件更新前執行
- 2、sync:強制效果始終同步觸發
- 3、post:組件更新後執行
<script setup lang="ts"> import { ref, watchEffect } from "vue"; let num = ref(0) //3s後改變值 setTimeout(() => { num.value++ }, 3000) watchEffect((onInvalidate) => { console.log(num.value) onInvalidate(() => { console.log('執行'); }); }, { flush: "post", //此時這個函數會在組件更新之後去執行 onTrigger(e) { //作為一個調試工具,可在開發中方便調試 console.log('觸發', e); }, }) </script>
生命周期
和 vue2 相比的話,基本上就是將 Vue2 中的beforeDestroy名稱變更成beforeUnmount; destroyed 表更為 unmounted;然後用setup代替了兩個鉤子函數 beforeCreate 和 created;新增了兩個開發環境用於調試的鉤子
父子組件傳參
defineProps
父組件傳參
<template> <Children :msg="msg" :list="list"></Children> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Children from './Children.vue' const msg = ref('hello 啊,樹哥') const list = reactive<number[]>([1, 2, 3]) </script>
在 script setup 中,引入的組件會自動註冊,所以可以直接使用,無需再通過components進行註冊
子組件接受值
defineProps 來接收父組件傳遞的值, defineProps是無須引入的直接使用即可
<template> <div> <p>msg:{{msg}}</p> <p>list:{{list}}</p> </div> </template> <script setup lang="ts"> defineProps<{ msg: string, list: number[] }>() </script>
使用 withDefaults 定義預設值
<template> <div> <p>msg:{{msg}}</p> <p>list:{{list}}</p> </div> </template> <script setup lang="ts"> type Props = { msg?: string, list?: number[] } // withDefaults 的第二個參數便是預設參數設置,會被編譯為運行時 props 的 default 選項 withDefaults(defineProps<Props>(), { msg: '張麻子', list: () => [4, 5, 6] }) </script>
子組件向父組件拋出事件
defineEmits
子組件派發事件
<template> <div> <p>msg:{{msg}}</p> <p>list:{{list}}</p> <button @click="onChangeMsg">改變msg</button> </div> </template> <script setup lang="ts"> type Props = { msg?: string, list?: number[] } withDefaults(defineProps<Props>(), { msg: '張麻子', list: () => [4, 5, 6] }) const emits = defineEmits(['changeMsg']) const onChangeMsg = () => { emits('changeMsg','黃四郎') } </script>
子組件綁定了一個click 事件 然後通過defineEmits 註冊了一個自定義事件,點擊按鈕的時候,觸發 emit 調用我們註冊的事件,傳遞參數
父組件接收
<template> <Children :msg="msg" :list="list" @changeMsg="changeMsg"></Children> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import Children from './Children.vue' const msg = ref('hello 啊,樹哥') const list = reactive<number[]>([1, 2, 3]) const changeMsg = (v: string) => { msg.value = v } </script>
defineExpose 獲取子組件的實例和內部屬性
在 script-setup 模式下,所有數據只是預設 return 給 template 使用,不會暴露到組件外,所以父組件是無法直接通過掛載 ref 變數獲取子組件的數據。
如果要調用子組件的數據,需要先在子組件顯示的暴露出來,才能夠正確的拿到,這個操作,就是由 defineExpose 來完成。
子組件
<template> <p>{{name}}</p> </template> <script lang="ts" setup> import { ref } from 'vue' const name = ref('張麻子') const changeName = () => { name.value = '縣長' } // 將方法、變數暴露給父組件使用,父組件才可通過 ref API拿到子組件暴露的數據 defineExpose({ name, changeName }) </script>
父組件
<template> <div> <child ref='childRef' /> <button @click="getName">獲取子組件中的數據</button> </div> </template> <script lang="ts" setup> import { ref } from 'vue' import child from './Child.vue' // 子組件ref(TypeScript語法) const childRef = ref<InstanceType<typeof child>>() const getName = () => { // 獲取子組件name console.log(childRef.value!.name) // 執行子組件方法 childRef.value?.changeName() // 獲取修改後的name console.log(childRef.value!.name) } </script>
註意:defineProps 、defineEmits 、 defineExpose 和 withDefaults 這四個巨集函數只能在
<script setup>
中使用。他們不需要導入,會隨著<script setup>
的處理過程中一起被編譯。
插槽
在 Vue2 的中一般中具名插槽和作用域插槽分別使用slot和slot-scope來實現,如:
父組件
<template> <div> <p style="color:red">父組件</p> <Child ref='childRef'> <template slot="content" slot-scope="{ msg }"> <div>{{ msg }}</div> </template> </Child> </div> </template> <script lang="ts" setup> import Child from './Child.vue' </script>
子組件
<template> <div>child</div> <slot name="content" msg="hello 啊,樹哥!"></slot> </template>
在 Vue3 中將slot和slot-scope進行了合併統一使用,使用 v-slot, v-slot:slotName
簡寫 #slotName
父組件
<template> <div> <p style="color:red">父組件</p> <Child> <template v-slot:content="{ msg }"> <div>{{ msg }}</div> </template> </Child> </div> </template> <script lang="ts" setup> import Child from './Child.vue' </script> <!-- 簡寫 --> <Child> <template #content="{ msg }"> <div>{{ msg }}</div> </template> </Child>
實際上,v-slot 在 Vue2.6+ 的版本就可以使用。
非同步組件
通過 defineAsyncComponent 非同步載入
<template> <Children :msg="msg" :list="list" @changeMsg="changeMsg"></Children> </template> <script setup lang="ts"> import { ref, reactive,defineAsyncComponent } from 'vue' // import Children from './Children.vue' const Children = defineAsyncComponent(() => import('./Children.vue')) </script>
Suspense
Suspense 允許應用程式在等待非同步組件時渲染一些其它內容,在 Vue2 中,必須使用條件判斷(例如 v-if、 v-else等)來檢查數據是否已載入並顯示一些其它內容;但是,在 Vue3 新增了 Suspense 了,就不必跟蹤何時載入數據並呈現相應的內容。
他是一個帶插槽的組件,只是它的插槽指定了default 和 fallback 兩種狀態。
Suspense 使用:
- 1、使用
<Suspense></Suspense>
包裹所有非同步組件相關代碼 - 2、
<template v-slot:default></template>
插槽包裹非同步組件 - 3、
<template v-slot:fallback></template>
插槽包裹渲染非同步組件渲染之前的內容
<template> <Suspense> <template #default> <!-- 非同步組件-預設渲染的頁面 --> <Children :msg="msg" :list="list" @changeMsg="changeMsg"></Children> </template> <template #fallback> <!-- 頁面還沒載入出來展示的頁面 --> <div>loading...</div> </template> </Suspense> </template> <script setup lang="ts"> import { ref, reactive, defineAsyncComponent } from 'vue' const Children = defineAsyncComponent(() => import('./Children.vue')) </script>
Teleport傳送組件
Teleport 是一種能夠將我們的模板渲染至指定DOM節點,不受父級style、v-show等屬性影響,但data、prop數據依舊能夠共用的技術
主要解決的問題:因為Teleport節點掛載在其他指定的DOM節點下,完全不受父級style樣式影響
使用: 通過to 屬性插入到指定元素位置,如 body,html,自定義className等等。
<template> <!-- 插入至 body --> <Teleport to="body"> <Children></Children> </Teleport> <!-- 預設 #app 下 --> <Children></Children> </template> <script lang="ts" setup> import Children from './Children.vue' </script>
keep-alive 緩存組件
- 作用和vue2一致,只是生命周期名稱有所更改
- 初次進入時: onMounted> onActivated
- 退出後觸發 deactivated
- 再次進入:只會觸發 onActivated
事件掛載的方法等,只執行一次的放在 onMounted中;組件每次進去執行的方法放在 onActivated中
provide/inject
provide 可以在祖先組件中指定我們想要提供給後代組件的數據或方法,而在任何後代組件中,我們都可以使用 inject 來接收 provide 提供的數據或方法。
父組件
<template> <Children></Children> </template> <script setup lang="ts"> import { ref, provide } from 'vue' import Children from "./Children.vue" const msg = ref('hello 啊,樹哥') provide('msg', msg) </script>
子組件
<template> <div> <p>msg:{{msg}}</p> <button @click="onChangeMsg">改變msg</button> </div> </template> <script setup lang="ts"> import { inject, Ref, ref } from 'vue' const msg = inject<Ref<string>>('msg',ref('hello啊!')) const onChangeMsg = () => { msg.value = 'shuge' } </script>
如果你想要傳入的值能響應式的改變,需要通過ref 或 reactive 添加響應式
v-model 升級
v-model 在vue3可以說是破壞式更新,改動還是不少的
我們都知道,v-model 是props 和 emit 組合而成的語法糖,vue3中 v-model 有以下改動
- 變更:value => modelValue
- 變更:update:input => update:modelValue
- 新增:一個組件可以設置多個 v-model
- 新增:開發者可以自定義 v-model修飾符
- v-bind 的 .sync 修飾符和組件的 model 選項已移除
子組件
<template> <div> <p>{{msg}},{{modelValue}}</p> <button @click="onChangeMsg">改變msg</button> </div> </template> <script setup lang="ts"> type Props = { modelValue: string, msg: string } defineProps<Props>() const emit = defineEmits(['update:modelValue', 'update:msg']) const onChangeMsg = () => { // 觸發父組件的值更新 emit('update:modelValue', '恰如此時此刻') emit('update:msg', '彼時彼刻') } </script>
父組件
<template> // v-model:modelValue簡寫為v-model // 綁定多個v-model <Children v-model="name" v-model:msg="msg"></Children> </template> <script setup lang="ts"> import { ref } from 'vue' import Children from "./Children.vue" const msg = ref('hello啊') const name = ref('樹哥') </script>
自定義指令
自定義指令的生命周期
- created 元素初始化的時候
- beforeMount 指令綁定到元素後調用 只調用一次
- mounted 元素插入父級dom調用
- beforeUpdate 元素被更新之前調用
- update 這個周期方法被移除 改用updated
- beforeUnmount 在元素被移除前調用
- unmounted 指令被移除後調用 只調用一次
實現一個自定義拖拽指令
<template> <div v-move class="box"> <div class="header"></div> <div> 內容 </div> </div> </template> <script setup lang='ts'> import { Directive } from "vue"; const vMove: Directive = { mounted(el: HTMLElement) { let moveEl = el.firstElementChild as HTMLElement; const mouseDown = (e: MouseEvent) => { //滑鼠點擊物體那一刻相對於物體左側邊框的距離=點擊時的位置相對於瀏覽器最左邊的距離-物體左邊框相對於瀏覽器最左邊的距離 console.log(e.clientX, e.clientY, "起始位置", el.offsetLeft); let X = e.clientX - el.offsetLeft; let Y = e.clientY - el.offsetTop; const move = (e: MouseEvent) => { el.style.left = e.clientX - X + "px"; el.style.top = e.clientY - Y + "px"; console.log(e.clientX, e.clientY, "位置改變"); }; document.addEventListener("mousemove", move); document.addEventListener("mouseup", () => { document.removeEventListener("mousemove", move); }); }; moveEl.addEventListener("mousedown", mouseDown); }, }; </script> <style > .box { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; border: 1px solid #ccc; } .header { height: 20px; background: black; cursor: move; } </style>
自定義 hooks
我們都知道在 vue 中有個東西叫 mixins,他可以將多個組件中相同的邏輯抽離出來,實現一次寫代碼,多組件受益的效果。
但是 mixins 的副作用就是引用的多了變數的來源就不清晰了,而且還會有變數來源不明確,不利於閱讀,容易使代碼變得難以維護。
- Vue3 的 hook函數 相當於 vue2 的 mixin, 不同在與 hooks 是函數
- Vue3 的 hook函數 可以幫助我們提高代碼的復用性, 讓我們能在不同的組件中都利用 hooks 函數
useWindowResize
我們來實現一個視窗改變時獲取寬高的 hook
import { onMounted, onUnmounted, ref } from "vue"; function useWindowResize() { const width = ref(0); const height = ref(0); function onResize() { width.value = window.innerWidth; height.value = window.innerHeight; } onMounted(() => { window.addEventListener("resize", onResize); onResize(); }); onUnmounted(() => { window.removeEventListener("resize", onResize); }); return { width, height }; } export default useWindowResize;
使用:
<template> <h3>屏幕尺寸</h3> <div>寬度:{{ width }}</div> <div>高度:{{ height }}</div> </template> <script setup lang="ts"> import useWindowResize from "../hooks/useWindowResize.ts"; const { width, height } = useWindowResize(); </script>
style v-bind CSS變數註入
<template> <span> style v-bind CSS變數註入</span> </template> <script lang="ts" setup> import { ref } from 'vue' const color = ref('red') </script> <style scoped> span { /* 使用v-bind綁定組件中定義的變數 */ color: v-bind('color'); } </style>