前言
Vue3.0 在去年 9 月正式發佈了,看大家都有在熱情的擁抱 Vue3.0。今年初新項目也開始使用 Vue3.0 來開發,這篇文章就是在使用後的一個總結, 包含 Vue3 新特性的使用以及一些使用經驗分享。
爲什麼要升級 Vue3
使用 Vue2.x 的小夥伴都熟悉,Vue2.x 中所有數據都是定義在data
中,方法定義在methods
中的,並且使用this
來調用對應的數據和方法。那 Vue3.x 中就可以不這麼玩了, 具體怎麼玩我們後續再說, 先說一下 Vue2.x 版本這麼寫有什麼缺陷,所有才會進行升級變更的。
回顧 Vue2.x 實現加減
<template>
<div class="homePage">
<p>count: {{ count }}</p>
<p>倍數: {{ multiple }}</p>
<div>
<button style="margin-right: 10px" @click="increase">加1</button>
<button @click="decrease">減一</button>
</div>
</div>
</template>
<script>
export default {
data() {
return { count: 0 };
},
computed: {
multiple() {
return 2 * this.count;
},
},
methods: {
increase() {
this.count++;
},
decrease() {
this.count--;
},
},
};
</script>
複製代碼
上面代碼只是實現了對count
的加減以及顯示倍數, 就需要分別在 data、methods、computed 中進行操作,當我們增加一個需求,就會出現下圖的情況:
當我們業務複雜了就會大量出現上面的情況, 隨着複雜度上升,就會出現這樣一張圖, 每個顏色的方塊表示一個功能:
甚至一個功能還有會依賴其他功能,全攪合在一起。
當這個組件的代碼超過幾百行時,這時增加或者修改某個需求, 就要在 data、methods、computed 以及 mounted 中反覆的跳轉,這其中的的痛苦寫過的都知道。
那我們就想啊, 如果可以按照邏輯進行分割,將上面這張圖變成下邊這張圖,是不是就清晰很多了呢, 這樣的代碼可讀性和可維護性都更高:
那麼 vue2.x 版本給出的解決方案就是 Mixin, 但是使用 Mixin 也會遇到讓人苦惱的問題:
- 命名衝突問題
- 不清楚暴露出來的變量的作用
- 邏輯重用到其他 component 經常遇到問題
關於上面經常出現的問題我就不一一舉例了,使用過的小夥伴多多少少都會遇到。文章的重點不是 Mixin, 如果確實想知道的就留言啦~
所以,我們 Vue3.x 就推出了Composition API
主要就是爲了解決上面的問題,將零散分佈的邏輯組合在一起來維護,並且還可以將單獨的功能邏輯拆分成單獨的文件。接下來我們就重點認識Composition API
。
Composition API
setup
setup 是 Vue3.x 新增的一個選項, 他是組件內使用 Composition API
的入口。
setup 執行時機
我在學習過程中看到很多文章都說 setup 是在 beforeCreate
和created
之間, 這個結論是錯誤的。實踐是檢驗真理的唯一標準, 於是自己去檢驗了一下:
export default defineComponent({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
});
複製代碼
setup 執行時機是在 beforeCreate 之前執行,詳細的可以看後面生命週期講解。
setup 參數
使用setup
時,它接受兩個參數:
- props: 組件傳入的屬性
- context
setup 中接受的props
是響應式的, 當傳入新的 props 時,會及時被更新。由於是響應式的, 所以不可以使用 ES6 解構,解構會消除它的響應式。 錯誤代碼示例, 這段代碼會讓 props 不再支持響應式:
// demo.vue
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
複製代碼
那在開發中我們想要使用解構,還能保持props
的響應式,有沒有辦法解決呢?大家可以思考一下,在後面toRefs
學習的地方爲大家解答。 接下來我們來說一下setup
接受的第二個參數context
,我們前面說了setup
中不能訪問 Vue2 中最常用的this
對象,所以context
中就提供了this
中最常用的三個屬性:attrs
、slot
和emit
,分別對應 Vue2.x 中的 $attr
屬性、slot
插槽 和$emit
發射事件。並且這幾個屬性都是自動同步最新的值,所以我們每次使用拿到的都是最新值。
reactive、ref 與 toRefs
在 vue2.x 中, 定義數據都是在data
中, 但是 Vue3.x 可以使用reactive
和ref
來進行數據定義。 那麼ref
和reactive
他們有什麼區別呢?分別什麼時候使用呢?說到這裏,我又不得不提一下,看到很多網上不少文章說 (reactive
用於處理對象的雙向綁定,ref
則處理 js 基礎類型的雙向綁定)。我其實不太贊同這樣的說法,這樣很容易初學者認爲ref
就能處理 js 基本類型, 比如ref
也是可以定義對象的雙向綁定的啊, 上段代碼:
setup() {
const obj = ref({count:1, name:"張三"})
setTimeout(() =>{
obj.value.count = obj.value.count + 1
obj.value.name = "李四"
}, 1000)
return{
obj
}
}
複製代碼
我們將obj.count
和obj.name
綁定到頁面上也是可以的;但是reactive
函數確實可以代理一個對象, 但是不能代理基本類型,例如字符串、數字、boolean 等。 接下來使用代碼展示一下ref
、reactive
的使用: 運行效果: 上面的代碼中,我們綁定到頁面是通過user.name
,user.age
;這樣寫感覺很繁瑣,我們能不能直接將user
中的屬性解構出來使用呢? 答案是不能直接對user
進行結構, 這樣會消除它的響應式, 這裏就和上面我們說props
不能使用 ES6 直接解構就呼應上了。那我們就想使用解構後的數據怎麼辦,解決辦法就是使用toRefs
。 toRefs 用於將一個 reactive 對象轉化爲屬性全部爲 ref 對象的普通對象。具體使用方式如下:
<template>
<div class="homePage">
<p>第 {{ year }} 年</p>
<p>姓名: {{ nickname }}</p>
<p>年齡: {{ age }}</p>
</div>
</template>
<script>
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
setup() {
const year = ref(0);
const user = reactive({ nickname: "xiaofan", age: 26, gender: "女" });
setInterval(() => {
year.value++;
user.age++;
}, 1000);
return {
year,
// 使用reRefs
...toRefs(user),
};
},
});
</script>
複製代碼
生命週期鉤子
我們可以直接看生命週期圖來認識都有哪些生命週期鉤子 (圖片是根據官網翻譯後繪製的): 從圖中我們可以看到 Vue3.0 新增了setup
,這個在前面我們也詳細說了, 然後是將 Vue2.x 中的beforeDestroy
名稱變更成beforeUnmount
; destroyed
表更爲 unmounted
,作者說這麼變更純粹是爲了更加語義化,因爲一個組件是一個mount
和unmount
的過程。其他 Vue2 中的生命週期仍然保留。 上邊生命週期圖
中並沒包含全部的生命週期鉤子, 還有其他的幾個, 全部生命週期鉤子如圖所示: 我們可以看到beforeCreate
和created
被setup
替換了(但是 Vue3 中你仍然可以使用, 因爲 Vue3 是向下兼容的, 也就是你實際使用的是 vue2 的)。其次,鉤子命名都增加了on
; Vue3.x 還新增用於調試的鉤子函數onRenderTriggered
和onRenderTricked
下面我們簡單使用幾個鉤子, 方便大家學習如何使用,Vue3.x 中的鉤子是需要從 vue 中導入的:
import { defineComponent, onBeforeMount, onMounted, onBeforeUpdate,onUpdated,
onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked,
onRenderTriggered } from "vue"; export default defineComponent({ //
beforeCreate和created是vue2的 beforeCreate() {
console.log("------beforeCreate-----"); }, created() {
console.log("------created-----"); }, setup() { console.log("------setup-----");
// vue3.x生命週期寫在setup中 onBeforeMount(() => {
console.log("------onBeforeMount-----"); }); onMounted(() => {
console.log("------onMounted-----"); }); // 調試哪些數據發生了變化
onRenderTriggered((event) =>{ console.log("------onRenderTriggered-----",event);
}) }, });
複製代碼
關於生命週期相關的內容就介紹到這裏,下面我們介紹一下 Vue3.x 中watch
有什麼不同。
watch 與 watchEffect 的用法
watch 函數用來偵聽特定的數據源,並在回調函數中執行副作用。默認情況是惰性的,也就是說僅在偵聽的源數據變更時才執行回調。
watch(source, callback, [options])
複製代碼
參數說明:
- source: 可以支持 string,Object,Function,Array; 用於指定要偵聽的響應式變量
- callback: 執行的回調函數
- options:支持 deep、immediate 和 flush 選項。
接下來我會分別介紹這個三個參數都是如何使用的, 如果你對 watch 的使用不明白的請往下看:
偵聽 reactive 定義的數據
import { defineComponent, ref, reactive, toRefs, watch } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
setTimeout(() => {
state.age++;
}, 1000);
// 修改age值時會觸發 watch的回調
watch(
() => state.age,
(curAge, preAge) => {
console.log("新值:", curAge, "老值:", preAge);
}
);
return {
...toRefs(state),
};
},
});
複製代碼
偵聽 ref 定義的數據
const year = ref(0);
setTimeout(() => {
year.value++;
}, 1000);
watch(year, (newVal, oldVal) => {
console.log("新值:", newVal, "老值:", oldVal);
});
複製代碼
偵聽多個數據
上面兩個例子中,我們分別使用了兩個 watch, 當我們需要偵聽多個數據源時, 可以進行合併, 同時偵聽多個數據:
watch([() => state.age, year], ([curAge, newVal], [preAge, oldVal]) => {
console.log("新值:", curAge, "老值:", preAge); console.log("新值:", newVal,
"老值:", oldVal); });
複製代碼
偵聽複雜的嵌套對象
我們實際開發中,複雜數據隨處可見, 比如:
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type: "三室兩廳",
},
},
});
watch(
() => state.room,
(newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
},
{ deep: true }
);
複製代碼
如果不使用第三個參數deep:true
, 是無法監聽到數據變化的。 前面我們提到,默認情況下,watch 是惰性的, 那什麼情況下不是惰性的, 可以立即執行回調函數呢?其實使用也很簡單, 給第三個參數中設置immediate: true
即可。關於flush
配置,還在學習,後期會補充
stop 停止監聽
我們在組件中創建的watch
監聽,會在組件被銷燬時自動停止。如果在組件銷燬之前我們想要停止掉某個監聽, 可以調用watch()
函數的返回值,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止監聽
stopWatchRoom()
}, 3000)
複製代碼
還有一個監聽函數watchEffect
, 在我看來watch
已經能滿足監聽的需求,爲什麼還要有watchEffect
呢?雖然我沒有 get 到它的必要性,但是還是要介紹一下watchEffect
,首先看看它的使用和watch
究竟有何不同。
import { defineComponent, ref, reactive, toRefs, watchEffect } from "vue";
export default defineComponent({
setup() {
const state = reactive({ nickname: "xiaofan", age: 20 });
let year = ref(0)
setInterval(() =>{
state.age++
year.value++
},1000)
watchEffect(() => {
console.log(state);
console.log(year);
}
);
return {
...toRefs(state)
}
},
});
複製代碼
執行結果首先打印一次state
和year
值;然後每隔一秒,打印state
和year
值。 從上面的代碼可以看出, 並沒有像watch
一樣需要先傳入依賴,watchEffect
會自動收集依賴, 只要指定一個回調函數。在組件初始化時, 會先執行一次來收集依賴, 然後當收集到的依賴中數據發生變化時, 就會再次執行回調函數。所以總結對比如下:
- watchEffect 不需要手動傳入依賴
- watchEffect 會先執行一次用來自動收集依賴
- watchEffect 無法獲取到變化前的值, 只能獲取變化後的值
上面介紹了 Vue3 Composition API
的部分內容, 還有很多非常好用的 API, 建議直接查看官網 composition-api。 其實我們也能進行自定義封裝。
自定義 Hooks
開篇的時候我們使用 Vue2.x 寫了一個實現加減的例子, 這裏可以將其封裝成一個 hook, 我們約定這些「自定義 Hook」以 use 作爲前綴,和普通的函數加以區分。 useCount.ts
實現:
import { ref, Ref, computed } from "vue";
type CountResultProps = {
count: Ref<number>;
multiple: Ref<number>;
increase: (delta?: number) => void;
decrease: (delta?: number) => void;
};
export default function useCount(initValue = 1): CountResultProps {
const count = ref(initValue);
const increase = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value += delta;
} else {
count.value += 1;
}
};
const multiple = computed(() => count.value * 2);
const decrease = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value -= delta;
} else {
count.value -= 1;
}
};
return {
count,
multiple,
increase,
decrease,
};
}
複製代碼
接下來看一下在組件中使用useCount
這個 hook
:
<template>
<p>count: {{ count }}</p>
<p>倍數: {{ multiple }}</p>
<div>
<button @click="increase()">加1</button>
<button @click="decrease()">減一</button>
</div>
</template>
<script lang="ts">
import useCount from "../hooks/useCount";
setup() {
const { count, multiple, increase, decrease } = useCount(10);
return {
count,
multiple,
increase,
decrease,
};
},
</script>
複製代碼
開篇 Vue2.x 實現,分散在data
,method
,computed
等, 如果剛接手項目,實在無法快速將data
字段和method
關聯起來,而 Vue3 的方式可以很明確的看出,將 count 相關的邏輯聚合在一起, 看起來舒服多了, 而且useCount
還可以擴展更多的功能。 項目開發完之後,後續還會寫一篇總結項目中使用到的「自定義 Hooks 的文章」,幫助大家更高效的開發, 關於Composition API
和自定義 Hooks 就介紹到這裏, 接下來簡單介紹一下 vue2.x 與 vue3 響應式對比。
簡單對比 vue2.x 與 vue3.x 響應式
其實在 Vue3.x 還沒有發佈 bate 的時候, 很火的一個話題就是Vue3.x 將使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
。 沒有無緣無故的愛,也沒有無緣無故的恨。爲何要將Object.defineProperty
換掉呢,咋們可以簡單聊一下。 我剛上手 Vue2.x 的時候就經常遇到一個問題,數據更新了啊,爲何頁面不更新呢?什麼時候用$set
更新,什麼時候用$forceUpdate
強制更新,你是否也一度陷入困境。後來的學習過程中開始接觸源碼,才知道一切的根源都是 Object.defineProperty
。 對這塊想要深入瞭解的小夥伴可以看這篇文章 爲什麼 Vue3.0 不再使用 defineProperty 實現數據監聽?要詳細解釋又是一篇文章,這裏就簡單對比一下Object.defineProperty
與 Proxy
Object.defineProperty
只能劫持對象的屬性, 而 Proxy 是直接代理對象
由於Object.defineProperty
只能劫持對象屬性,需要遍歷對象的每一個屬性,如果屬性值也是對象,就需要遞歸進行深度遍歷。但是 Proxy 直接代理對象, 不需要遍歷操作
Object.defineProperty
對新增屬性需要手動進行Observe
因爲Object.defineProperty
劫持的是對象的屬性,所以新增屬性時,需要重新遍歷對象, 對其新增屬性再次使用Object.defineProperty
進行劫持。也就是 Vue2.x 中給數組和對象新增屬性時,需要使用$set
才能保證新增的屬性也是響應式的, $set
內部也是通過調用Object.defineProperty
去處理的。
Teleport
Teleport 是 Vue3.x 新推出的功能, 沒聽過這個詞的小夥伴可能會感到陌生;翻譯過來是傳送
的意思,可能還是覺得不知所以,沒事下邊我就給大家形象的描述一下。
Teleport 是什麼呢?
Teleport 就像是哆啦 A 夢中的「任意門」,任意門的作用就是可以將人瞬間傳送到另一個地方。有了這個認識,我們再來看一下爲什麼需要用到 Teleport 的特性呢,看一個小例子: 在子組件Header
中使用到Dialog
組件,我們實際開發中經常會在類似的情形下使用到 Dialog
,此時Dialog
就被渲染到一層層子組件內部,處理嵌套組件的定位、z-index
和樣式都變得困難。 Dialog
從用戶感知的層面,應該是一個獨立的組件,從 dom 結構應該完全剝離 Vue 頂層組件掛載的 DOM;同時還可以使用到 Vue 組件內的狀態(data
或者props
)的值。簡單來說就是,即希望繼續在組件內部使用Dialog
, 又希望渲染的 DOM 結構不嵌套在組件的 DOM 中。 此時就需要 Teleport 上場,我們可以用<Teleport>
包裹Dialog
, 此時就建立了一個傳送門,可以將Dialog
渲染的內容傳送到任何指定的地方。 接下來就舉個小例子,看看 Teleport 的使用方式
Teleport 的使用
我們希望 Dialog 渲染的 dom 和頂層組件是兄弟節點關係, 在index.html
文件中定義一個供掛載的元素:
<body>
<div id="app"></div>
<div id="dialog"></div>
</body>
複製代碼
定義一個Dialog
組件Dialog.vue
, 留意 to
屬性, 與上面的id
選擇器一致:
<template>
<teleport to="#dialog">
<div class="dialog">
<div class="dialog_wrapper">
<div class="dialog_header" v-if="title">
<slot name="header">
<span>{{ title }}</span>
</slot>
</div>
</div>
<div class="dialog_content">
<slot></slot>
</div>
<div class="dialog_footer">
<slot name="footer"></slot>
</div>
</div>
</teleport>
</template>
複製代碼
最後在一個子組件Header.vue
中使用Dialog
組件, 這裏主要演示 Teleport 的使用,不相關的代碼就省略了。header
組件
<div class="header">
...
<navbar />
<Dialog v-if="dialogVisible"></Dialog>
</div>
...
複製代碼
Dom 渲染效果如下: 圖片. png 可以看到,我們使用 teleport
組件,通過 to
屬性,指定該組件渲染的位置與 <div id="app"></div>
同級,也就是在 body
下,但是 Dialog
的狀態 dialogVisible
又是完全由內部 Vue 組件控制.
Suspense
Suspense
是 Vue3.x 中新增的特性, 那它有什麼用呢?別急,我們通過 Vue2.x 中的一些場景來認識它的作用。 Vue2.x 中應該經常遇到這樣的場景:
<template>
<div>
<div v-if="!loading">
...
</div>
<div v-if="loading">
加載中...
</div>
</div>
</template>
複製代碼
在前後端交互獲取數據時, 是一個異步過程,一般我們都會提供一個加載中的動畫,當數據返回時配合v-if
來控制數據顯示。 如果你使用過vue-async-manager
這個插件來完成上面的需求, 你對Suspense
可能不會陌生,Vue3.x 感覺就是參考了vue-async-manager
. Vue3.x 新出的內置組件Suspense
, 它提供兩個template
slot, 剛開始會渲染一個 fallback 狀態下的內容, 直到到達某個條件後纔會渲染 default 狀態的正式內容, 通過使用Suspense
組件進行展示異步渲染就更加的簡單。:::warning 如果使用 Suspense
, 要返回一個 promise :::Suspense
組件的使用:
<Suspense>
<template #default>
<async-component></async-component>
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</Suspense>
複製代碼
asyncComponent.vue
:
<<template>
<div>
<h4>這個是一個異步加載數據</h4>
<p>用戶名:{{user.nickname}}</p>
<p>年齡:{{user.age}}</p>
</div>
</template>
<script>
import { defineComponent } from "vue"
import axios from "axios"
export default defineComponent({
setup(){
const rawData = await axios.get("http://xxx.xinp.cn/user")
return {
user: rawData.data
}
}
})
</script>
複製代碼
從上面代碼來看,Suspense
只是一個帶插槽的組件,只是它的插槽指定了default
和 fallback
兩種狀態。
片段(Fragment)
在 Vue2.x 中, template
中只允許有一個根節點:
<template>
<div>
<span></span>
<span></span>
</div>
</template>
複製代碼
但是在 Vue3.x 中,你可以直接寫多個根節點, 是不是很爽:
<template>
<span></span>
<span></span>
</template>
複製代碼
更好的 Tree-Shaking
Vue3.x 在考慮到 tree-shaking
的基礎上重構了全局和內部 API, 表現結果就是現在的全局 API 需要通過 ES Module
的引用方式進行具名引用, 比如在 Vue2.x 中,我們要使用 nextTick
:
// vue2.x
import Vue from "vue"
Vue.nextTick(()=>{
...
})
複製代碼
Vue.nextTick()
是一個從 Vue 對象直接暴露出來的全局 API,其實 $nextTick()
只是 Vue.nextTick()
的一個簡易包裝,只是爲了方便而把後者的回調函數的 this
綁定到了當前的實例。雖然我們藉助webpack
的tree-shaking
, 但是不管我們實際上是否使用Vue.nextTick()
, 最終都會進入我們的生產代碼, 因爲 Vue 實例是作爲單個對象導出的, 打包器無法堅持出代碼總使用了對象的哪些屬性。 在 Vue3.x 中改寫成這樣:
import { nextTick } from "vue"
nextTick(() =>{
...
})
複製代碼
受影響的 API
這是一個比較大的變化, 因爲以前的全局 API 現在只能通過具名導入,這一更改會對以下 API 有影響:
Vue.nextTick
Vue.observable
(用Vue.reactive
替換)Vue.version
Vue.compile
(僅限完整版本時可用)Vue.set
(僅在 2.x 兼容版本中可用)Vue.delete
(與上同)
內置工具
出來上面的 API 外, 還有許多內置的組件 以上僅適用於 ES Modules
builds,用於支持 tree-shaking 的綁定器——UMD 構建仍然包括所有特性,並暴露 Vue 全局變量上的所有內容 (編譯器將生成適當的輸出,以使用全局外的 api 而不是導入)。::: 前面都是 Vue3.0 的一些新特性,後面着重介紹一下相對於 Vue2.x 來說, 有什麼變更呢?
變更
slot 具名插槽語法
在 Vue2.x 中, 具名插槽的寫法:
<!-- 子組件中:-->
<slot name="title"></slot>
複製代碼
在父組件中使用:
<template slot="title">
<h1>歌曲:成都</h1>
<template>
複製代碼
如果我們要在 slot 上面綁定數據,可以使用作用域插槽,實現如下:
// 子組件
<slot name="content" :data="data"></slot>
export default {
data(){
return{
data:["走過來人來人往","不喜歡也得欣賞","陪伴是最長情的告白"]
}
}
}
複製代碼
<!-- 父組件中使用 -->
<template slot="content" slot-scope="scoped">
<div v-for="item in scoped.data">{{item}}</div>
<template>
複製代碼
在 Vue2.x 中具名插槽和作用域插槽分別使用slot
和slot-scope
來實現, 在 Vue3.0 中將slot
和slot-scope
進行了合併同意使用。 Vue3.0 中v-slot
:
<!-- 父組件中使用 -->
<template v-slot:content="scoped">
<div v-for="item in scoped.data">{{item}}</div>
</template>
<!-- 也可以簡寫成: -->
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
複製代碼
自定義指令
首先回顧一下 Vue 2 中實現一個自定義指令:
// 註冊一個全局自定義指令 `v-focus`
Vue.directive('focus', {
// 當被綁定的元素插入到 DOM 中時……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
複製代碼
在 Vue 2 中, 自定義指令通過以下幾個可選鉤子創建:
- bind:只調用一次,指令第一次綁定到元素時調用。在這裏可以進行一次性的初始化設置。
- inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。
- update:所在組件的 VNode 更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新 (詳細的鉤子函數參數見下)。
- componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新後調用。
- unbind:只調用一次,指令與元素解綁時調用。
在 Vue 3 中對自定義指令的 API 進行了更加語義化的修改, 就如組件生命週期變更一樣, 都是爲了更好的語義化, 變更如下: 所以在 Vue3 中, 可以這樣來自定義指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
複製代碼
然後可以在模板中任何元素上使用新的 v-focus
指令, 如下:
<input v-focus />
複製代碼
v-model 升級
在使用 Vue 3 之前就瞭解到 v-model
發生了很大的變化, 使用過了之後才真正的 get 到這些變化, 我們先縱觀一下發生了哪些變化, 然後再針對的說一下如何使用:
- 變更:在自定義組件上使用
v-model
時, 屬性以及事件的默認名稱變了 - 變更:
v-bind
的.sync
修飾符在 Vue 3 中又被去掉了, 合併到了v-model
裏 - 新增:同一組件可以同時設置多個
v-model
- 新增:開發者可以自定義
v-model
修飾符
有點懵?彆着急,往下看 在 Vue2 中, 在組件上使用 v-model
其實就相當於傳遞了value
屬性, 並觸發了input
事件:
<!-- Vue 2 -->
<search-input v-model="searchValue"><search-input>
<!-- 相當於 -->
<search-input :value="searchValue" @input="searchValue=$event"><search-input>
複製代碼
這時v-model
只能綁定在組件的value
屬性上,那我們就不開心了, 我們就像給自己的組件用一個別的屬性,並且我們不想通過觸發input
來更新值,在.async
出來之前,Vue 2 中這樣實現:
// 子組件:searchInput.vue
export default {
model:{
prop: 'search',
event:'change'
}
}
複製代碼
修改後, searchInput 組件使用v-model
就相當於這樣:
<search-input v-model="searchValue"><search-input>
<!-- 相當於 -->
<search-input :search="searchValue" @change="searchValue=$event"><search-input>
複製代碼
但是在實際開發中,有些場景我們可能需要對一個 prop 進行 “雙向綁定”, 這裏以最常見的 modal 爲例子:modal 挺合適屬性雙向綁定的,外部可以控制組件的visible
顯示或者隱藏,組件內部關閉可以控制 visible
屬性隱藏,同時 visible 屬性同步傳輸到外部。組件內部, 當我們關閉modal
時, 在子組件中以 update:PropName 模式觸發事件:
this.$emit('update:visible', false)
複製代碼
然後在父組件中可以監聽這個事件進行數據更新:
<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
複製代碼
此時我們也可以使用v-bind.async
來簡化實現:
<modal :visible.async="isVisible"></modal>
複製代碼
上面回顧了 Vue2 中v-model
實現以及組件屬性的雙向綁定,那麼在 Vue 3 中應該怎樣實現的呢? 在 Vue3 中, 在自定義組件上使用v-model
, 相當於傳遞一個modelValue
屬性, 同時觸發一個update:modelValue
事件:
<modal v-model="isVisible"></modal>
<!-- 相當於 -->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>
複製代碼
如果要綁定屬性名, 只需要給v-model
傳遞一個參數就行, 同時可以綁定多個v-model
:
<modal v-model:visible="isVisible" v-model:content="content"></modal>
<!-- 相當於 -->
<modal
:visible="isVisible"
:content="content"
@update:visible="isVisible"
@update:content="content"
/>
複製代碼
不知道你有沒有發現,這個寫法完全沒有.async
什麼事兒了, 所以啊,Vue 3 中又拋棄了.async
寫法, 統一使用v-model
異步組件
Vue3 中 使用 defineAsyncComponent
定義異步組件,配置選項 component
替換爲 loader
,Loader 函數本身不再接收 resolve 和 reject 參數,且必須返回一個 Promise,用法如下:
<template>
<!-- 異步組件的使用 -->
<AsyncPage />
</tempate>
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// 無配置項異步組件
AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
// 有配置項異步組件
AsyncPageWithOptions: defineAsyncComponent({
loader: () => import(".NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: () => import("./ErrorComponent.vue"),
loadingComponent: () => import("./LoadingComponent.vue"),
})
},
}
</script>
鏈接:https://juejin.cn/post/6940454764421316644