![](http://img.szonline.cn/2022/1118/20221118104248143.jpg)
作者:京東科技 牛至偉
近半年有幸參與了一個(gè)創(chuàng)新項(xiàng)目,由于沒(méi)有任何歷史包袱,所以選擇了Vue3技術(shù)棧,總體來(lái)說(shuō)感受如下:
? setup語(yǔ)法糖<script setup lang="ts">擺脫了書寫聲明式的代碼,用起來(lái)很流暢,提升不少效率
(資料圖片僅供參考)
? 可以通過(guò)Composition API(組合式API)封裝可復(fù)用邏輯,將UI和邏輯分離,提高復(fù)用性,view層代碼展示更清晰
? 和Vue3更搭配的狀態(tài)管理庫(kù)Pinia,少去了很多配置,使用起來(lái)更便捷
? 構(gòu)建工具Vite,基于ESM和Rollup,省去本地開發(fā)時(shí)的編譯步驟,但是build打包時(shí)還是會(huì)編譯(考慮到兼容性)
? 必備VSCode插件Volar,支持Vue3內(nèi)置API的TS類型推斷,但是不兼容Vue2,如果需要在Vue2和Vue3項(xiàng)目中切換,比較麻煩
當(dāng)然也遇到一些問(wèn)題,最典型的就是響應(yīng)式相關(guān)的問(wèn)題
本篇主要借助watch函數(shù),理解ref、reactive等響應(yīng)式數(shù)據(jù)/狀態(tài),有興趣的同學(xué)可以查看Vue3源代碼部分加深理解,
watch數(shù)據(jù)源可以是ref (包括計(jì)算屬性)、響應(yīng)式對(duì)象、getter 函數(shù)、或多個(gè)數(shù)據(jù)源組成的數(shù)組
import { ref, reactive, watch, nextTick } from "vue"http://定義4種響應(yīng)式數(shù)據(jù)/狀態(tài)//1、ref值為基本類型const simplePerson = ref("張三") //2、ref值為引用類型,等價(jià)于:person.value = reactive({ name: "張三" })const person = ref({ name: "張三"})//3、ref值包含嵌套的引用類型,等價(jià)于:complexPerson.value = reactive({ name: "張三", info: { age: 18 } })const complexPerson = ref({ name: "張三", info: { age: 18 } })//4、reactiveconst reactivePerson = reactive({ name: "張三", info: { age: 18 } })//改變屬性,觀察以下不同情景下的監(jiān)聽結(jié)果nextTick(() => { simplePerson.value = "李四" person.value.name = "李四" complexPerson.value.info.age = 20 reactivePerson.info.age = 22})//情景一:數(shù)據(jù)源為RefImplwatch(simplePerson, (newVal) => { console.log(newVal) //輸出:李四})//情景二:數(shù)據(jù)源為"張三"watch(simplePerson.value, (newVal) => { console.log(newVal) //非法數(shù)據(jù)源,監(jiān)聽不到且控制臺(tái)告警 })//情景三:數(shù)據(jù)源為RefImpl,但是.value才是響應(yīng)式對(duì)象,所以要加deepwatch(person, (newVal) => { console.log(newVal) //輸出:{name: "李四"}},{ deep: true //必須設(shè)置,否則監(jiān)聽不到內(nèi)部變化}) //情景四:數(shù)據(jù)源為響應(yīng)式對(duì)象watch(person.value, (newVal) => { console.log(newVal) //輸出:{name: "李四"}})//情景五:數(shù)據(jù)源為"張三"watch(person.value.name, (newVal) => { console.log(newVal) //非法數(shù)據(jù)源,監(jiān)聽不到且控制臺(tái)告警 })//情景六:數(shù)據(jù)源為getter函數(shù),返回基本類型watch( () => person.value.name, (newVal) => { console.log(newVal) //輸出:李四 })//情景七:數(shù)據(jù)源為響應(yīng)式對(duì)象(在Vue3中狀態(tài)都是默認(rèn)深層響應(yīng)式的)watch(complexPerson.value.info, (newVal, oldVal) => { console.log(newVal) //輸出:Proxy {age: 20} console.log(newVal === oldVal) //輸出:true}) //情景八:數(shù)據(jù)源為getter函數(shù),返回響應(yīng)式對(duì)象watch( () => complexPerson.value.info, (newVal) => { console.log(newVal) //除非設(shè)置deep: true或info屬性被整體替換,否則監(jiān)聽不到 })//情景九:數(shù)據(jù)源為響應(yīng)式對(duì)象watch(reactivePerson, (newVal) => { console.log(newVal) //不設(shè)置deep: true也可以監(jiān)聽到 })
總結(jié):
在Vue3中狀態(tài)都是默認(rèn)深層響應(yīng)式的(情景七),嵌套的引用類型在取值(get)時(shí)一定是返回Proxy響應(yīng)式對(duì)象watch數(shù)據(jù)源為響應(yīng)式對(duì)象時(shí)(情景四、七、九),會(huì)隱式的創(chuàng)建一個(gè)深層偵聽器,不需要再顯示設(shè)置deep: true情景三和情景八兩種情況下,必須顯示設(shè)置deep: true,強(qiáng)制轉(zhuǎn)換為深層偵聽器情景五和情景七對(duì)比下,雖然寫法完全相同,但是如果屬性值為基本類型時(shí)是監(jiān)聽不到的,尤其是ts類型聲明為any時(shí),ide也不會(huì)提示告警,導(dǎo)致排查問(wèn)題比較費(fèi)力所以精確的ts類型聲明很重要,否則經(jīng)常會(huì)出現(xiàn)莫名其妙的watch不生效的問(wèn)題ref值為基本類型時(shí)通過(guò)get\\set攔截實(shí)現(xiàn)響應(yīng)式;ref值為引用類型時(shí)通過(guò)將.value屬性轉(zhuǎn)換為reactive響應(yīng)式對(duì)象實(shí)現(xiàn);deep會(huì)影響性能,而reactive會(huì)隱式的設(shè)置deep: true,所以只有明確狀態(tài)數(shù)據(jù)結(jié)構(gòu)比較簡(jiǎn)單且數(shù)據(jù)量不大時(shí)使用reactive,其他一律使用reftype Props = { placeholder?: string modelValue: string multiple?: boolean}const props = withDefaults(defineProps(), { placeholder: "請(qǐng)選擇", multiple: false,})
? 自定義組件
//FieldSelector.vuetype Props = { businessTableUuid: string businessTableFieldUuid?: string}const props = defineProps()const emits = defineEmits([ "update:businessTableUuid", "update:businessTableFieldUuid",])const businessTableUuid = ref("")const businessTableFieldUuid = ref("")// props.businessTableUuid、props.businessTableFieldUuid轉(zhuǎn)為本地狀態(tài),此處省略//表切換const tableChange = (businessTableUuid: string) => { emits("update:businessTableUuid", businessTableUuid) emits("update:businessTableFieldUuid", "") businessTableFieldUuid.value = ""}//字段切換const fieldChange = (businessTableFieldUuid: string) => { emits("update:businessTableFieldUuid", businessTableFieldUuid)}
? 使用組件
<script setup lang="ts">import { reactive } from "vue"const stringFilter = reactive({ businessTableUuid: "", businessTableFieldUuid: ""})</script>
利用Vue3的Composition/組合式API,將某種邏輯涉及到的狀態(tài),以及修改狀態(tài)的方法封裝成一個(gè)自定義hook,將組件中的邏輯解耦,這樣即使UI有不同的形態(tài)或者調(diào)整,只要邏輯不變,就可以復(fù)用邏輯。下面是本項(xiàng)目中涉及的一個(gè)真實(shí)案例-邏輯樹組件,UI有2種形態(tài)且可以相互轉(zhuǎn)化。
? 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(root ? [root] : []) const level = ref(0) //添加節(jié)點(diǎn) const add = (node: TreeNode, pid: string = "root"): boolean => { //添加根節(jié)點(diǎn) if (pid === "") { tree.value = [node] return true } level.value = 0 const pNode = find(tree.value, pid) if (!pNode) return false //嵌套關(guān)系不能超過(guò)3層 if (pNode.level && pNode.level > 2) return false if (!node.id) { node.id = nanoid() } if (pNode.nodeType === "operator") { pNode.children.push(node) } else { //如果父節(jié)點(diǎn)不是關(guān)系節(jié)點(diǎn),則構(gòu)建新的關(guān)系節(jié)點(diǎn) 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 } //刪除節(jié)點(diǎn) const remove = (id: string) => { const node = find(tree.value, id) if (!node) return //根節(jié)點(diǎn)處理 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) { //如果只剩下一個(gè)節(jié)點(diǎn),則替換父節(jié)點(diǎn)(關(guān)系節(jié)點(diǎn)) const [one] = pNode.children Object.assign( pNode, { ...one, }, { pid: pNode.pid, }, ) if (pNode.pid === "") { pNode.id = "root" } } } //切換邏輯關(guān)系:且/或 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" } //查找節(jié)點(diǎn) 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 } //提供遍歷節(jié)點(diǎn)方法,支持回調(diào) 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組件為遞歸組件,內(nèi)部實(shí)現(xiàn)不再展開)
//組件1<script setup lang="ts"> import { useDynamicTree } from "@/hooks/useDynamicTree" const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() const handleAdd = () => { //添加條件 } const handleRemove = () => { //刪除條件 } const toggleOperator = () => { //切換邏輯關(guān)系:且、或 }</script>
//組件2<script setup lang="ts"> import { useDynamicTree } from "@/hooks/useDynamicTree" const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() const handleAdd = () => { //添加條件 } const handleRemove = () => { //刪除條件 } const toggleOperator = () => { //切換邏輯關(guān)系:且、或 } </script>
將復(fù)雜邏輯的狀態(tài)以及修改狀態(tài)的方法提升到store內(nèi)部管理,可以避免props的層層傳遞,減少props復(fù)雜度,狀態(tài)管理更清晰
? 定義一個(gè)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({ userName: "", realName: "", headImg: "", organizationFullName: "" }) const fullName = computed(() => { return `${userInfo.userName}[${userInfo.realName}]` }) const setUserInfo = (info: UserInfo) => { Object.assgin(userInfo, {...info}) } return { userInfo, fullName, setUserInfo }})
? 在組件中使用
<script setup lang="ts"> import { useUserStore } from "@/stores/user" import avatar from "@/assets/avatar.png" const { userInfo } = useUserStore()</script>你好,{{ userInfo.realName }},歡迎回來(lái)
{{ userInfo.organizationFullName }}
標(biāo)簽: 引用類型 基本類型 邏輯關(guān)系