寫在最前:本文轉(zhuǎn)自掘金
Composition API
Composition API 主要解決了高蜂,將零散分布的邏輯組合在一起來維護(hù)事秀,并且還可以將單獨(dú)的功能邏輯拆分成單獨(dú)的文件。
setup
setup 是 Vue3.x新增的一個(gè)選項(xiàng)山宾,他是組件內(nèi)使用 Compsition API
的入口扎阶。
setup 執(zhí)行時(shí)機(jī)
setup 執(zhí)行時(shí)機(jī)是在 beforeCreate
之前執(zhí)行
setup 參數(shù)
setup接收兩個(gè)參數(shù):
- props: 組件傳入的屬性
- context
setup 中接收的props
是響應(yīng)式的揩晴,當(dāng)傳入的新的props時(shí)筐钟,會及時(shí)被更新。由于是響應(yīng)式的疹鳄,所以不可以使用ES6結(jié)構(gòu)拧略,結(jié)構(gòu)會消除它的響應(yīng)式。
// 錯(cuò)誤代碼示例瘪弓,這代代碼會讓props不再支持響應(yīng)式
export default defineComponent({
setup(props, context){
const {name} = props
console.log(name)
},
})
在toRefs
學(xué)習(xí)的地方為大家解答垫蛆。接下來我們來說一下setup
接受的第二個(gè)參數(shù)context
,我們前面說了setup
中不能訪問Vue2中最常用的this
對象,所以context
中提供了this
中最常用的是三個(gè)屬性:attrs
月褥、slot
弛随、emit
,分別對應(yīng)Vue2.x中的$attr
屬性宁赤、slot
插槽和$emit
觸發(fā)事件。并且這幾個(gè)屬性都是自動(dòng)同步最新的值栓票,所以我們每次使用拿到的都是最新的值决左。
reactive、 ref 與toRefs
在Vue3.x中走贪,定義數(shù)據(jù)可以使用reactive
和ref
來進(jìn)行數(shù)據(jù)定義佛猛。那么ref
和reactive
他們有什么區(qū)別呢?分別什么時(shí)候使用呢坠狡?ref
可以處理js基礎(chǔ)類型的雙向綁定继找,也可以定義對象類型雙向綁定。但是reactive
函數(shù)只能代理對象類型逃沿。
我們在模板中使用
user.name
,user.age
這樣寫感覺很繁瑣婴渡,如果需要解構(gòu)出來,就需要toRefs
凯亮。toRefs
用于將一個(gè)reactive
對象轉(zhuǎn)化為屬性全部為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>
生命周期鉤子
我們可以直接看生命周期圖來認(rèn)識有哪些生命周期鉤子
從圖中我們可以看到Vue3.0新增了setup
,然后是將Vue2.x中的beforeDestroy
名稱變更為beforeUnmount
假消;destroyed
名稱變更為unmounted
柠并。其它Vue2中的生命周期仍然保留。上邊生命周期圖并沒有包含全部的生命周期鉤子富拗,還有其它幾個(gè)臼予,全部聲明周期鉤子如下圖所示:
我們可以看到beforeCreate
和created
被setup
替換了。其次啃沪,鉤子函數(shù)都增加了on
前綴粘拾;Vue3.x還新增了用于調(diào)試的鉤子函數(shù)onRenderTriggered
和onRenderTricked
下面我們就簡單使用幾個(gè)鉤子,方便大家學(xué)習(xí)如何使用谅阿,Vue3.x中的鉤子函數(shù)是需要從vue中導(dǎo)入的
import {defineComponent, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmout, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered} from 'vue';
export default defineComponent({
beforeCreate(){
console.log("beforeCreated")
},
created(){
console.log("created")
},
setup(){
console.log("setup");
// vue3.x生命周期寫在setup中
onBeforeMount(()=>{
console.log("onBeforeMount")
});
onMounted(()=>{
console.log("onMounted")
});
// 調(diào)試哪些數(shù)據(jù)發(fā)生了變化
onRenderTriggered((event)=>{
console.log("onRenderTriggered", event)
});
}
})
watch 與 watchEffect 的用法
watch函數(shù)用來偵聽特定的數(shù)據(jù)源半哟,并在回調(diào)函數(shù)中執(zhí)行邏輯。默認(rèn)情況是惰性的签餐,也就是說僅在偵聽的源數(shù)據(jù)變更時(shí)才執(zhí)行回調(diào)寓涨。
watch(source, callback, [options])
參數(shù)說明
- source: 可以支持string, Object, Function, Array;用于指定要偵聽的響應(yīng)式變量
- callback: 執(zhí)行的回調(diào)函數(shù)
- options: 支持deep, immediate 和 flush 選項(xiàng)
偵聽reactive定義的數(shù)據(jù)
import {defineComponent, ref, reactive, toRefs, watch} from 'vue';
export default defineComponent({
setup(){
const state = reactive({nickname: "xiaofan", age: 20});
setTimeout(()=>{
state.age++;
}, 1000);
// 修改age值時(shí)會觸發(fā)watch的回調(diào)
watch(()=> state.age, (curAge, preAge)=>{console.log(curAge, preAge)});
return {
...toRefs(state),
}
}
})
偵聽ref定義的數(shù)據(jù)
const year = ref(0);
setTimeout(()=>{
year.value++;
}, 1000);
watch(year, (newVal, oldVal)=>{
console.log("新值", newVal, "老值", oldVal);
})
偵聽多個(gè)數(shù)據(jù)
watch([()=>test.age,year],([curAge,newVal],[preAge, oldVal])=>{console.log(curAge,preAge);console.log(newVal,oldVal)})
偵聽復(fù)雜的嵌套對象
const state = reactive({
room: {
id: 100,
attrs: {
size: "140平方米",
type: "三室兩廳",
},
},
});
watch(
() => state.room,
(newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
},
{ deep: true }
);
如果不適用第三個(gè)參數(shù)deep:true
,是無法監(jiān)聽到數(shù)據(jù)變化的氯檐。前面我們提到戒良,默認(rèn)情況下,watch是惰性的冠摄,那什么情況下不是惰性的糯崎,可以立即執(zhí)行回調(diào)函數(shù)呢几缭?給第三個(gè)參數(shù)中設(shè)置 immediate:true
即可。關(guān)于flush
配置沃呢,還在學(xué)習(xí)年栓,后期補(bǔ)充。
stop停止監(jiān)聽
我們在組件中創(chuàng)建的watch
監(jiān)聽薄霜,會在組件被銷毀時(shí)自動(dòng)停止某抓。如果在組件銷毀之前我們想要停止掉某個(gè)監(jiān)聽,可以調(diào)用watch()
函數(shù)的返回值惰瓜,操作如下:
const stopWatchRoom = watch(() => state.room, (newType, oldType) => {
console.log("新值:", newType, "老值:", oldType);
}, {deep:true});
setTimeout(()=>{
// 停止監(jiān)聽
stopWatchRoom()
}, 3000)
watchEffect 的用法
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)}
},
})
執(zhí)行結(jié)果首先打印一次state
和year
值否副;然后每隔一秒,打印state
和year
值崎坊。
從上面的代碼可以看出备禀, 并沒有像watch
一樣需要先傳入依賴,watchEffect
會自動(dòng)收集依賴, 只要指定一個(gè)回調(diào)函數(shù)奈揍。在組件初始化時(shí)曲尸, 會先執(zhí)行一次來收集依賴, 然后當(dāng)收集到的依賴中數(shù)據(jù)發(fā)生變化時(shí)打月, 就會再次執(zhí)行回調(diào)函數(shù)队腐。所以總結(jié)對比如下:
- watchEffect 不需要手動(dòng)傳入依賴
- watchEffect 會先執(zhí)行一次用來自動(dòng)收集依賴
- watchEffect 無法獲取到變化前的值, 只能獲取變化后的值
computed 計(jì)算屬性
setup() {
let name = ref('xiaofan')
let age = ref(21)
//計(jì)算屬性
let getInfo = computed(() => {
return `我的名字:${name.value},今年${age.value}奏篙,請多多指教`
})
return {
name,
age,
getInfo,
}
}
自定義 Hooks
我們來寫了一個(gè)實(shí)現(xiàn)加減的例子柴淘,這里將其封裝成hook,我們約定這些[自定義的hook]以use作為前綴秘通,和普通的函數(shù)加以區(qū)分为严。useCount.ts
實(shí)現(xiàn):
import { ref, Ref, computed } from 'vue'
interface 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
:
<template>
<p>count: {{ count }}</p>
<p>倍數(shù): {{ 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>
teleport
Teleport 是什么呢?
Teleport
就像是哆啦 A 夢中的「任意門」肺稀,任意門的作用就是可以將人瞬間傳送到另一個(gè)地方第股。有了這個(gè)認(rèn)識,我們再來看一下為什么需要用到 Teleport
的特性呢话原,看一個(gè)小例子:
在子組件Header
中使用到Dialog
組件夕吻,我們實(shí)際開發(fā)中經(jīng)常會在類似的情形下使用到 Dialog
,此時(shí)Dialog
就被渲染到一層層子組件內(nèi)部繁仁,處理嵌套組件的定位涉馅、z-index
和樣式都變得困難。
Dialog
從用戶感知的層面黄虱,應(yīng)該是一個(gè)獨(dú)立的組件稚矿,從 dom
結(jié)構(gòu)應(yīng)該完全剝離Vue
頂層組件掛載的 DOM
;同時(shí)還可以使用到 Vue
組件內(nèi)的狀態(tài)(data
或者props
)的值。簡單來說就是,即希望繼續(xù)在組件內(nèi)部使用Dialog
, 又希望渲染的 DOM
結(jié)構(gòu)不嵌套在組件的 DOM
中晤揣。
此時(shí)就需要 Teleport
上場桥爽,我們可以用<Teleport>
包裹Dialog
, 此時(shí)就建立了一個(gè)傳送門,可以將Dialog
渲染的內(nèi)容傳送到任何指定的地方昧识。
接下來就舉個(gè)小例子钠四,看看 Teleport
的使用方式
Teleport 的使用
我們希望 Dialog
渲染的 dom
和頂層組件是兄弟節(jié)點(diǎn)關(guān)系, 在index.html
文件中定義一個(gè)供掛載的元素:
<body>
<div id="app"></div>
<div id="dialog"></div>
</body>
定義一個(gè)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>
最后在一個(gè)子組件Header.vue
中使用Dialog
組件, 這里主要演示 Teleport
的使用滞诺,不相關(guān)的代碼就省略了形导。header
組件
<div class="header">
...
<navbar />
<Dialog v-if="dialogVisible"></Dialog>
</div>
...
可以看到,我們使用
teleport
組件习霹,通過 to
屬性,指定該組件渲染的位置與 <div id="app"></div>
同級炫隶,也就是在 body
下淋叶,但是 Dialog
的狀態(tài)dialogVisible
又是完全由內(nèi)部 Vue 組件控制.
Suspense
暫未學(xué)習(xí)
片段(Fragment)
在 Vue2.x 中, template
中只允許有一個(gè)根節(jié)點(diǎn)伪阶,
但是在 Vue3.x 中煞檩,你可以直接寫多個(gè)根節(jié)點(diǎn), 是不是很爽栅贴。
更好的 Tree - Shaking
Vue3.x 在考慮到tree-shaking
的基礎(chǔ)上重構(gòu)了全局和內(nèi)部API斟湃,表現(xiàn)結(jié)果就是現(xiàn)在的全局API需要通過ESMoudle
的引用方式進(jìn)行具名引用,比如在Vue2.x中檐薯,我們要使用nextTick
:
// vue2.x
import Vue from 'vue'
Vue.nextTick(()=>{
...
})
Vue.nextTick()
是一個(gè)從Vue對象直接暴露出來的API凝赛,其實(shí)$nextTick()
只是Vue.nextTick()
的一個(gè)簡易包裝,只是為了方便而把后者的回調(diào)函數(shù)的this
綁定到了當(dāng)前實(shí)例坛缕。雖然我們借助webpack
的tree-shaking
墓猎,但是不管我們實(shí)際上是否使用Vue.nextTick()
,最終都會進(jìn)入我們的生產(chǎn)代碼赚楚,因?yàn)閂ue實(shí)例是作為單個(gè)對象導(dǎo)出的毙沾,打包器無法檢測出代碼中使用對象的哪些屬性。在Vue3.x中改成這樣寫:
import {nextTick} from 'vue'
nextTick(()=>{
...
})
受影響的API
- Vue.nextTick
- Vue.observable(用 Vue.reactive 替換)
- Vue.version
- Vue.compile(僅限完整版本時(shí)可用)
- Vue.set(僅在 2.x 兼容版本中可用)
- Vue.delete(與上同)
變更
slot 具名插槽語法
在vue2.x中宠页,具名插槽的寫法:
<!-- 子組件中 -->
<slot name="title"></slot>
在父組件中使用:
<template slot="title">
<h1>歌曲</h1>
</template>
如果我們要在slot上面綁定數(shù)據(jù)左胞,可以使用作用域插槽,實(shí)現(xiàn)如下:
// 子組件
<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
來實(shí)現(xiàn)举户,在Vue3.x中將slot
和slot-scope
進(jìn)行了合并統(tǒng)一使用烤宙。Vue3.x中 v-slot
<!--父組件中使用-->
<template v-slot:content="scoped">
<div v-for="item in scope.data">{{item}}</div>
</template>
<!--也可以簡寫成:-->
<template #content="{data}">
<div v-for="item in data">{{item}}</div>
</template>
自定義指令
首先回顧下 Vue2 中實(shí)現(xiàn)一個(gè)自定義指令:
// 注冊一個(gè)全局自定義指令 v-focus
Vue.directive('focus', {
inserted: function (el){
// 聚焦
el.focus()
}
})
在vue2.x中,自定義指令通過以下幾個(gè)可選鉤子創(chuàng)建:
- bind: 只調(diào)用一次敛摘,指令第一次綁定到元素時(shí)調(diào)用门烂。在這里可以進(jìn)行一次性的初始化設(shè)置。
- inserted: 被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用(僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)
- update: 所在組件的VNode更新時(shí)調(diào)用屯远,但是可能發(fā)生在其子VNode更新之前蔓姚。指令的值可能發(fā)生了改變,也可能沒有慨丐。但是你可以通過比較更新前后的值來忽略不必要的模板更新坡脐。
- componentUpdated: 指令所在組件的VNode 及其子VNode 全部更新后調(diào)用
-
unbind: 只調(diào)用一次,指令與元素解綁時(shí)調(diào)用
在Vue3 中對自定義的API進(jìn)行了更加語義化的修改房揭。
所以在 Vue3 中备闲, 可以這樣來自定義指令:
const { createApp } from "vue"
const app = createApp({})
app.directive('focus', {
mounted(el) {
el.focus()
}
})
然后可以在模板中任何元素上使用新的v-focus
指令, 如下:
<input v-focus />
v-model 升級
在使用Vue3 之前就了解到 v-model
發(fā)生了很大的變化捅暴,下面來了解下發(fā)生了哪些變化:
- 變更:在自定義組件上使用
v-model
時(shí)恬砂,屬性以及事件的默認(rèn)名稱變了 - 變更:
v-bind
的.sync
修飾符在Vue3中去掉,合并到v-model
里 - 新增: 同一組件可以同時(shí)設(shè)置多個(gè)
v-model
- 新增: 開發(fā)者可以自定義
v-model
修飾符
在vue2中蓬痒,在組件上使用 v-model
其實(shí)就相當(dāng)于傳遞了 value屬性呐舔,并觸發(fā)了input
事件:
<search-input v-model="searchValue"></search-input>
<!-- 相當(dāng)于 -->
<search-input :value="searchValue" @input="searchValue=$event"></search-input>
這時(shí) v-model
只能綁定在組件的value
屬性上垄潮,如果我們想給自己組件用一個(gè)別的屬性婆芦,并且不想通過input
來更新值楼吃,在.sync
出來之前,Vue2是這樣實(shí)現(xiàn)的:
// 子組件:searchInput.vue
export default {
model:{
prop: 'search',
event:'change'
}
}
修改后亲轨, searchInput
組件使用v-model
就相當(dāng)于這樣:
<search-input v-model="searchValue"><search-input>
<!-- 相當(dāng)于 -->
<search-input :search="searchValue" @change="searchValue=$event"><search-input>
但是在實(shí)際開發(fā)中趋惨,有些場景我們可能需要對一個(gè) prop
進(jìn)行 “雙向綁定”, 這里以最常見的 modal
為例子:modal
挺合適屬性雙向綁定的惦蚊,外部可以控制組件的visible
顯示或者隱藏器虾,組件內(nèi)部關(guān)閉可以控制 visible
屬性隱藏,同時(shí)visible
屬性同步傳輸?shù)酵獠垦病=M件內(nèi)部曾撤, 當(dāng)我們關(guān)閉modal
時(shí), 在子組件中以 update:PropName
模式觸發(fā)事件:
this.$emit('update:visible', false)
然后在父組件中可以監(jiān)聽這個(gè)事件進(jìn)行數(shù)據(jù)更新:
<modal :visible="isVisible" @update:visible="isVisible = $event"></modal>
此時(shí)我們也可以使用v-bind.sync
來簡化實(shí)現(xiàn):
<modal :visible.sync="isVisible"></modal>
上面回顧了 Vue2 中v-model
實(shí)現(xiàn)以及組件屬性的雙向綁定,那么在 Vue 3 中應(yīng)該怎樣實(shí)現(xiàn)的呢晕粪?在vue3中挤悉,在自定義組件上使用v-model
,相當(dāng)于傳遞一個(gè)modelValue
屬性巫湘,同時(shí)觸發(fā)一個(gè)update:modelValue
事件:
<modal v-model="isVisible"></modal>
<!--相當(dāng)于-->
<modal :modelValue="isVisible" @update:modelValue="isVisible = $event"></modal>
如果要綁定屬性名装悲,只需要給v-model
傳遞一個(gè)參數(shù)就行,同時(shí)可以綁定多個(gè)v-model
<modal v-model:visible="isVisible" v-model:content="content"></modal>
<!-- 相當(dāng)于 -->
<modal
:visible="isVisible"
:content="content"
@update:visible="isVisible"
@update:content="content"
/>
不知道你有沒有發(fā)現(xiàn)尚氛,這個(gè)寫法完全沒有.sync
什么事兒了诀诊, 所以啊,Vue 3 中又拋棄了.sync
寫法阅嘶, 統(tǒng)一使用v-model
異步組件
Vue3 中使用defineAsyncComponent
定義異步組件属瓣,配置選項(xiàng)component
替換為loader
载迄,Loader函數(shù)本身不在接收resolve 和 reject 參數(shù),且必須返回一個(gè)Promise 抡蛙,用法如下
<template>
<!--異步組件的使用-->
<AsyncPage/>
</template>
<script>
import {defineAsyncComponent} from 'vue';
export default{
components:{
// 無配置項(xiàng)異步組件
AsyncPage: defineAsyncComponent(()=>import('./NextPage.vue')),
// 有配置項(xiàng)異步組件
AsyncPage: defineAsyncComponent(()=>{
loader:()=>import('./NextPage.vue'),
delay:200,
timeout:3000,
errorComponent:()=>import("./ErrorComponent.vue"),
loadingComponent:()=>import("./LoadingComponent.vue")
})
},
}
</script>