作者:京东科技 牛至伟

近半年有幸参与了一个创新项目,由于没有任何历史包袱,所以选择了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 的魔法力量的更多相关文章

  1. 009_python魔法函数

    11. (译)Python魔法方法指南 原文: http://www.rafekettler.com/magicmethods.html 原作者: Rafe Kettler 翻译: hit9 原版(英 ...

  2. python,魔法方法指南

    1.简介 本指南归纳于我的几个月的博客,主题是 魔法方法 . 什么是魔法方法呢?它们在面向对象的Python的处处皆是.它们是一些可以让你对类添加“魔法”的特殊方法. 它们经常是两个下划线包围来命名的 ...

  3. python之魔法方法介绍

    1.1. 简介 什么是魔法方法呢?它们在面向对象的Python的处处皆是.它们是一些可以让你对类添加“魔法”的特殊方法. 它们经常是两个下划线包围来命名的(比如 __init__ , __lt__ ) ...

  4. python_面向对象魔法方法指南

    原文: http://www.rafekettler.com/magicmethods.html 原作者: Rafe Kettler 翻译: hit9 原版(英文版) Repo: https://gi ...

  5. 20145213《Java程序设计》第二周学习总结

    20145213<Java程序设计>第二周学习总结 教材学习内容总结 本周娄老师给的任务是学习教材的第三章--基础语法.其实我觉得还蛮轻松的,因为在翻开厚重的书本,一股熟悉的气息扑面而来, ...

  6. BZOJ-1925 地精部落 烧脑DP+滚动数组

    1925: [Sdoi2010]地精部落 Time Limit: 10 Sec Memory Limit: 64 MB Submit: 1053 Solved: 633 [Submit][Status ...

  7. some words we should know

    2010年,芬兰艺术家Mikko Kuorinki做了一件独特的艺术品. 他在赫尔辛基的奇亚斯玛当代艺术博物馆(Kiasma museum),找了一堵墙,装了一个175 x 320cm的木架子,上面用 ...

  8. 暑假集训(3)第四弹 -----Frogger(Poj2253)

    题意梗概:青蛙王子最近喜欢上了另一只经常坐在荷叶上的青蛙公主.不过这件事不小心走漏了风声,被某fff团团员知 道了,在青蛙王子准备倾述心意的那一天,fff团团员向湖泊中注入大量的充满诅咒力量的溶液.这 ...

  9. [学习笔记]设计模式之Decorator

    写在前面 为方便读者,本文已添加至索引: 设计模式 学习笔记索引 Decorator(装饰)模式,可以动态地给一个对象添加一些额外的职能.为了更好地理解这个模式,我们将时间线拉回Bridge模式笔记的 ...

  10. akoj-1140-英雄联盟阵营

    英雄联盟阵营 Time Limit:1000MS  Memory Limit:65536KTotal Submit:54 Accepted:16 Description 符文之地——瓦罗兰,作为最大的 ...

随机推荐

  1. 云图说|云数据库MySQL内核小版本升级全攻略

    摘要:华为云数据库MySQL支持自动或手动升级内核小版本,内核小版本的升级涉及性能提升.新功能或问题修复等. 华为云有新的内核小版本发布时,您可以在"实例管理"页面看到内核小版本升 ...

  2. 物联网设备上云难?华为云IoT帮你一键完成模型定义,快速在线调试设备

    摘要:在不到3分钟的操作里,不仅完成了一款智慧烟感设备在云端的模型定义,还通过在线调试了解到了设备和远端通信的过程. 本文分享自华为云社区<物联网设备上云难?华为云IoT帮你一键完成模型定义,快 ...

  3. 面试官问:mysql中时间日期类型和字符串类型的选择

    摘要:MySQL中有多种表示时间日期的数据类型,主要有YEAR.TIME.DATE.DATETIME.TIMESTAMP等 本文分享自华为云社区<一针见血,mysql中时间日期类型和字符串类型的 ...

  4. 云图说|初识ModelArts开发者生态社区——AI Gallery

    阅识风云是华为云信息大咖,擅长将复杂信息多元化呈现,其出品的一张图(云图说).深入浅出的博文(云小课)或短视频(云视厅)总有一款能让您快速上手华为云.更多精彩内容请单击此处. 摘要: AI Galle ...

  5. .NET Moq mock internal类型

    问题 Can not create proxy for type xxx because type xxx is not accessible. Make it public, or internal ...

  6. 批量上传 jar 包到远程 maven 仓库

    mvn deploy命令: mvn deploy:deploy-file -Dmaven.test.skip=true -Dfile=log-lib-1.1.jar -DgroupId=com.sun ...

  7. CodeForces-339B-Xenia and Ringroad (循环队列,水题)

    Xenia lives in a city that has n houses built along the main ringroad. The ringroad houses are numbe ...

  8. AtCoder Beginner Contest 197(Sponsored by Panasonic) Person Editorial

    A - Rotate 先输出第二和第三个字符,然后再输出第一个字符即可 B - Visibility 以 \((x,y)\) 作为起点向4个方向探索不是 # 的点,注意一下会在\((x,y)\)重复计 ...

  9. 六、java操作swift对象存储(resultful方式实现)

    系列导航 一.swift对象存储环境搭建 二.swift添加存储策略 三.swift大对象--动态大对象 四.swift大对象--静态态大对象 五.java操作swift对象存储(官网样例) 六.ja ...

  10. 十、docker镜像的分层

    系列导航 一.docker入门(概念) 二.docker的安装和镜像管理 三.docker容器的常用命令 四.容器的网络访问 五.容器端口转发 六.docker数据卷 七.手动制作docker镜像 八 ...