一些概念
Vue Composition API(VCA) 在實(shí)現(xiàn)上也其實(shí)只是把 Vue 本身就有的響應(yīng)式系統(tǒng)更顯式地暴露出來(lái)而已。
這不是函數(shù)式,只是 API 暴露為函數(shù)。
3.0 Template 編譯出來(lái)的性能會(huì)比手寫 jsx 快好幾倍萨驶。
——尤雨溪
Vue2 傳統(tǒng)的 data,computed艇肴,watch腔呜,methods 寫法叁温,我們稱之為「選項(xiàng)式api(Options API )」
Vue3 使用 Composition API (VCA)可以根據(jù)邏輯功能來(lái)組織代碼,一個(gè)功能相關(guān)的 api 會(huì)放在一起核畴。
Vue 和 React 的邏輯復(fù)用手段
到目前為止膝但,
Vue:Mixins(混入)、HOC(高階組件)膛檀、作用域插槽锰镀、Vue Composition API(VCA/組合式API)娘侍。
React:Mixins咖刃、HOC、Render Props憾筏、Hook嚎杨。
我們可以看到都是一段越來(lái)越好的成長(zhǎng)史,這里就不再舉例贅述氧腰,本文重心在 VCA枫浙,VCA 更偏向于「組合」的概念。
5個(gè)維度來(lái)講 Vue3
1. 框架
一個(gè)例子先來(lái)了解 VCA
在 Vue 中古拴,有了抽象封裝組件的概念箩帚,解決了在頁(yè)面上模塊越多,越顯臃腫的問(wèn)題黄痪。但即使進(jìn)行組件封裝紧帕,在應(yīng)用越來(lái)越大的時(shí)候,會(huì)發(fā)現(xiàn)頁(yè)面的邏輯功能點(diǎn)越來(lái)越多桅打,
data/computed/watch/methods
中會(huì)被不斷塞入邏輯功能是嗜,所以要將邏輯再進(jìn)行抽離組合、復(fù)用挺尾,這就是 VCA鹅搪。
舉個(gè)簡(jiǎn)單的例子:
我們要實(shí)現(xiàn) 3 個(gè)邏輯
- 根據(jù) id 獲取表格的數(shù)據(jù)
- 可對(duì)表格數(shù)據(jù)進(jìn)行搜索過(guò)濾
- 彈框新增數(shù)據(jù)到表格中
Vue2 options api 的處理
為了閱讀質(zhì)量,省略了部分代碼遭铺,但不影響我們了解 VCA
// 邏輯功能(1)
const getTableDataApi = id => {
const mockData = {
1: [
{ id: 11, name: '張三1' },
{ id: 12, name: '李四1' },
{ id: 13, name: '王五1' }
],
2: [
{ id: 21, name: '張三2' },
{ id: 22, name: '李四2' },
{ id: 23, name: '王五2' }
]
};
return new Promise(resolve => {
setTimeout(() => {
resolve(mockData[id] || []);
}, 1000);
});
};
export default {
name: 'VCADemo',
components: { Modal },
data() {
return {
// 邏輯功能(1)
id: 1,
table: [],
// 邏輯功能(2)
search: '',
// 邏輯功能(3)
modalShow: false,
form: {
id: '',
name: ''
}
};
},
computed: {
// 邏輯功能(2)
getTableDataBySearch() {
return this.table.filter(item => item.name.indexOf(this.search) !== -1);
}
},
watch: {
// 邏輯功能(1)
id: 'getTableData'
},
mounted() {
// 邏輯功能(1)
this.getTableData();
},
methods: {
// 邏輯功能(1)
async getTableData() {
const res = await getTableDataApi(this.id);
this.table = res;
},
// 邏輯功能(3)
handleAdd() {
this.modalShow = true;
},
// 邏輯功能(3)
handlePost() {
const { id, name } = this.form;
this.table.push({ id, name });
this.modalShow = false;
}
}
};
這里只是舉例簡(jiǎn)單的邏輯丽柿。如果項(xiàng)目復(fù)雜了,邏輯增多了魂挂。涉及到一個(gè)邏輯的改動(dòng)航厚,我們就可能需要修改分布在不同位置的相同功能點(diǎn),提升了維護(hù)成本锰蓬。
Vue3 composion api 的處理
讓我們來(lái)關(guān)注邏輯幔睬,抽離邏輯,先看主體的代碼結(jié)構(gòu)
import useTable from './composables/useTable';
import useSearch from './composables/useSearch';
import useAdd from './composables/useAdd';
export default defineComponent({
name: 'VCADemo',
components: { Modal },
setup() {
// 邏輯功能(1)
const { id, table, getTable } = useTable(id);
// 邏輯功能(2)
const { search, getTableBySearch } = useSearch(table);
// 邏輯功能(3)
const { modalShow, form, handleAdd, handlePost } = useAdd(table);
return {
id,
table,
getTable,
search,
getTableBySearch,
modalShow,
form,
handleAdd,
handlePost
};
}
});
setup 接收兩個(gè)參數(shù):props芹扭,context麻顶∩舛叮可以返回一個(gè)對(duì)象,對(duì)象的各個(gè)屬性都是被 proxy
的辅肾,進(jìn)行監(jiān)聽(tīng)追蹤队萤,將在模板上進(jìn)行響應(yīng)式渲染。
我們來(lái)關(guān)注其中一個(gè)邏輯矫钓,useTable
要尔,一般來(lái)說(shuō)我們會(huì)用 use
開頭進(jìn)行命名,有那味了~
// VCADemo/composables/useTable.ts
// 邏輯功能(1)相關(guān)
import { ref, onMounted, watch, Ref } from 'vue';
import { ITable } from '../index.type';
const getTableApi = (id: number): Promise<ITable[]> => {
const mockData: { [key: number]: ITable[] } = {
1: [
{ id: '11', name: '張三1' },
{ id: '12', name: '李四1' },
{ id: '13', name: '王五1' }
],
2: [
{ id: '21', name: '張三2' },
{ id: '22', name: '李四2' },
{ id: '23', name: '王五2' }
]
};
return new Promise(resolve => {
setTimeout(() => {
resolve(mockData[id] || []);
}, 1000);
});
};
export default function useTable() {
const id = ref<number>(1);
const table = ref<ITable[]>([]);
const getTable = async () => {
table.value = await getTableApi(id.value);
};
onMounted(getTable);
watch(id, getTable);
return {
id,
table,
getTable
};
}
我們把相關(guān)邏輯獨(dú)立抽離新娜,并「組合」在一起了赵辕,可以看到在 vue 包暴露很多獨(dú)立函數(shù)提供我們使用,已經(jīng)不再 OO 了概龄,嗅到了一股 FP 的氣息~
上面這個(gè)例子先說(shuō)明了 VCA 的帶來(lái)的好處还惠,Vue3 的核心當(dāng)然是 VCA,Vue3 不僅僅是 VCA私杜,讓我們帶著好奇往下看~
生命周期蚕键,Vue2 vs Vue3
選項(xiàng)式 API(Vue2) | Hook inside setup(Vue3) |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
Hook inside setup,顧名思義衰粹,VCA 建議在 setup
這個(gè)大方法里面寫我們的各種邏輯功能點(diǎn)锣光。
Teleport 組件
傳送,將組件的 DOM 元素掛載在任意指定的一個(gè) DOM 元素铝耻,與 React Portals 的概念是一致的誊爹。
一個(gè)典型的例子,我們?cè)诮M件調(diào)用了 Modal 彈框組件田篇,我們希望的彈框是這樣子的替废,絕對(duì)居中,層級(jí)最高泊柬,如:
組件的結(jié)構(gòu)是這樣子的
<Home>
<Modal />
</Home>
但是如果在父組件 Home 有類似這樣的樣式椎镣,如 transform
:
就會(huì)影響到 Modal 的位置,即使 Modal 用了 position:fixed
來(lái)定位兽赁,如:
這就是為什么我們需要用 Teleport 組件來(lái)幫助我們 “跳出” 容器状答,避免受到父組件的一些約束控制,把組件的 DOM 元素掛載到 body 下刀崖,如:
<Teleport to="body">
<div v-if="show">
...Modal 組件的 DOM 結(jié)構(gòu)...
</div>
</Teleport>
注意:即使 Modal 跳出了容器惊科,也保持 “父子組件關(guān)系”,只是 DOM 元素的位置被移動(dòng)了而已 亮钦。
異步組件(defineAsyncComponent)
我們都知道在 Vue2 也有異步組件的概念馆截,但整體上來(lái)說(shuō)不算完整~,Vue3 提供了 defineAsyncComponent
方法與 Suspense
內(nèi)置組件,我們可以用它們來(lái)做一個(gè)優(yōu)雅的異步組件加載方案蜡娶。
直接看代碼:
HOCLazy/index.tsx
import { defineAsyncComponent, defineComponent } from 'vue';
import MySuspense from './MySuspense.vue';
export default function HOCLazy(chunk: any, isComponent: boolean = false) {
const wrappedComponent = defineAsyncComponent(chunk);
return defineComponent({
name: 'HOCLazy',
setup() {
const props = { isComponent, wrappedComponent };
return () => <MySuspense {...props} />;
}
});
}
解釋:HOCLazy 接收了兩個(gè)參數(shù)混卵,chunk
就是我們經(jīng)常采用的組件異步加載方式如:chunk=()=>import(xxx.vue)
,isComponent
表示當(dāng)前的“組件”是一個(gè) 組件級(jí) or 頁(yè)面級(jí)窖张,通過(guò)判斷 isComponent
來(lái)分別對(duì)應(yīng)不同的 “l(fā)oading” 操作幕随。
HOCLazy/MySuspense.vue
<template>
<Suspense>
<template #default>
<component :is="wrappedComponent"
v-bind="$attrs" />
</template>
<template #fallback>
<div>
<Teleport to="body"
:disabled="isComponent">
<div v-if="delayShow"
class="loading"
:class="{component:isComponent}">
<!-- 組件和頁(yè)面有兩種不一樣的loading方式,這里不再詳細(xì)封裝 -->
<div> {{isComponent?'組件級(jí)':'頁(yè)面級(jí)'}}Loading ...</div>
</div>
</Teleport>
</div>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: 'HOCLazy',
props: ['isComponent', 'wrappedComponent'],
setup(props) {
const delayShow = ref<boolean>(false);
onMounted(() => {
setTimeout(() => {
delayShow.value = true;
// delay 自己拿捏宿接,也可以以 props 的方式傳入
}, 300);
});
return { ...props, delayShow };
}
});
</script>
<style lang="less" scoped>
.loading {
// 組件級(jí)樣式
&.component {
}
// 頁(yè)面級(jí)樣式
}
</style>
解釋:
- Suspense 組件有兩個(gè)插槽赘淮,具名插槽
fallback
我們這里可以理解成一個(gè) loading 的占位符,在異步組件還沒(méi)顯示之前的后備內(nèi)容睦霎。 - 這里還用了 Vue 的動(dòng)態(tài)組件 component 來(lái)靈活的傳入一個(gè)異步組件梢卸,
v-bind="$attrs"
來(lái)保證我們傳遞給目標(biāo)組件的 props 不會(huì)消失。 - fallback 中我們利用了判斷 isComponent 來(lái)展示不同的 loading 碎赢,因?yàn)槲覀兿M?yè)面級(jí)的 loading 是“全局”的低剔,組件級(jí)是在原來(lái)的文檔流速梗,這里用了
Teleport :disabled="isComponent"
來(lái)控制是否跳出肮塞。 - 細(xì)心的小伙伴會(huì)發(fā)現(xiàn)這里做了一個(gè)延遲顯示
delayShow
,如果我們沒(méi)有這個(gè)延遲姻锁,在網(wǎng)絡(luò)環(huán)境良好的情況下枕赵,loading 每次都會(huì)一閃而過(guò),會(huì)有一種“反優(yōu)化”的感覺(jué)位隶。
調(diào)用 HOCLazy:
為了更好的看出效果拷窜,我們封裝了 slow 方法來(lái)延遲組件加載:
utils/slow.ts
const slow = (comp: any, delay: number = 1000): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => resolve(comp), delay);
});
};
export default slow;
調(diào)用(組件級(jí))
<template>
<LazyComp1 str="hello~" />
</template>
const LazyComp1 = HOCLazy(
() => slow(import('@/components/LazyComp1.vue'), 1000),
true
);
// ...
components: {
LazyComp1
},
// ...
看個(gè)效果:
其實(shí)這與 React 中的
React.lazy + React.Suspense
的概念是一致的,之前寫過(guò)的一篇文章 《React丨用戶體驗(yàn)丨h(huán)ook版 lazy loading》涧黄,小伙伴可以看看做下對(duì)比~
ref篮昧,reactive,toRef笋妥,toRefs 的區(qū)別使用
ref(reference)
ref 和 reactive 的存在都是了追蹤值變化(響應(yīng)式)懊昨,ref 有個(gè)「包裝」的概念,它用來(lái)包裝原始值類型春宣,如 string 和 number 酵颁,我們都知道不是引用類型是無(wú)法追蹤后續(xù)的變化的。ref 返回的是一個(gè)包含 .value
屬性的對(duì)象月帝。
setup(props, context) {
const count = ref<number>(1);
// 賦值
count.value = 2;
// 讀取
console.log('count.value :>> ', count.value);
return { count };
}
在 template 中 ref 包裝對(duì)象會(huì)被自動(dòng)展開(Ref Unwrapping)躏惋,也就是我們?cè)谀0謇锊挥迷?.value
<template>
{{count}}
</template>
reactive
與 Vue2 中的 Vue.observable()
是一個(gè)概念。
用來(lái)返回一個(gè)響應(yīng)式對(duì)象嚷辅,如:
const obj = reactive({
count: 0
})
// 改變
obj.count++
注意:它用來(lái)返回一個(gè)響應(yīng)式對(duì)象簿姨,本身就是對(duì)象,所以不需要包裝簸搞。我們使用它的屬性扁位,不需要加 .value
來(lái)獲取深寥。
toRefs
官網(wǎng):因?yàn)?props 是響應(yīng)式的,你不能使用 ES6 解構(gòu)贤牛,因?yàn)樗鼤?huì)消除 prop 的響應(yīng)性惋鹅。
讓我們關(guān)注 setup
方法的 props 的相關(guān)操作:
<template>
{{name}}
<button @click="handleClick">點(diǎn)我</button>
</template>
// ...
props: {
name: { type: String, default: ' ' }
},
setup(props) {
const { name } = props;
const handleClick = () => {
console.log('name :>> ', name);
};
return { handleClick };
}
// ...
注意:props 無(wú)需通過(guò) setup 函數(shù) return,也可以在 template 進(jìn)行綁定對(duì)應(yīng)的值
我們都知道解構(gòu)是 es6 一種便捷的手段殉簸,編譯成 es5 闰集,如:
// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;
假設(shè)父組件更改了 props.name 值,當(dāng)我們?cè)冱c(diǎn)擊了 button 輸出的 name 就還是之前的值般卑,不會(huì)跟著變化武鲁,這其實(shí)是一個(gè)基礎(chǔ)的 js 的知識(shí)點(diǎn)。
為了方便我們對(duì)它進(jìn)行包裝蝠检,toRefs
可以理解成批量包裝 props 對(duì)象沐鼠,如:
const { name } = toRefs(props);
const handleClick = () => {
// 因?yàn)槭前b對(duì)象,所以讀取的時(shí)候要用.value
console.log('name :>> ', name.value);
};
可以理解這一切都是因?yàn)槲覀円媒鈽?gòu)叹谁,toRefs
所采取的解決方案饲梭。
toRef
toRef 的用法,就是多了一個(gè)參數(shù)焰檩,允許我們針對(duì)一個(gè) key 進(jìn)行包裝憔涉,如:
const name = toRef(props,'name');
console.log('name :>> ', name.value);
watchEffect vs watch
Vue3 的 watch 方法與 Vue2 的概念類似,watchEffect 會(huì)讓我們有些疑惑析苫。其實(shí) watchEffect 與 watch 大體類似兜叨,區(qū)別在于:
watch 可以做到的
- 懶執(zhí)行副作用
- 更具體地說(shuō)明什么狀態(tài)應(yīng)該觸發(fā)偵聽(tīng)器重新運(yùn)行
- 訪問(wèn)偵聽(tīng)狀態(tài)變化前后的值
對(duì)于 Vue2 的 watch 方法,Vue3 的 "watch" 多了一個(gè)「清除副作用」 的概念衩侥,我們著重關(guān)注這點(diǎn)国旷。
這里拿 watchEffect
來(lái)舉例:
watchEffect:它立即執(zhí)行傳入的一個(gè)函數(shù),同時(shí)響應(yīng)式追蹤其依賴茫死,并在其依賴變更時(shí)重新運(yùn)行該函數(shù)跪但。
watchEffect 方法簡(jiǎn)單結(jié)構(gòu)
watchEffect(onInvalidate => {
// 執(zhí)行副作用
// do something...
onInvalidate(() => {
// 執(zhí)行/清理失效回調(diào)
// do something...
})
})
執(zhí)行失效回調(diào),有兩個(gè)時(shí)機(jī)
- 副作用即將重新執(zhí)行時(shí)璧榄,也就是監(jiān)聽(tīng)的數(shù)據(jù)發(fā)生改變時(shí)
- 組件卸載時(shí)
一個(gè)例子:我們要通過(guò) id 發(fā)起請(qǐng)求獲取「水果」的詳情特漩,我們監(jiān)聽(tīng) id,當(dāng) id 切換過(guò)于頻繁(還沒(méi)等上個(gè)異步數(shù)據(jù)返回成功)骨杂⊥可恚可能會(huì)導(dǎo)致最后 id=1
的數(shù)據(jù)覆蓋了id=2
的數(shù)據(jù),這并不是我們希望的搓蚪。
我們來(lái)模擬并解決這個(gè)場(chǎng)景:
模擬接口 getFruitsById
interface IFruit {
id: number;
name: string;
imgs: string;
}
const list: { [key: number]: IFruit } = {
1: { id: 1, name: '蘋果', imgs: 'https://xxx.apple.jpg' },
2: { id: 2, name: '香蕉', imgs: 'https://xxx.banana.jpg' }
};
const getFruitsById = (
id: number,
delay: number = 3000
): [Promise<IFruit>, () => void] => {
let _reject: (reason?: any) => void;
const _promise: Promise<IFruit> = new Promise((resolve, reject) => {
_reject = reject;
setTimeout(() => {
resolve(list[id]);
}, delay);
});
return [
_promise,
() =>
_reject({
message: 'abort~'
})
];
};
這里封裝了“取消請(qǐng)求”的方法蛤售,利用 reject 來(lái)完成這一動(dòng)作。
在 setup 方法
setup() {
const id = ref<number>(1);
const detail = ref<IFruit | {}>({});
watchEffect(async onInvalidate => {
onInvalidate(() => {
cancel && cancel();
});
// 模擬id=2的時(shí)候請(qǐng)求時(shí)間 1s,id=1的時(shí)候請(qǐng)求時(shí)間 2s
const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
const res = await p;
detail.value = res;
});
// 模擬頻繁切換id悴能,獲取香蕉的時(shí)候揣钦,獲取蘋果的結(jié)果還沒(méi)有回來(lái),取消蘋果的請(qǐng)求漠酿,保證數(shù)據(jù)不會(huì)被覆蓋
id.value = 2;
// 最后 detail 值為 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}
如果沒(méi)有執(zhí)行 cancel()
冯凹,那么 detail 的數(shù)據(jù)將會(huì)是 { "id": 1, "name": "蘋果", "imgs": "https://xxx.apple.jpg" }
,因?yàn)?id=1 數(shù)據(jù)比較“晚接收到”炒嘲。
這就是在異步場(chǎng)景下常見(jiàn)的例子宇姚,清理失效的回調(diào),保證當(dāng)前副作用有效夫凸,不會(huì)被覆蓋浑劳。感興趣的小伙伴可以繼續(xù)深究。
fragment(片段)
我們都知道在封裝組件的時(shí)候夭拌,只能有一個(gè) root 魔熏。在 Vue3 允許我們有多個(gè) root ,也就是片段鸽扁,但是在一些操作值得我們注意蒜绽。
當(dāng) inheritAttrs=true[默認(rèn)]
時(shí),組件會(huì)自動(dòng)在 root 繼承合并 class 献烦,如:
子組件
<template>
<div class="fragment">
<div>div1</div>
<div>div2</div>
</div>
</template>
父組件調(diào)用滓窍,新增了一個(gè) class
<MyFragment class="extend-class" />
子組件會(huì)被渲染成
<div class="fragment extend-class">
<div> div1 </div>
<div> div2 </div>
</div>
如果我們使用了 片段 卖词,就需要顯式的去指定綁定 attrs 巩那,如子組件:
<template>
<div v-bind="$attrs">div1</div>
<div>div2</div>
</template>
emits
在 Vue2 我們會(huì)對(duì) props 里的數(shù)據(jù)進(jìn)行規(guī)定類型,默認(rèn)值此蜈,非空等一些驗(yàn)證即横,可以理解 emits 做了類似的事情,把 emit 規(guī)范起來(lái)裆赵,如:
// 也可以直接用數(shù)組东囚,不做驗(yàn)證
// emits: ['on-update', 'on-other'],
emits: {
// 賦值 null 不驗(yàn)證
'on-other': null,
// 驗(yàn)證
'on-update'(val: number) {
if (val === 1) {
return true;
}
// 自定義報(bào)錯(cuò)
console.error('val must be 1');
return false;
}
},
setup(props, ctx) {
const handleEmitUpdate = () => {
// 驗(yàn)證 val 不為 1,控制臺(tái)報(bào)錯(cuò)
ctx.emit('on-update', 2);
};
const handleEmitOther = () => {
ctx.emit('on-other');
};
return { handleEmitUpdate, handleEmitOther };
}
在 setup 中战授,emit 已經(jīng)不再用 this.$emit
了页藻,而是 setup 的第二個(gè)參數(shù) context
上下文來(lái)獲取 emit 。
v-model
個(gè)人還是挺喜歡 v-model 的更新的植兰,可以提升封裝組件的體驗(yàn)感~
在Vue2份帐,假設(shè)我需要封裝一個(gè)彈框組件 Modal,用
show
變量來(lái)控制彈框的顯示隱藏楣导,這肯定是一個(gè)父子組件都要維護(hù)的值废境。因?yàn)閱蜗驍?shù)據(jù)流,所以需要在 Modal 組件 emit 一個(gè)事件,父組件監(jiān)聽(tīng)事件接收并修改這個(gè)show
值噩凹。
為了方便我們會(huì)有一些語(yǔ)法糖巴元,如 v-model,但是在 Vue2 一個(gè)組件上只能有一個(gè) v-model 驮宴,因?yàn)檎Z(yǔ)法糖的背后是value
和@input
的組成逮刨, 如果還有多個(gè)類似這樣的 “雙向修改數(shù)據(jù)”,我們就需要用語(yǔ)法糖.sync
同步修飾符堵泽。
Vue3 把這兩個(gè)語(yǔ)法糖統(tǒng)一了禀忆,所以我們現(xiàn)在可以在一個(gè)組件上使用 多個(gè) v-model 語(yǔ)法糖,舉個(gè)例子:
先從父組件看
<VModel v-model="show"
v-model:model1="check"
v-model:model2.hello="textVal" />
hello為自定義修飾符
我們?cè)谝粋€(gè)組件上用了 3 個(gè) v-model 語(yǔ)法糖落恼,分別是
v-model 語(yǔ)法糖 | 對(duì)應(yīng)的 prop | 對(duì)應(yīng)的 event | 自定義修飾符對(duì)應(yīng)的 prop |
---|---|---|---|
v-model(default) | modelValue | update:modelValue | 無(wú) |
v-model:model1 | model1 | update:model1 | 無(wú) |
v-model:model2 | model2 | update:model2 | model2Modifiers |
這樣子我們就更清晰的在子組件我們要進(jìn)行一些什么封裝了箩退,如:
VModel.vue
// ...
props: {
modelValue: { type: Boolean, default: false },
model1: { type: Boolean, default: false },
model2: { type: String, default: '' },
model2Modifiers: {
type: Object,
default: () => ({})
}
},
emits: ['update:modelValue', 'update:model1', 'update:model2'],
// ...
key attribute
<template>
<input type="text"
placeholder="請(qǐng)輸入賬號(hào)"
v-if="show" />
<input type="text"
placeholder="請(qǐng)輸入郵箱"
v-else />
<button @click="show=!show">Toggle</button>
</template>
類似這樣的 v-if/v-else,在 Vue2 中佳谦,會(huì)盡可能高效地渲染元素戴涝,通常會(huì)復(fù)用已有元素而不是從頭開始渲染,所以當(dāng)我們?cè)诘谝粋€(gè) input 中輸入钻蔑,然后切換第二個(gè)
input 啥刻。第一個(gè) input 的值將會(huì)被保留復(fù)用。
有些場(chǎng)景下我們不要復(fù)用它們咪笑,需要添加一個(gè)唯一的 key 可帽,如:
<template>
<input type="text"
placeholder="請(qǐng)輸入賬號(hào)"
v-if="show"
key="account" />
<input type="text"
placeholder="請(qǐng)輸入郵箱"
v-else
key="email" />
<button @click="show=!show">Toggle</button>
</template>
但是在 Vue3 我們不用顯式的去添加 key ,這兩個(gè) input 元素也是完全獨(dú)立的窗怒,因?yàn)?Vue3 會(huì)對(duì) v-if/v-else 自動(dòng)生成唯一的 key映跟。
全局 API
在 Vue2 我們對(duì)于一些全局的配置可能是這樣子的,例如我們使用了一個(gè)插件
Vue.use({
/* ... */
});
const app1 = new Vue({ el: '#app-1' });
const app2 = new Vue({ el: '#app-2' });
但是這樣子這會(huì)影響兩個(gè)根實(shí)例扬虚,也就是說(shuō)努隙,會(huì)變得不可控。
在 Vue3 引入一個(gè)新的 API createApp
方法辜昵,返回一個(gè)實(shí)例:
import { createApp } from 'vue';
const app = createApp({ /* ... */ });
然后我們就可以在這個(gè)實(shí)例上掛載全局相關(guān)方法荸镊,并只對(duì)當(dāng)前實(shí)例生效,如:
app
.component(/* ... */)
.directive(/* ... */ )
.mixin(/* ... */ )
.use(/* ... */ )
.mount('#app');
需要注意的是堪置,在 Vue2 我們用了 Vue.prototype.$http=()=>{}
這樣的寫法躬存,來(lái)對(duì) “根Vue” 的 prototype 進(jìn)行掛載方法,使得我們?cè)谧咏M件舀锨,可以通過(guò)原型鏈的方式找到 $http
方法岭洲,即 this.$http
。
而在 Vue3 我們類似這樣的掛載需要用一個(gè)新的屬性 globalProperties
:
app.config.globalProperties.$http = () => {}
在 setup 內(nèi)部使用 $http
:
setup() {
const {
ctx: { $http }
} = getCurrentInstance();
}
2. 底層優(yōu)化
Proxy 代理
Vue2 響應(yīng)式的基本原理雁竞,就是通過(guò) Object.defineProperty
钦椭,但這個(gè)方式存在缺陷拧额。使得 Vue 不得不通過(guò)一些手段來(lái) hack,如:
- Vue.$set() 動(dòng)態(tài)添加新的響應(yīng)式屬性
- 無(wú)法監(jiān)聽(tīng)數(shù)組變化彪腔,Vue 底層需要對(duì)數(shù)組的一些操作方法侥锦,進(jìn)行再封裝。如
push
德挣,pop
等方法恭垦。
而在 Vue3 中優(yōu)先使用了 Proxy 來(lái)處理,它代理的是整個(gè)對(duì)象而不是對(duì)象的屬性格嗅,可對(duì)于整個(gè)對(duì)象進(jìn)行操作番挺。不僅提升了性能,也沒(méi)有上面所說(shuō)的缺陷屯掖。
簡(jiǎn)單舉兩個(gè)例子:
- 動(dòng)態(tài)添加響應(yīng)式屬性
const targetObj = { id: '1', name: 'zhagnsan' };
const proxyObj = new Proxy(targetObj, {
get: function (target, propKey, receiver) {
console.log(`getting key:${propKey}`);
return Reflect.get(...arguments);
},
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey}玄柏,value:${value}`);
return Reflect.set(...arguments);
}
});
proxyObj.age = 18;
// setting key:age,value:18
如上贴铜,用 Proxy
我們對(duì) proxyObj
對(duì)象動(dòng)態(tài)添加的屬性也會(huì)被攔截到粪摘。
Reflect
對(duì)象是ES6 為了操作對(duì)象而提供的新 API。它有幾個(gè)內(nèi)置的方法绍坝,就如上面的 get
/ set
徘意,這里可以理解成我們用 Reflect
更加方便,否則我們需要如:
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return target[propKey];
},
- 對(duì)數(shù)組的操作進(jìn)行攔截
const targetArr = [1, 2];
const proxyArr = new Proxy(targetArr, {
set: function (target, propKey, value, receiver) {
console.log(`setting key:${propKey}轩褐,value:${value}`);
return Reflect.set(...arguments);
}
});
proxyArr.push('3');
// setting key:2椎咧,value:3
// setting key:length,value:3
靜態(tài)提升(hoistStatic) vdom
我們都知道 Vue 有虛擬dom的概念把介,它能為我們?cè)跀?shù)據(jù)改變時(shí)高效的渲染頁(yè)面勤讽。
Vue3 優(yōu)化了 vdom 的更新性能,簡(jiǎn)單舉個(gè)例子
Template
<div class="div">
<div>content</div>
<div>{{message}}</div>
</div>
Compiler 后劳澄,沒(méi)有靜態(tài)提升
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", { class: "div" }, [
_createVNode("div", null, "content"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
Compiler 后地技,有靜態(tài)提升
const _hoisted_1 = { class: "div" }
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "content", -1 /* HOISTED */)
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_hoisted_2,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
靜態(tài)提升包含「靜態(tài)節(jié)點(diǎn)」和「靜態(tài)屬性」的提升,也就是說(shuō)秒拔,我們把一些靜態(tài)的不會(huì)變的節(jié)點(diǎn)用變量緩存起來(lái),提供下次 re-render 直接調(diào)用飒硅。
如果沒(méi)有做這個(gè)動(dòng)作砂缩,當(dāng) render
重新執(zhí)行時(shí),即使標(biāo)簽是靜態(tài)的三娩,也會(huì)被重新創(chuàng)建庵芭,這就會(huì)產(chǎn)生性能消耗。
3. 與 TS
3.0 的一個(gè)主要設(shè)計(jì)目標(biāo)是增強(qiáng)對(duì) TypeScript 的支持雀监。原本我們期望通過(guò) Class API 來(lái)達(dá)成這個(gè)目標(biāo)双吆,但是經(jīng)過(guò)討論和原型開發(fā)眨唬,我們認(rèn)為 Class 并不是解決這個(gè)問(wèn)題的正確路線,基于 Class 的 API 依然存在類型問(wèn)題好乐∝腋停——尤雨溪
基于函數(shù)的 API 天然 與 TS 完美結(jié)合。
defineComponent
在 TS 下蔚万,我們需要用 Vue 暴露的方法 defineComponent岭妖,它單純?yōu)榱祟愋屯茖?dǎo)而存在的。
props 推導(dǎo)
import { defineComponent } from 'vue';
export default defineComponent({
props: {
val1: String,
val2: { type: String, default: '' },
},
setup(props, context) {
props.val1;
}
})
當(dāng)我們?cè)?setup 方法訪問(wèn) props 時(shí)候反璃,我們可以看到被推導(dǎo)后的類型昵慌,
- val1 我們沒(méi)有設(shè)置默認(rèn)值,所以它為
string | undefined
- 而 val2 的值有值淮蜈,所以是
string
斋攀,如圖:
PropType
我們關(guān)注一下 props 定義的類型,如果是一個(gè)復(fù)雜對(duì)象梧田,我們就要用 PropType 來(lái)進(jìn)行強(qiáng)轉(zhuǎn)聲明蜻韭,如:
interface IObj {
id: number;
name: string;
}
obj: {
type: Object as PropType<IObj>,
default: (): IObj => ({ id: 1, name: '張三' })
},
或 聯(lián)合類型
type: {
type: String as PropType<'success' | 'error' | 'warning'>,
default: 'warning'
},
4. build丨更好的 tree-sharking(搖樹優(yōu)化)
tree-sharking 即在構(gòu)建工具構(gòu)建后消除程序中無(wú)用的代碼,來(lái)減少包的體積柿扣。
基于函數(shù)的 API 每一個(gè)函數(shù)都可以用 import { method1肖方,method2 } from "xxx";
,這就對(duì) tree-sharking 非常友好未状,而且函數(shù)名同變量名都可以被壓縮俯画,對(duì)象去不可以。舉個(gè)例子司草,我們封裝了一個(gè)工具艰垂,工具提供了兩個(gè)方法,用 method1
埋虹,method2
來(lái)代替猜憎。
我們把它們封裝成一個(gè)對(duì)象,并且暴露出去搔课,如:
// utils
const obj = {
method1() {},
method2() {}
};
export default obj;
// 調(diào)用
import util from '@/utils';
util.method1();
經(jīng)過(guò)webpack打包壓縮之后為:
a={method1:function(){},method2:function(){}};a.method1();
我們不用對(duì)象的形式胰柑,而用函數(shù)的形式來(lái)看看:
// utils
export function method1() {}
export function method2() {}
// 調(diào)用
import { method1 } from '@/utils';
method1();
經(jīng)過(guò)webpack打包壓縮之后為:
function a(){}a();
用這個(gè)例子我們就可以了解 Vue3 為什么能更好的 tree-sharking 算利,因?yàn)樗玫氖腔诤瘮?shù)形式的API丹锹,如:
import {
defineComponent,
reactive,
ref,
watchEffect,
watch,
onMounted,
toRefs,
toRef
} from 'vue';
5. options api 與 composition api 取舍
我們上面的代碼都是在 setup 內(nèi)部實(shí)現(xiàn),但是目前 Vue3 還保留了 Vue2 的 options api 寫法伪货,就是可以“并存”袍啡,如:
// ...
setup() {
const val = ref<string>('');
const fn = () => {};
return {
val,
fn
};
},
mounted() {
// 在 mounted 生命周期可以訪問(wèn)到 setup return 出來(lái)的對(duì)象
console.log(this.val);
this.fn();
},
// ...
結(jié)合 react 踩官,我們知道 “函數(shù)式”,hook 是未來(lái)的一個(gè)趨勢(shì)境输。
所以個(gè)人建議還是采用都在 setup
內(nèi)部寫邏輯的方式蔗牡,因?yàn)?Vue3 可以完全提供 Vue2 的全部能力颖系。
總結(jié)
個(gè)人覺(jué)得不管是 React Hook 還是 Vue3 的 VCA,我們都可以看到現(xiàn)在的前端框架趨勢(shì)辩越,“更函數(shù)式”嘁扼,讓邏輯復(fù)用更靈活。hook 的模式新增了 React / Vue 的抽象層級(jí)区匣,「組件級(jí) + 函數(shù)級(jí)」偷拔,可以讓我們處理邏輯時(shí)分的更細(xì),更好維護(hù)亏钩。
Vue3 One Piece莲绰,nice !
最后姑丑,前端精本精祝您圣誕快樂(lè)??~ (聽(tīng)說(shuō)公眾號(hào)關(guān)注「前端精」會(huì)更快樂(lè)哦~