感受 Vue3 的魔法力量

感受,vue3,魔法,力量 · 浏览次数 : 48

小编点评

**组件1** ```typescript import { useDynamicTree } from '@/hooks/useDynamicTree' const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() const handleAdd = () => { //添加条件 } const handleRemove = () => { //删除条件 } const toggleOperator = () => { //切换逻辑关系:且、或 } ``` **组件2** ```typescript import { useDynamicTree } from '@/hooks/useDynamicTree' const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree() const handleAdd = () => { //添加条件 } const handleRemove = () => { //删除条件 } const toggleOperator = () => { //切换逻辑关系:且、或 } ``` **store** ```typescript 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 }}) ``` **使用** ```typescript import { useUserStore } from '@/stores/user' const { userInfo, fullName, setUserInfo } = useUserStore() //...其他逻辑 console.log(userInfo.userName, fullName); ``` **排版** ``` <div class="welcome" font-JDLangZheng> <el-space> <el-avatar :size="60" :src="userInfo.headImg ? userInfo.headImg : avatar""> </div> <div> <p>你好,{{ userInfo.realName }},欢迎回来</p> <p style="font-size: 14px">{{ userInfo.organizationFullName }}</p> </div> ```

正文

  

作者:京东科技 牛至伟

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

感受 Vue3 的魔法力量

摆脱了书写声明式的代码,用起来很流畅,提升不少效率,封装可复用逻辑,将 UI 和逻辑分离,提高复用性,view 层代码展示更清晰,少去了很多配置,使用起来更便捷

Vue3使用Vuex 教程(这才是真正的小白教程!)

我的项目是vue3+element-plus 我是个菜鸡,我不懂前端。想做一个tags的导航标签页。但是点击标签页之后页面仍然是会重新请求。感觉这不就跟没做一样吗? 遂百度GPT,第一种方式采用的就是存储到session里。搞了大半天,突然觉得这样好捞,遂采用vuex。虽然两者理论上区别不大,但是说

Vue3学习(二十四)- 文档页面功能开发

写在前面 这部分真的感觉超级难,其实也不能说难,主要是真的想不到这个思路应该这么做,或者说他好厉害,他怎么知道该这么设计实现。 说下难点吧,我觉得后天逻辑还好,主要是前端部分真的需要点花点时间来思考,比如布局、交互设计的实现等等。 文档页面功能开发 1、任务拆解 增加文档页面,首页点击电子书时,跳转

工作感受月(2024年07月)

224年07月01日 今日工作事项: 1/ 上午处理app service plan的cpu和memory指标数据显示为0,影响了autoscale的正常运行。情况很不乐观。明天是否还是一样问题呢? 2/ 处理手中旧事,跟进全部案例24个中的10+的案例,问是否可以关闭。总关闭量在5个。 3/ 下午

23ai免费版本环境快速就绪

笔者感受就是搞一套Oracle 23ai的学习测试环境,从未如此的简单高效。 因为近期Oracle 23ai这个话题很火,很多人也在找实验环境想亲自体验测试一番。 其实搞这样的环境没有任何的门槛,甚至无需注册任何账号,直接安装免费的Oracle VM VirtualBox,下载现成的23ai环境,双

4大焕新,华为云CCE带你感受容器化上云体验

在容器化日益成为中大型企业上云主流选择的情况下,容器服务如何能帮助用户更简单快捷的上云、高效可信赖的运维?

《一个程序猿的生命周期》-《发展篇》- 46.2000万预算的项目,为什么跟踪15个月失败了

给我们带来最直接的感受是:看似前景很美好,实质上却很卷。最应该反思和总结的是:尊重价值规律,在价值规律的原则下做事是否具备合理性,这是走的更远的基本保障,但是又很难做到。

SLF4J门面日志框架源码探索

我们通过代码入手,层层加码,直观感受SLF4J打印日志,并跟踪代码追本溯源。主要了解,SLF4J是如何作为门面和其他日志框架进行解耦。

ChatGPT开发实战

1.概述 前段时间使用体验了ChatGPT的用法,感受到ChatGPT的强大,通过搜索关键字或者输入自己的意图,能够快速得到自己想要的信息和结果。今天笔者将深挖一下ChatGPT,给大家介绍如何使用ChatGPT的API来实战开发一些例子。 2.内容 2.1 ChatGPT起源 这个还得从谷歌发布B

OOP课第三阶段总结

OOP课第三阶段总结 前言: 我想说的第一句是:”我感受到了设计上的极大缺陷“,从一开始,我完全就忽略了引脚的存在。因为在第二阶段中,家电模拟大作业一、二在不需要考虑引脚的情况下也可以完成。但是当来到第三次,出现了互斥开关,因为互斥开关的特殊性,它具有1、2、3三个引脚,并且连接方式多种多样,所以再