感受 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 函数、或多个数据源组成的数组

1import { ref, reactive, watch, nextTick } from 'vue' 2 3//定义4种响应式数据/状态 4//1、ref值为基本类型 5const simplePerson = ref('张三') 6//2、ref值为引用类型,等价于:person.value = reactive({ name: '张三' }) 7const person = ref({ 8 name: '张三' 9}) 10//3、ref值包含嵌套的引用类型,等价于:complexPerson.value = reactive({ name: '张三', info: { age: 18 } }) 11const complexPerson = ref({ name: '张三', info: { age: 18 } }) 12//4、reactive 13const reactivePerson = reactive({ name: '张三', info: { age: 18 } }) 14 15//改变属性,观察以下不同情景下的监听结果 16nextTick(() => { 17 simplePerson.value = '李四' 18 person.value.name = '李四' 19 complexPerson.value.info.age = 20 20 reactivePerson.info.age = 22 21}) 22 23//情景一:数据源为RefImpl 24watch(simplePerson, (newVal) => { 25 console.log(newVal) //输出:李四 26}) 27//情景二:数据源为'张三' 28watch(simplePerson.value, (newVal) => { 29 console.log(newVal) //非法数据源,监听不到且控制台告警 30}) 31//情景三:数据源为RefImpl,但是.value才是响应式对象,所以要加deep 32watch(person, (newVal) => { 33 console.log(newVal) //输出:{name: '李四'} 34},{ 35 deep: true //必须设置,否则监听不到内部变化 36}) 37//情景四:数据源为响应式对象 38watch(person.value, (newVal) => { 39 console.log(newVal) //输出:{name: '李四'} 40}) 41//情景五:数据源为'张三' 42watch(person.value.name, (newVal) => { 43 console.log(newVal) //非法数据源,监听不到且控制台告警 44}) 45//情景六:数据源为getter函数,返回基本类型 46watch( 47 () => person.value.name, 48 (newVal) => { 49 console.log(newVal) //输出:李四 50 } 51) 52//情景七:数据源为响应式对象(在Vue3中状态都是默认深层响应式的) 53watch(complexPerson.value.info, (newVal, oldVal) => { 54 console.log(newVal) //输出:Proxy {age: 20} 55 console.log(newVal === oldVal) //输出:true 56}) 57//情景八:数据源为getter函数,返回响应式对象 58watch( 59 () => complexPerson.value.info, 60 (newVal) => { 61 console.log(newVal) //除非设置deep: true或info属性被整体替换,否则监听不到 62 } 63) 64//情景九:数据源为响应式对象 65watch(reactivePerson, (newVal) => { 66 console.log(newVal) //不设置deep: true也可以监听到 67}) 68 69 70 71 72

总结:

  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篇

设置默认值

1type Props = { 2 placeholder?: string 3 modelValue: string 4 multiple?: boolean 5} 6const props = withDefaults(defineProps<Props>(), { 7 placeholder: '请选择', 8 multiple: false, 9}) 10 11 12 13 14

双向绑定(多个值)

• 自定义组件

1//FieldSelector.vue 2type Props = { 3 businessTableUuid: string 4 businessTableFieldUuid?: string 5} 6const props = defineProps<Props>() 7const emits = defineEmits([ 8 'update:businessTableUuid', 9 'update:businessTableFieldUuid', 10]) 11const businessTableUuid = ref('') 12const businessTableFieldUuid = ref('') 13// props.businessTableUuid、props.businessTableFieldUuid转为本地状态,此处省略 14//表切换 15const tableChange = (businessTableUuid: string) => { 16 emits('update:businessTableUuid', businessTableUuid) 17 emits('update:businessTableFieldUuid', '') 18 businessTableFieldUuid.value = '' 19} 20//字段切换 21const fieldChange = (businessTableFieldUuid: string) => { 22 emits('update:businessTableFieldUuid', businessTableFieldUuid) 23} 24 25 26 27 28

• 使用组件

1<template> 2 <FieldSelector 3 v-model:business-table-uuid="stringFilter.businessTableUuid" 4 v-model:business-table-field-uuid="stringFilter.businessTableFieldUuid" 5 /> 6</template> 7<script setup lang="ts"> 8import { reactive } from 'vue' 9const stringFilter = reactive({ 10 businessTableUuid: '', 11 businessTableFieldUuid: '' 12}) 13</script> 14 15 16 17 18

单向数据流

  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

1import { ref } from 'vue' 2import { nanoid } from 'nanoid' 3export type TreeNode = { 4 id?: string 5 pid: string 6 nodeUuid?: string 7 partentUuid?: string 8 nodeType: string 9 nodeValue?: any 10 logicValue?: any 11 children: TreeNode[] 12 level?: number 13} 14export const useDynamicTree = (root?: TreeNode) => { 15 const tree = ref<TreeNode[]>(root ? [root] : []) 16 const level = ref(0) 17 //添加节点 18 const add = (node: TreeNode, pid: string = 'root'): boolean => { 19 //添加根节点 20 if (pid === '') { 21 tree.value = [node] 22 return true 23 } 24 level.value = 0 25 const pNode = find(tree.value, pid) 26 if (!pNode) return false 27 //嵌套关系不能超过3层 28 if (pNode.level && pNode.level > 2) return false 29 if (!node.id) { 30 node.id = nanoid() 31 } 32 if (pNode.nodeType === 'operator') { 33 pNode.children.push(node) 34 } else { 35 //如果父节点不是关系节点,则构建新的关系节点 36 const current = JSON.parse(JSON.stringify(pNode)) 37 current.pid = pid 38 current.id = nanoid() 39 Object.assign(pNode, { 40 nodeType: 'operator', 41 nodeValue: 'and', 42 // 重置回显信息 43 logicValue: undefined, 44 nodeUuid: undefined, 45 parentUuid: undefined, 46 children: [current, node], 47 }) 48 } 49 return true 50 } 51 //删除节点 52 const remove = (id: string) => { 53 const node = find(tree.value, id) 54 if (!node) return 55 //根节点处理 56 if (node.pid === '') { 57 tree.value = [] 58 return 59 } 60 const pNode = find(tree.value, node.pid) 61 if (!pNode) return 62 const index = pNode.children.findIndex((item) => item.id === id) 63 if (index === -1) return 64 pNode.children.splice(index, 1) 65 if (pNode.children.length === 1) { 66 //如果只剩下一个节点,则替换父节点(关系节点) 67 const [one] = pNode.children 68 Object.assign( 69 pNode, 70 { 71 ...one, 72 }, 73 { 74 pid: pNode.pid, 75 }, 76 ) 77 if (pNode.pid === '') { 78 pNode.id = 'root' 79 } 80 } 81 } 82 //切换逻辑关系:且/或 83 const toggleOperator = (id: string) => { 84 const node = find(tree.value, id) 85 if (!node) return 86 if (node.nodeType !== 'operator') return 87 node.nodeValue = node.nodeValue === 'and' ? 'or' : 'and' 88 } 89 //查找节点 90 const find = (node: TreeNode[], id: string): TreeNode | undefined => { 91 // console.log(node, id) 92 for (let i = 0; i < node.length; i++) { 93 if (node[i].id === id) { 94 Object.assign(node[i], { 95 level: level.value, 96 }) 97 return node[i] 98 } 99 if (node[i].children?.length > 0) { 100 level.value += 1 101 const result = find(node[i].children, id) 102 if (result) { 103 return result 104 } 105 level.value -= 1 106 } 107 } 108 return undefined 109 } 110 //提供遍历节点方法,支持回调 111 const dfs = (node: TreeNode[], callback: (node: TreeNode) => void) => { 112 for (let i = 0; i < node.length; i++) { 113 callback(node[i]) 114 if (node[i].children?.length > 0) { 115 dfs(node[i].children, callback) 116 } 117 } 118 } 119 return { 120 tree, 121 add, 122 remove, 123 toggleOperator, 124 dfs, 125 } 126} 127 128 129 130 131 132 133

• 在不同组件中使用(UI1/UI2组件为递归组件,内部实现不再展开)

1//组件1 2<template> 3 <UI1 4 :logic="logic" 5 :on-add="handleAdd" 6 :on-remove="handleRemove" 7 :toggle-operator="toggleOperator" 8 </UI1> 9</template> 10<script setup lang="ts"> 11 import { useDynamicTree } from '@/hooks/useDynamicTree' 12 const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() 13 const handleAdd = () => { 14 //添加条件 15 } 16 const handleRemove = () => { 17 //删除条件 18 } 19 const toggleOperator = () => { 20 //切换逻辑关系:且、或 21 } 22</script> 23 24 25 26 27
1//组件2 2<template> 3 <UI2 :logic="logic" 4 :on-add="handleAdd" 5 :on-remove="handleRemove" 6 :toggle-operator="toggleOperator" 7 </UI2> 8</template> 9<script setup lang="ts"> 10 import { useDynamicTree } from '@/hooks/useDynamicTree' 11 const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() 12 const handleAdd = () => { //添加条件 } 13 const handleRemove = () => { //删除条件 } 14 const toggleOperator = () => { //切换逻辑关系:且、或 } 15</script> 16 17 18 19 20

Pinia状态管理篇

将复杂逻辑的状态以及修改状态的方法提升到store内部管理,可以避免props的层层传递,减少props复杂度,状态管理更清晰

• 定义一个store(非声明式):User.ts

1import { computed, reactive } from 'vue' 2import { defineStore } from 'pinia' 3type UserInfo = { 4 userName: string 5 realName: string 6 headImg: string 7 organizationFullName: string 8} 9export const useUserStore = defineStore('user', () => { 10 const userInfo = reactive<UserInfo>({ 11 userName: '', 12 realName: '', 13 headImg: '', 14 organizationFullName: '' 15 }) 16 const fullName = computed(() => { 17 return `${userInfo.userName}[${userInfo.realName}]` 18 }) 19 const setUserInfo = (info: UserInfo) => { 20 Object.assgin(userInfo, {...info}) 21 } 22 return { 23 userInfo, 24 fullName, 25 setUserInfo 26 } 27}) 28 29 30 31 32 33 34

• 在组件中使用

1<template> 2 <div class="welcome" font-JDLangZheng> 3 <el-space> 4 <el-avatar :size="60" :src="userInfo.headImg ? userInfo.headImg : avatar"> </el-avatar> 5 <div> 6 <p>你好,{{ userInfo.realName }},欢迎回来</p> 7 <p style="font-size: 14px">{{ userInfo.organizationFullName }}</p> 8 </div> 9 </el-space> 10 </div> 11</template> 12<script setup lang="ts"> 13 import { useUserStore } from '@/stores/user' 14 import avatar from '@/assets/avatar.png' 15 const { userInfo } = useUserStore() 16</script> 17 18 19 20 21
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

PPDB:今晚老齐直播

【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。

FLV文件格式

1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  

mysql设置时区

mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0