乐趣区

关于vue.js:感受-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、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>




退出移动版