写点什么

感受 Vue3 的魔法力量

  • 2023-01-28
    北京
  • 本文字数:5591 字

    阅读完需:约 18 分钟

感受 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、reactiveconst reactivePerson = reactive({ name: '张三', info: { age: 18 } })
//改变属性,观察以下不同情景下的监听结果nextTick(() => { simplePerson.value = '李四' person.value.name = '李四' complexPerson.value.info.age = 20 reactivePerson.info.age = 22})
//情景一:数据源为RefImplwatch(simplePerson, (newVal) => { console.log(newVal) //输出:李四})//情景二:数据源为'张三'watch(simplePerson.value, (newVal) => { console.log(newVal) //非法数据源,监听不到且控制台告警 })//情景三:数据源为RefImpl,但是.value才是响应式对象,所以要加deepwatch(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.vuetype 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>
复制代码


发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
感受 Vue3 的魔法力量_代码_京东科技开发者_InfoQ写作社区