一些概念
Vue Composition API(VCA) 在實現上也其實只是把 Vue 本身就有的響應式系統更顯式地暴露出來而已。
這不是函數式,只是 API 暴露為函數。
3.0 Template 編譯出來的性能會比手寫 jsx 快好幾倍。
——尤雨溪
Vue2 傳統的 data,computed,watch,methods 寫法,我們稱之為「選項式api(Options API )」
Vue3 使用 Composition API (VCA)可以根據邏輯功能來組織代碼,一個功能相關的 api 會放在一起。
Vue 和 React 的邏輯復用手段
到目前為止,
Vue:Mixins(混入)、HOC(高階組件)、作用域插槽、Vue Composition API(VCA/組合式API)。
React:Mixins、HOC、Render Props、Hook。
我們可以看到都是一段越來越好的成長史,這里就不再舉例贅述,本文重心在 VCA,VCA 更偏向于「組合」的概念。
5個維度來講 Vue3
1. 框架
一個例子先來了解 VCA
在 Vue 中,有了抽象封裝組件的概念,解決了在頁面上模塊越多,越顯臃腫的問題。但即使進行組件封裝,在應用越來越大的時候,會發現頁面的邏輯功能點越來越多,
data/computed/watch/methods
中會被不斷塞入邏輯功能,所以要將邏輯再進行抽離組合、復用,這就是 VCA。
舉個簡單的例子:
我們要實現 3 個邏輯
- 根據 id 獲取表格的數據
- 可對表格數據進行搜索過濾
- 彈框新增數據到表格中
Vue2 options api 的處理
為了閱讀質量,省略了部分代碼,但不影響我們了解 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;
}
}
};
這里只是舉例簡單的邏輯。如果項目復雜了,邏輯增多了。涉及到一個邏輯的改動,我們就可能需要修改分布在不同位置的相同功能點,提升了維護成本。
Vue3 composion api 的處理
讓我們來關注邏輯,抽離邏輯,先看主體的代碼結構
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 接收兩個參數:props,context。可以返回一個對象,對象的各個屬性都是被 proxy
的,進行監聽追蹤,將在模板上進行響應式渲染。
我們來關注其中一個邏輯,useTable
,一般來說我們會用 use
開頭進行命名,有那味了~
// VCADemo/composables/useTable.ts
// 邏輯功能(1)相關
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
};
}
我們把相關邏輯獨立抽離,并「組合」在一起了,可以看到在 vue 包暴露很多獨立函數提供我們使用,已經不再 OO 了,嗅到了一股 FP 的氣息~
上面這個例子先說明了 VCA 的帶來的好處,Vue3 的核心當然是 VCA,Vue3 不僅僅是 VCA,讓我們帶著好奇往下看~
生命周期,Vue2 vs Vue3
選項式 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
這個大方法里面寫我們的各種邏輯功能點。
Teleport 組件
傳送,將組件的 DOM 元素掛載在任意指定的一個 DOM 元素,與 React Portals 的概念是一致的。
一個典型的例子,我們在組件調用了 Modal 彈框組件,我們希望的彈框是這樣子的,絕對居中,層級最高,如:
組件的結構是這樣子的
<Home>
<Modal />
</Home>
但是如果在父組件 Home 有類似這樣的樣式,如 transform
:
就會影響到 Modal 的位置,即使 Modal 用了 position:fixed
來定位,如:
這就是為什么我們需要用 Teleport 組件來幫助我們 “跳出” 容器,避免受到父組件的一些約束控制,把組件的 DOM 元素掛載到 body 下,如:
<Teleport to="body">
<div v-if="show">
...Modal 組件的 DOM 結構...
</div>
</Teleport>
注意:即使 Modal 跳出了容器,也保持 “父子組件關系”,只是 DOM 元素的位置被移動了而已 。
異步組件(defineAsyncComponent)
我們都知道在 Vue2 也有異步組件的概念,但整體上來說不算完整~,Vue3 提供了 defineAsyncComponent
方法與 Suspense
內置組件,我們可以用它們來做一個優雅的異步組件加載方案。
直接看代碼:
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 接收了兩個參數,chunk
就是我們經常采用的組件異步加載方式如:chunk=()=>import(xxx.vue)
,isComponent
表示當前的“組件”是一個 組件級 or 頁面級,通過判斷 isComponent
來分別對應不同的 “loading” 操作。
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}">
<!-- 組件和頁面有兩種不一樣的loading方式,這里不再詳細封裝 -->
<div> {{isComponent?'組件級':'頁面級'}}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 {
// 組件級樣式
&.component {
}
// 頁面級樣式
}
</style>
解釋:
- Suspense 組件有兩個插槽,具名插槽
fallback
我們這里可以理解成一個 loading 的占位符,在異步組件還沒顯示之前的后備內容。 - 這里還用了 Vue 的動態組件 component 來靈活的傳入一個異步組件,
v-bind="$attrs"
來保證我們傳遞給目標組件的 props 不會消失。 - fallback 中我們利用了判斷 isComponent 來展示不同的 loading ,因為我們希望頁面級的 loading 是“全局”的,組件級是在原來的文檔流,這里用了
Teleport :disabled="isComponent"
來控制是否跳出。 - 細心的小伙伴會發現這里做了一個延遲顯示
delayShow
,如果我們沒有這個延遲,在網絡環境良好的情況下,loading 每次都會一閃而過,會有一種“反優化”的感覺。
調用 HOCLazy:
為了更好的看出效果,我們封裝了 slow 方法來延遲組件加載:
utils/slow.ts
const slow = (comp: any, delay: number = 1000): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => resolve(comp), delay);
});
};
export default slow;
調用(組件級)
<template>
<LazyComp1 str="hello~" />
</template>
const LazyComp1 = HOCLazy(
() => slow(import('@/components/LazyComp1.vue'), 1000),
true
);
// ...
components: {
LazyComp1
},
// ...
看個效果:
其實這與 React 中的
React.lazy + React.Suspense
的概念是一致的,之前寫過的一篇文章 《React丨用戶體驗丨hook版 lazy loading》,小伙伴可以看看做下對比~
ref,reactive,toRef,toRefs 的區別使用
ref(reference)
ref 和 reactive 的存在都是了追蹤值變化(響應式),ref 有個「包裝」的概念,它用來包裝原始值類型,如 string 和 number ,我們都知道不是引用類型是無法追蹤后續的變化的。ref 返回的是一個包含 .value
屬性的對象。
setup(props, context) {
const count = ref<number>(1);
// 賦值
count.value = 2;
// 讀取
console.log('count.value :>> ', count.value);
return { count };
}
在 template 中 ref 包裝對象會被自動展開(Ref Unwrapping),也就是我們在模板里不用再 .value
<template>
{{count}}
</template>
reactive
與 Vue2 中的 Vue.observable()
是一個概念。
用來返回一個響應式對象,如:
const obj = reactive({
count: 0
})
// 改變
obj.count++
注意:它用來返回一個響應式對象,本身就是對象,所以不需要包裝。我們使用它的屬性,不需要加 .value
來獲取。
toRefs
官網:因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性。
讓我們關注 setup
方法的 props 的相關操作:
<template>
{{name}}
<button @click="handleClick">點我</button>
</template>
// ...
props: {
name: { type: String, default: ' ' }
},
setup(props) {
const { name } = props;
const handleClick = () => {
console.log('name :>> ', name);
};
return { handleClick };
}
// ...
注意:props 無需通過 setup 函數 return,也可以在 template 進行綁定對應的值
我們都知道解構是 es6 一種便捷的手段,編譯成 es5 ,如:
// es6 syntax
const { name } = props;
// to es5 syntax
var name = props.name;
假設父組件更改了 props.name 值,當我們再點擊了 button 輸出的 name 就還是之前的值,不會跟著變化,這其實是一個基礎的 js 的知識點。
為了方便我們對它進行包裝,toRefs
可以理解成批量包裝 props 對象,如:
const { name } = toRefs(props);
const handleClick = () => {
// 因為是包裝對象,所以讀取的時候要用.value
console.log('name :>> ', name.value);
};
可以理解這一切都是因為我們要用解構,toRefs
所采取的解決方案。
toRef
toRef 的用法,就是多了一個參數,允許我們針對一個 key 進行包裝,如:
const name = toRef(props,'name');
console.log('name :>> ', name.value);
watchEffect vs watch
Vue3 的 watch 方法與 Vue2 的概念類似,watchEffect 會讓我們有些疑惑。其實 watchEffect 與 watch 大體類似,區別在于:
watch 可以做到的
- 懶執行副作用
- 更具體地說明什么狀態應該觸發偵聽器重新運行
- 訪問偵聽狀態變化前后的值
對于 Vue2 的 watch 方法,Vue3 的 "watch" 多了一個「清除副作用」 的概念,我們著重關注這點。
這里拿 watchEffect
來舉例:
watchEffect:它立即執行傳入的一個函數,同時響應式追蹤其依賴,并在其依賴變更時重新運行該函數。
watchEffect 方法簡單結構
watchEffect(onInvalidate => {
// 執行副作用
// do something...
onInvalidate(() => {
// 執行/清理失效回調
// do something...
})
})
執行失效回調,有兩個時機
- 副作用即將重新執行時,也就是監聽的數據發生改變時
- 組件卸載時
一個例子:我們要通過 id 發起請求獲取「水果」的詳情,我們監聽 id,當 id 切換過于頻繁(還沒等上個異步數據返回成功)。可能會導致最后 id=1
的數據覆蓋了id=2
的數據,這并不是我們希望的。
我們來模擬并解決這個場景:
模擬接口 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~'
})
];
};
這里封裝了“取消請求”的方法,利用 reject 來完成這一動作。
在 setup 方法
setup() {
const id = ref<number>(1);
const detail = ref<IFruit | {}>({});
watchEffect(async onInvalidate => {
onInvalidate(() => {
cancel && cancel();
});
// 模擬id=2的時候請求時間 1s,id=1的時候請求時間 2s
const [p, cancel] = getFruitsById(id.value, id.value === 2 ? 1000 : 2000);
const res = await p;
detail.value = res;
});
// 模擬頻繁切換id,獲取香蕉的時候,獲取蘋果的結果還沒有回來,取消蘋果的請求,保證數據不會被覆蓋
id.value = 2;
// 最后 detail 值為 { "id": 2, "name": "香蕉", "imgs": "https://xxx.banana.jpg" }
}
如果沒有執行 cancel()
,那么 detail 的數據將會是 { "id": 1, "name": "蘋果", "imgs": "https://xxx.apple.jpg" }
,因為 id=1 數據比較“晚接收到”。
這就是在異步場景下常見的例子,清理失效的回調,保證當前副作用有效,不會被覆蓋。感興趣的小伙伴可以繼續深究。
fragment(片段)
我們都知道在封裝組件的時候,只能有一個 root 。在 Vue3 允許我們有多個 root ,也就是片段,但是在一些操作值得我們注意。
當 inheritAttrs=true[默認]
時,組件會自動在 root 繼承合并 class ,如:
子組件
<template>
<div class="fragment">
<div>div1</div>
<div>div2</div>
</div>
</template>
父組件調用,新增了一個 class
<MyFragment class="extend-class" />
子組件會被渲染成
<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 我們會對 props 里的數據進行規定類型,默認值,非空等一些驗證,可以理解 emits 做了類似的事情,把 emit 規范起來,如:
// 也可以直接用數組,不做驗證
// emits: ['on-update', 'on-other'],
emits: {
// 賦值 null 不驗證
'on-other': null,
// 驗證
'on-update'(val: number) {
if (val === 1) {
return true;
}
// 自定義報錯
console.error('val must be 1');
return false;
}
},
setup(props, ctx) {
const handleEmitUpdate = () => {
// 驗證 val 不為 1,控制臺報錯
ctx.emit('on-update', 2);
};
const handleEmitOther = () => {
ctx.emit('on-other');
};
return { handleEmitUpdate, handleEmitOther };
}
在 setup 中,emit 已經不再用 this.$emit
了,而是 setup 的第二個參數 context
上下文來獲取 emit 。
v-model
個人還是挺喜歡 v-model 的更新的,可以提升封裝組件的體驗感~
在Vue2,假設我需要封裝一個彈框組件 Modal,用
show
變量來控制彈框的顯示隱藏,這肯定是一個父子組件都要維護的值。因為單向數據流,所以需要在 Modal 組件 emit 一個事件,父組件監聽事件接收并修改這個show
值。
為了方便我們會有一些語法糖,如 v-model,但是在 Vue2 一個組件上只能有一個 v-model ,因為語法糖的背后是value
和@input
的組成, 如果還有多個類似這樣的 “雙向修改數據”,我們就需要用語法糖.sync
同步修飾符。
Vue3 把這兩個語法糖統一了,所以我們現在可以在一個組件上使用 多個 v-model 語法糖,舉個例子:
先從父組件看
<VModel v-model="show"
v-model:model1="check"
v-model:model2.hello="textVal" />
hello為自定義修飾符
我們在一個組件上用了 3 個 v-model 語法糖,分別是
v-model 語法糖 | 對應的 prop | 對應的 event | 自定義修飾符對應的 prop |
---|---|---|---|
v-model(default) | modelValue | update:modelValue | 無 |
v-model:model1 | model1 | update:model1 | 無 |
v-model:model2 | model2 | update:model2 | model2Modifiers |
這樣子我們就更清晰的在子組件我們要進行一些什么封裝了,如:
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="請輸入賬號"
v-if="show" />
<input type="text"
placeholder="請輸入郵箱"
v-else />
<button @click="show=!show">Toggle</button>
</template>
類似這樣的 v-if/v-else,在 Vue2 中,會盡可能高效地渲染元素,通常會復用已有元素而不是從頭開始渲染,所以當我們在第一個 input 中輸入,然后切換第二個
input 。第一個 input 的值將會被保留復用。
有些場景下我們不要復用它們,需要添加一個唯一的 key ,如:
<template>
<input type="text"
placeholder="請輸入賬號"
v-if="show"
key="account" />
<input type="text"
placeholder="請輸入郵箱"
v-else
key="email" />
<button @click="show=!show">Toggle</button>
</template>
但是在 Vue3 我們不用顯式的去添加 key ,這兩個 input 元素也是完全獨立的,因為 Vue3 會對 v-if/v-else 自動生成唯一的 key。
全局 API
在 Vue2 我們對于一些全局的配置可能是這樣子的,例如我們使用了一個插件
Vue.use({
/* ... */
});
const app1 = new Vue({ el: '#app-1' });
const app2 = new Vue({ el: '#app-2' });
但是這樣子這會影響兩個根實例,也就是說,會變得不可控。
在 Vue3 引入一個新的 API createApp
方法,返回一個實例:
import { createApp } from 'vue';
const app = createApp({ /* ... */ });
然后我們就可以在這個實例上掛載全局相關方法,并只對當前實例生效,如:
app
.component(/* ... */)
.directive(/* ... */ )
.mixin(/* ... */ )
.use(/* ... */ )
.mount('#app');
需要注意的是,在 Vue2 我們用了 Vue.prototype.$http=()=>{}
這樣的寫法,來對 “根Vue” 的 prototype 進行掛載方法,使得我們在子組件,可以通過原型鏈的方式找到 $http
方法,即 this.$http
。
而在 Vue3 我們類似這樣的掛載需要用一個新的屬性 globalProperties
:
app.config.globalProperties.$http = () => {}
在 setup 內部使用 $http
:
setup() {
const {
ctx: { $http }
} = getCurrentInstance();
}
2. 底層優化
Proxy 代理
Vue2 響應式的基本原理,就是通過 Object.defineProperty
,但這個方式存在缺陷。使得 Vue 不得不通過一些手段來 hack,如:
- Vue.$set() 動態添加新的響應式屬性
- 無法監聽數組變化,Vue 底層需要對數組的一些操作方法,進行再封裝。如
push
,pop
等方法。
而在 Vue3 中優先使用了 Proxy 來處理,它代理的是整個對象而不是對象的屬性,可對于整個對象進行操作。不僅提升了性能,也沒有上面所說的缺陷。
簡單舉兩個例子:
- 動態添加響應式屬性
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
我們對 proxyObj
對象動態添加的屬性也會被攔截到。
Reflect
對象是ES6 為了操作對象而提供的新 API。它有幾個內置的方法,就如上面的 get
/ set
,這里可以理解成我們用 Reflect
更加方便,否則我們需要如:
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return target[propKey];
},
- 對數組的操作進行攔截
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
靜態提升(hoistStatic) vdom
我們都知道 Vue 有虛擬dom的概念,它能為我們在數據改變時高效的渲染頁面。
Vue3 優化了 vdom 的更新性能,簡單舉個例子
Template
<div class="div">
<div>content</div>
<div>{{message}}</div>
</div>
Compiler 后,沒有靜態提升
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 后,有靜態提升
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 */)
]))
}
靜態提升包含「靜態節點」和「靜態屬性」的提升,也就是說,我們把一些靜態的不會變的節點用變量緩存起來,提供下次 re-render 直接調用。
如果沒有做這個動作,當 render
重新執行時,即使標簽是靜態的,也會被重新創建,這就會產生性能消耗。
3. 與 TS
3.0 的一個主要設計目標是增強對 TypeScript 的支持。原本我們期望通過 Class API 來達成這個目標,但是經過討論和原型開發,我們認為 Class 并不是解決這個問題的正確路線,基于 Class 的 API 依然存在類型問題。——尤雨溪
基于函數的 API 天然 與 TS 完美結合。
defineComponent
在 TS 下,我們需要用 Vue 暴露的方法 defineComponent,它單純為了類型推導而存在的。
props 推導
import { defineComponent } from 'vue';
export default defineComponent({
props: {
val1: String,
val2: { type: String, default: '' },
},
setup(props, context) {
props.val1;
}
})
當我們在 setup 方法訪問 props 時候,我們可以看到被推導后的類型,
- val1 我們沒有設置默認值,所以它為
string | undefined
- 而 val2 的值有值,所以是
string
,如圖:
PropType
我們關注一下 props 定義的類型,如果是一個復雜對象,我們就要用 PropType 來進行強轉聲明,如:
interface IObj {
id: number;
name: string;
}
obj: {
type: Object as PropType<IObj>,
default: (): IObj => ({ id: 1, name: '張三' })
},
或 聯合類型
type: {
type: String as PropType<'success' | 'error' | 'warning'>,
default: 'warning'
},
4. build丨更好的 tree-sharking(搖樹優化)
tree-sharking 即在構建工具構建后消除程序中無用的代碼,來減少包的體積。
基于函數的 API 每一個函數都可以用 import { method1,method2 } from "xxx";
,這就對 tree-sharking 非常友好,而且函數名同變量名都可以被壓縮,對象去不可以。舉個例子,我們封裝了一個工具,工具提供了兩個方法,用 method1
,method2
來代替。
我們把它們封裝成一個對象,并且暴露出去,如:
// utils
const obj = {
method1() {},
method2() {}
};
export default obj;
// 調用
import util from '@/utils';
util.method1();
經過webpack打包壓縮之后為:
a={method1:function(){},method2:function(){}};a.method1();
我們不用對象的形式,而用函數的形式來看看:
// utils
export function method1() {}
export function method2() {}
// 調用
import { method1 } from '@/utils';
method1();
經過webpack打包壓縮之后為:
function a(){}a();
用這個例子我們就可以了解 Vue3 為什么能更好的 tree-sharking ,因為它用的是基于函數形式的API,如:
import {
defineComponent,
reactive,
ref,
watchEffect,
watch,
onMounted,
toRefs,
toRef
} from 'vue';
5. options api 與 composition api 取舍
我們上面的代碼都是在 setup 內部實現,但是目前 Vue3 還保留了 Vue2 的 options api 寫法,就是可以“并存”,如:
// ...
setup() {
const val = ref<string>('');
const fn = () => {};
return {
val,
fn
};
},
mounted() {
// 在 mounted 生命周期可以訪問到 setup return 出來的對象
console.log(this.val);
this.fn();
},
// ...
結合 react ,我們知道 “函數式”,hook 是未來的一個趨勢。
所以個人建議還是采用都在 setup
內部寫邏輯的方式,因為 Vue3 可以完全提供 Vue2 的全部能力。
總結
個人覺得不管是 React Hook 還是 Vue3 的 VCA,我們都可以看到現在的前端框架趨勢,“更函數式”,讓邏輯復用更靈活。hook 的模式新增了 React / Vue 的抽象層級,「組件級 + 函數級」,可以讓我們處理邏輯時分的更細,更好維護。
Vue3 One Piece,nice !
最后,前端精本精祝您圣誕快樂??~ (聽說公眾號關注「前端精」會更快樂哦~