感受Vue3的魔法力量
近半年有幸參與了一個創新專案,由於沒有任何歷史包袱,所以選擇了Vue3技術棧,總體來說感受如下:
•
setup語法糖<script setup lang="ts">擺脫了書寫宣告式的程式碼,用起來很流暢,提升不少效率
•
可以通過Composition API(組合式API)封裝可複用邏輯,將UI和邏輯分離,提高複用性,view層程式碼展示更清晰
•
和Vue3更搭配的狀態管理庫Pinia,少去了很多配置,使用起來更便捷
•
構建工具Vite,基於ESM和Rollup,省去本地開發時的編譯步驟,但是build打包時還是會編譯(考慮到相容性)
•
必備VSCode外掛Volar,支援Vue3內建API的TS型別推斷,但是不相容Vue2,如果需要在Vue2和Vue3專案中切換,比較麻煩
當然也遇到一些問題,最典型的就是響應式相關的問題
響應式篇
本篇主要藉助watch函式,理解ref、reactive等響應式資料/狀態,有興趣的同學可以檢視Vue3原始碼部分加深理解,
watch資料來源可以是ref (包括計算屬性)、響應式物件、getter 函式、或多個數據源組成的陣列
import { ref, reactive, watch, nextTick } from 'vue'
//定義4種響應式資料/狀態
//1、ref值為基本型別
const simplePerson = ref('張三')
//2、ref值為引用型別,等價於:person.value = reactive({ name: '張三' })
const person = ref({
name: '張三'
})
//3、ref值包含巢狀的引用型別,等價於:complexPerson.value = reactive({ name: '張三', info: { age: 18 } })
const complexPerson = ref({ name: '張三', info: { age: 18 } })
//4、reactive
const reactivePerson = reactive({ name: '張三', info: { age: 18 } })
//改變屬性,觀察以下不同情景下的監聽結果
nextTick(() => {
simplePerson.value = '李四'
person.value.name = '李四'
complexPerson.value.info.age = 20
reactivePerson.info.age = 22
})
//情景一:資料來源為RefImpl
watch(simplePerson, (newVal) => {
console.log(newVal) //輸出:李四
})
//情景二:資料來源為'張三'
watch(simplePerson.value, (newVal) => {
console.log(newVal) //非法資料來源,監聽不到且控制檯告警
})
//情景三:資料來源為RefImpl,但是.value才是響應式物件,所以要加deep
watch(person, (newVal) => {
console.log(newVal) //輸出:{name: '李四'}
},{
deep: true //必須設定,否則監聽不到內部變化
})
//情景四:資料來源為響應式物件
watch(person.value, (newVal) => {
console.log(newVal) //輸出:{name: '李四'}
})
//情景五:資料來源為'張三'
watch(person.value.name, (newVal) => {
console.log(newVal) //非法資料來源,監聽不到且控制檯告警
})
//情景六:資料來源為getter函式,返回基本型別
watch(
() => person.value.name,
(newVal) => {
console.log(newVal) //輸出:李四
}
)
//情景七:資料來源為響應式物件(在Vue3中狀態都是預設深層響應式的)
watch(complexPerson.value.info, (newVal, oldVal) => {
console.log(newVal) //輸出:Proxy {age: 20}
console.log(newVal === oldVal) //輸出:true
})
//情景八:資料來源為getter函式,返回響應式物件
watch(
() => complexPerson.value.info,
(newVal) => {
console.log(newVal) //除非設定deep: true或info屬性被整體替換,否則監聽不到
}
)
//情景九:資料來源為響應式物件
watch(reactivePerson, (newVal) => {
console.log(newVal) //不設定deep: true也可以監聽到
})
總結:
1.
在Vue3中狀態都是預設深層響應式的(情景七),巢狀的引用型別在取值(get)時一定是返回Proxy響應式物件
2.
watch資料來源為響應式物件時(情景四、七、九),會隱式的建立一個深層偵聽器,不需要再顯示設定deep: true
3.
情景三和情景八兩種情況下,必須顯示設定deep: true,強制轉換為深層偵聽器
4.
情景五和情景七對比下,雖然寫法完全相同,但是如果屬性值為基本型別時是監聽不到的,尤其是ts型別宣告為any時,ide也不會提示告警,導致排查問題比較費力
5.
所以精確的ts型別宣告很重要,否則經常會出現莫名其妙的watch不生效的問題
6.
ref值為基本型別時通過get\set攔截實現響應式;ref值為引用型別時通過將.value屬性轉換為reactive響應式物件實現;
7.
deep會影響效能,而reactive會隱式的設定deep: true,所以只有明確狀態資料結構比較簡單且資料量不大時使用reactive,其他一律使用ref
Props篇
設定預設值
type Props = {
placeholder?: string
modelValue: string
multiple?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '請選擇',
multiple: false,
})
雙向繫結(多個值)
•
自定義元件
//FieldSelector.vue
type Props = {
businessTableUuid: string
businessTableFieldUuid?: string
}
const props = defineProps<Props>()
const emits = defineEmits([
'update:businessTableUuid',
'update:businessTableFieldUuid',
])
const businessTableUuid = ref('')
const businessTableFieldUuid = ref('')
// props.businessTableUuid、props.businessTableFieldUuid轉為本地狀態,此處省略
//表切換
const tableChange = (businessTableUuid: string) => {
emits('update:businessTableUuid', businessTableUuid)
emits('update:businessTableFieldUuid', '')
businessTableFieldUuid.value = ''
}
//欄位切換
const fieldChange = (businessTableFieldUuid: string) => {
emits('update:businessTableFieldUuid', businessTableFieldUuid)
}
•
使用元件
<template>
<FieldSelector
v-model:business-table-uuid="stringFilter.businessTableUuid"
v-model:business-table-field-uuid="stringFilter.businessTableFieldUuid"
/>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const stringFilter = reactive({
businessTableUuid: '',
businessTableFieldUuid: ''
})
</script>
單向資料流
1.
大部分情況下應該遵循【單向資料流】原則,禁止子元件直接修改props,否則複雜應用下的資料流將變得混亂,極易出現bug且難排查
2.
直接修改props會有告警,但是如果props是引用型別,修改props內部值將不會有告警提示,因此應該有團隊約定(第5條除外)
3.
如果props為引用型別,賦值到子元件狀態時,需要解除引用(第5條除外)
4.
複雜的邏輯,可以將狀態以及修改狀態的方法,封裝成自定義hooks或者提升到store內部,避免props的層層傳遞與修改
5.
一些父子元件本就緊密耦合的場景下,可以允許修改props內部的值,可以減少很多複雜度和工作量(需要團隊約定固定場景)
邏輯/UI解耦篇
利用Vue3的Composition/組合式API,將某種邏輯涉及到的狀態,以及修改狀態的方法封裝成一個自定義hook,將元件中的邏輯解耦,這樣即使UI有不同的形態或者調整,只要邏輯不變,就可以複用邏輯。下面是本專案中涉及的一個真實案例-邏輯樹元件,UI有2種形態且可以相互轉化。

•
hooks部分的程式碼:useDynamicTree.ts
import { ref } from 'vue'
import { nanoid } from 'nanoid'
export type TreeNode = {
id?: string
pid: string
nodeUuid?: string
partentUuid?: string
nodeType: string
nodeValue?: any
logicValue?: any
children: TreeNode[]
level?: number
}
export const useDynamicTree = (root?: TreeNode) => {
const tree = ref<TreeNode[]>(root ? [root] : [])
const level = ref(0)
//新增節點
const add = (node: TreeNode, pid: string = 'root'): boolean => {
//新增根節點
if (pid === '') {
tree.value = [node]
return true
}
level.value = 0
const pNode = find(tree.value, pid)
if (!pNode) return false
//巢狀關係不能超過3層
if (pNode.level && pNode.level > 2) return false
if (!node.id) {
node.id = nanoid()
}
if (pNode.nodeType === 'operator') {
pNode.children.push(node)
} else {
//如果父節點不是關係節點,則構建新的關係節點
const current = JSON.parse(JSON.stringify(pNode))
current.pid = pid
current.id = nanoid()
Object.assign(pNode, {
nodeType: 'operator',
nodeValue: 'and',
// 重置回顯資訊
logicValue: undefined,
nodeUuid: undefined,
parentUuid: undefined,
children: [current, node],
})
}
return true
}
//刪除節點
const remove = (id: string) => {
const node = find(tree.value, id)
if (!node) return
//根節點處理
if (node.pid === '') {
tree.value = []
return
}
const pNode = find(tree.value, node.pid)
if (!pNode) return
const index = pNode.children.findIndex((item) => item.id === id)
if (index === -1) return
pNode.children.splice(index, 1)
if (pNode.children.length === 1) {
//如果只剩下一個節點,則替換父節點(關係節點)
const [one] = pNode.children
Object.assign(
pNode,
{
...one,
},
{
pid: pNode.pid,
},
)
if (pNode.pid === '') {
pNode.id = 'root'
}
}
}
//切換邏輯關係:且/或
const toggleOperator = (id: string) => {
const node = find(tree.value, id)
if (!node) return
if (node.nodeType !== 'operator') return
node.nodeValue = node.nodeValue === 'and' ? 'or' : 'and'
}
//查詢節點
const find = (node: TreeNode[], id: string): TreeNode | undefined => {
// console.log(node, id)
for (let i = 0; i < node.length; i++) {
if (node[i].id === id) {
Object.assign(node[i], {
level: level.value,
})
return node[i]
}
if (node[i].children?.length > 0) {
level.value += 1
const result = find(node[i].children, id)
if (result) {
return result
}
level.value -= 1
}
}
return undefined
}
//提供遍歷節點方法,支援回撥
const dfs = (node: TreeNode[], callback: (node: TreeNode) => void) => {
for (let i = 0; i < node.length; i++) {
callback(node[i])
if (node[i].children?.length > 0) {
dfs(node[i].children, callback)
}
}
}
return {
tree,
add,
remove,
toggleOperator,
dfs,
}
}
•
在不同元件中使用(UI1/UI2元件為遞迴元件,內部實現不再展開)
//元件1
<template>
<UI1
:logic="logic"
:on-add="handleAdd"
:on-remove="handleRemove"
:toggle-operator="toggleOperator"
</UI1>
</template>
<script setup lang="ts">
import { useDynamicTree } from '@/hooks/useDynamicTree'
const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()
const handleAdd = () => {
//新增條件
}
const handleRemove = () => {
//刪除條件
}
const toggleOperator = () => {
//切換邏輯關係:且、或
}
</script>
//元件2
<template>
<UI2 :logic="logic"
:on-add="handleAdd"
:on-remove="handleRemove"
:toggle-operator="toggleOperator"
</UI2>
</template>
<script setup lang="ts">
import { useDynamicTree } from '@/hooks/useDynamicTree'
const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()
const handleAdd = () => { //新增條件 }
const handleRemove = () => { //刪除條件 }
const toggleOperator = () => { //切換邏輯關係:且、或 }
</script>
Pinia狀態管理篇
將複雜邏輯的狀態以及修改狀態的方法提升到store內部管理,可以避免props的層層傳遞,減少props複雜度,狀態管理更清晰
•
定義一個store(非宣告式):User.ts
import { computed, reactive } from 'vue'
import { defineStore } from 'pinia'
type UserInfo = {
userName: string
realName: string
headImg: string
organizationFullName: string
}
export const useUserStore = defineStore('user', () => {
const userInfo = reactive<UserInfo>({
userName: '',
realName: '',
headImg: '',
organizationFullName: ''
})
const fullName = computed(() => {
return `${userInfo.userName}[${userInfo.realName}]`
})
const setUserInfo = (info: UserInfo) => {
Object.assgin(userInfo, {...info})
}
return {
userInfo,
fullName,
setUserInfo
}
})
•
在元件中使用
<template>
<div class="welcome" font-JDLangZheng>
<el-space>
<el-avatar :size="60" :src="userInfo.headImg ? userInfo.headImg : avatar"> </el-avatar>
<div>
<p>你好,{{ userInfo.realName }},歡迎回來</p>
<p style="font-size: 14px">{{ userInfo.organizationFullName }}</p>
</div>
</el-space>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import avatar from '@/assets/avatar.png'
const { userInfo } = useUserStore()
</script>
「其他文章」
- 應用健康度隱患刨析解決系列之資料庫時區設定
- 對於Vue3和Ts的心得和思考
- 一文詳解擴散模型:DDPM
- zookeeper的Leader選舉原始碼解析
- 一文帶你搞懂如何優化慢SQL
- 京東金融Android瘦身探索與實踐
- 微前端框架single-spa子應用載入解析
- cookie時效無限延長方案
- 聊聊前端效能指標那些事兒
- Spring竟然可以建立“重複”名稱的bean?—一次專案中存在多個bean名稱重複問題的排查
- 京東金融Android瘦身探索與實踐
- Spring原始碼核心剖析
- 深入淺出RPC服務 | 不同層的網路協議
- 安全測試之探索windows遊戲掃雷
- 關於資料庫分庫分表的一點想法
- 對於Vue3和Ts的心得和思考
- Bitmap、RoaringBitmap原理分析
- 京東小程式CI工具實踐
- 測試用例設計指南
- 當你對 redis 說你中意的女孩是 Mia