前言:vue3新增了Composition API,其中最大的亮点非Hooks莫属,本文将以封装table组件来对比vue2选项式api和vue3中hooks使用方法,帮你更好的理解hooks有哪些优点。
vue2选项式
使用vue2选项式封装原生表格组件。 代码块包含了mthods、computed、created、mounted,computed和methods之间互相调用,耦合性很强,虽然使用方便,但是后期维护比较麻烦。
export default {
components: {
list,
tableTitle,
empty
},
props: {
// 展示的列
columns: {
type: Array,
default: () => []
},
// 源数据
data: {
type: Array,
default: () => []
},
// 是否显示标题头
showTitle: {
type: Boolean,
default: true
},
// 是否显示列表数据
showList: {
type: Boolean,
default: true
},
// 是否显示空状态
showEmpty: {
type: Boolean,
default: false
}
},
data() {
return {
rowTitleId: 'rowTitle' + Math.ceil(Math.random() * 10 ** 10),
rowListId: 'rowList' + Math.ceil(Math.random() * 10 ** 10),
fixedWidthDefault: 0.7, // 单元格默认宽度
viewWidth: 360, // 屏幕默认宽度
scopedSlotsKeys: [], // 插槽name数组
htmlFontSize: 0, // 1rem对应px值
// columns // 测试多级表头
}
},
created() {
this.getScopedSlotsKeys()
},
mounted() {
window.addEventListener('resize', this.init)
this.init()
},
beforeUpdate() {
this.getScopedSlotsKeys()
// this.scrollToTarget()
},
methods: {
init() {
this.getViewWidth()
this.getHtmlFontSize()
},
// 屏幕宽度
getViewWidth() {
this.$nextTick(() => {
const tableDom = this.$refs.tableRef
if (!tableDom) {
return
}
const tableBlockStyle = window.getComputedStyle(tableDom)
const width = +tableBlockStyle.width.replace('px', '')
const paddingLeft = +tableBlockStyle.paddingLeft.replace('px', '')
const paddingRight = +tableBlockStyle.paddingRight.replace('px', '')
const widthSum = width - paddingLeft - paddingRight
this.viewWidth = !isNaN(widthSum) ? widthSum : this.viewWidth
})
},
// 获取1rem对应的px值
getHtmlFontSize() {
this.$nextTick(() => {
const htmldom = document.querySelector('html')
this.htmlFontSize = +window.getComputedStyle(htmldom).fontSize.slice(0, -2)
})
},
// 表头数据
getColumnArr(list) {
return list.reduce(
(pre, cur) => {
pre[cur.deep - 1].push(cur)
return pre
},
Array.from({ length: this.maxDeep }).map(() => [])
)
},
// 获取插槽name数组
getScopedSlotsKeys() {
const slotKeys = Object.keys(this.$scopedSlots || {})
// console.log('slotKeys===>', slotKeys, this.$scopedSlots)
this.scopedSlotsKeys = slotKeys.reduce((pre, key) => {
if (typeof this.$scopedSlots[key] === 'function') {
pre.push(key)
}
return pre
}, [])
},
// 获取列宽和
getColWidth(arr) {
return +arr
.reduce((pre, cur) => {
if (!cur.hasChildren) {
pre += cur.width ? +cur.width : this.fixedWidthDefault
}
return pre
}, 0)
.toFixed(2)
},
// 获取title 插槽
getHeaderSlots(arr) {
return arr.filter(item => item.slotHeader && this.scopedSlotsKeys.includes(item.slotHeader))
},
// 获取列表插槽
getListSlots(arr) {
return arr.filter(item => this.scopedSlotsKeys.includes(item.key))
},
// title和list同步滚动
onscrollHandler(e, id) {
const dom = document.querySelector(`#${id}`)
if (dom) {
dom.scrollLeft = e?.target?.scrollLeft
}
},
// 滚动重置到左侧
resetScrollLeft() {
this.$nextTick(() => {
const domTitle = document.querySelector(`#${this.rowTitleId}`)
const domList = document.querySelector(`#${this.rowListId}`)
if (domTitle) {
scrollLeftTo(domTitle, 0, 0.3);
}
if (domList) {
scrollLeftTo(domList, 0, 0.3);
}
})
},
// 表头和内容列对齐
scrollToTarget() {
const domTitle = document.querySelector(`#${this.rowTitleId}`)
const domList = document.querySelector(`#${this.rowListId}`)
if (!domTitle) {
return
}
const domTitleScrollLeft = domTitle.scrollLeft
if (domList) {
scrollLeftTo(domList, domTitleScrollLeft, 0);
}
},
changeSort(obj) {
let target = null
this.columns.forEach(element => {
if (element.key === obj.key) {
if (element.sort === 0) {
element.sort = 2
} else {
element.sort = element.sort === 1 ? 2 : 1
}
target = {
...element
}
} else if (hasOwn(element, 'sort')) {
element.sort = 0
}
})
// console.log('columns', this.columns)
this.$emit('changeSort', target)
},
/**
* 数据处理
* @params columns: 遍历的数组
* @params obj: 处理后的结果
* @params deep: 当前深度
* @params isFirst: 是否是第一次调用函数
* @params currentKey: 从父级到当前子级的key
* @params fixed: 固定列子集添加fixed值
*/
getColumnList(columns, obj, deep, isFirst, currentKey, fixed) {
columns.forEach(item => {
// 重置深度
if (isFirst) {
deep = 1
}
const ob = {
...item
}
ob.keysStr = isFirst ? ob.key : currentKey + '-' + ob.key
ob.deep = deep
if (fixed) {
ob.fixed = fixed
}
if (!ob.children) {
ob.hasChildren = false
obj.list.push(ob)
} else {
ob.hasChildren = true
deep++
if (deep > obj.maxDeep) {
obj.maxDeep = deep
}
obj.list.push({
...ob,
children: []
})
this.getColumnList(item.children, obj, deep, false, ob.keysStr, ob.fixed || undefined)
}
})
}
},
computed: {
columnObj() {
const obj = {
list: [], // 扁平化数据列表
maxDeep: 1 // 最大深度
}
this.getColumnList(this.columns, obj, 1, true, '', undefined)
obj.list.forEach((item, index) => {
item.colSpan = obj.list.filter(ite => ite.keysStr.includes(item.key) && !ite.hasChildren).length
if (!item.hasChildren) {
item.rowSpan = obj.maxDeep - item.deep + 1 // 根据表格得出规律
} else {
item.rowSpan = 1
}
})
// console.log('list===>', obj.list, obj.maxDeep)
return obj
},
// 标题list
titleList() {
return this.columnObj.list
},
// 标题行数
maxDeep() {
return this.columnObj.maxDeep
},
// 要展示得列数
columnsList() {
return this.titleList.filter(item => !item.hasChildren)
},
// 所有列宽和
rowWidth() {
return this.columnsList.reduce((pre, cur) => {
pre += cur.width ? +cur.width : this.fixedWidthDefault
return pre
}, 0)
},
// x轴宽度超出屏幕宽度
isXScroll() {
// console.log('isXScroll', this.rowWidth, this.viewWidth, this.htmlFontSize)
return this.rowWidth * this.htmlFontSize - this.viewWidth > 0
},
// 左侧固定列
fixedLeftLength() {
const arr = this.columnsList.filter(item => item.fixed === 'left')
return arr.length
},
centerColumnLength() {
return this.columnsList.filter(item => !['left', 'right'].includes(item.fixed)).length
},
// 列list
columnArr() {
let obj = {
left: [],
center: [],
right: []
}
this.titleList.forEach((item, index) => {
// fixed 值为 String
if (item.fixed === 'left' || item.fixed === 'right') {
obj[item.fixed].push(item)
} else {
obj.center.push(item)
}
})
const wt = {
left: this.getColWidth(obj.left),
center: this.getColWidth(obj.center),
right: this.getColWidth(obj.right)
}
const arr = []
Object.keys(obj).forEach(key => {
if (obj[key].length > 0) {
arr.push({
titleList: this.getColumnArr(obj[key]),
col: obj[key].filter(item => !item.hasChildren),
fixed: key,
colWidth: wt[key], // 列宽度
// 此处宽度需动态计算,屏幕宽度不一样对应的1rem等于多少px不一样
width: ['left', 'right'].includes(key) ? wt[key] + 'rem' : `calc(100% - ${wt.left + wt.right}rem)`,
headerSlots: this.getHeaderSlots(obj[key]),
colSlots: this.getListSlots(obj[key]),
shadow: key !== 'center' ? this.isXScroll : false
})
}
})
// 最后一列添加类名
const obje = arr[arr.length - 1]
if (obje) {
const objCol = obje.col
const lastCol = objCol[objCol.length - 1]
if (lastCol) {
lastCol.isLastCol = true
}
obje.titleList.forEach(item => {
const last = item[item.length - 1]
if (last) {
last.isLastCol = true
}
})
}
// console.log('arr===>', arr)
return arr
}
}
}
</script>
vue3 hooks
vue3 hooks 和 vue2 中 mixin 设计初衷是为了提供一种更简洁和直观的方式来共享逻辑和状态,使用过mixin的应该知道它有一些不足之处,hooks刚好解决了这些问题,下面通过代码来看一下hooks优点。
上面代码我们可以封装成3个不同功能的hook,看起来更简介,更容易理解。
// 处理列数据
const { list: colList, maxDeep} = useColumns(props.columns)
// 获取左侧列数、中间列数
const { fixedLeftLength, centerColumnLength, isXScroll } = useDesign(tableRef, colList, fixedWidthDefault)
// 获取最终要渲染的数据
const { columnArr } = useColumnList(colList, maxDeep, isXScroll, fixedWidthDefault)
useDesign hooks函数依赖于useColumns hooks函数的返回值 useColumnList hooks函数依赖于 useDesign 和 useColumns 返回值,最终返回我们要渲染的数据columnArr, 从以上代码可以看出
hooks优点
1、每个hook都是一个独立函数,不需要考虑属性命名冲突
2、 可以清晰的看到属性来自哪个自定义hook
3、hooks可以接受参数,使用更灵活。
4、Vue3自定义Hook可以在引入的时候对同名变量重命名。
入口文件代码
<script setup lang="ts">
import { ref, reactive, computed, unref, onMounted, nextTick, useSlots } from 'vue'
import tableTitle from './title.vue'
import list from './list.vue'
import { useColumns } from '@/hooks/useColumns.ts'
import { useDesign } from '@/hooks/useDesign.ts'
import { useColumnList } from './hooks/useColumnList.ts'
const props = defineProps({
// 展示的列
columns: {
type: Array,
default: () => []
},
// 源数据
data: {
type: Array,
default: () => []
},
// 是否显示标题头
showTitle: {
type: Boolean,
default: true
},
// 是否显示列表数据
showList: {
type: Boolean,
default: true
},
// 是否显示边框
showBorder: {
type: Boolean,
default: false
}
})
const tableRef = ref(null)
const fixedWidthDefault = ref(70) // 单元格默认宽度
// 处理列数据
const { list: colList, maxDeep} = useColumns(props.columns)
// 获取左侧列数、中间列数
const { fixedLeftLength, centerColumnLength, isXScroll } = useDesign(tableRef, colList, fixedWidthDefault)
// 获取最终要渲染的数据
const { columnArr } = useColumnList(colList, maxDeep, isXScroll, fixedWidthDefault)
const rowTitleId = ref('rowTitle' + Math.ceil(Math.random() * 10 ** 10))
const rowListId = ref('rowList' + Math.ceil(Math.random() * 10 ** 10))
// title和list同步滚动
const onscrollHandler = function(e, id) {
const dom = document.querySelector(`#${id}`)
// console.log('onscrollHandler===>', e, dom)
if (dom) {
dom.scrollLeft = e?.target?.scrollLeft
}
}
</script>
useColumns hook 用来给初始数据添加一些属性
import { computed, unref} from 'vue'
import { Columms, ObjTarget } from '../types/column.ts'
/**
* 数据处理
* @params columns: 遍历的数组
* @params obj: 处理后的结果
* @params deep: 当前深度
* @params isFirst: 是否是第一次调用函数
* @params currentKey: 从父级到当前子级的key
* @params fixed: 固定列子集添加fixed值
*/
function getColumnList(columns: Array<Columms>, obj: ObjTarget, deep: number, isFirst: boolean, currentKey: string, fixed: string | undefined) {
columns.forEach(item => {
// 重置深度
if (isFirst) {
deep = 1
}
const ob: Columms = {
...item
}
ob.keysStr = isFirst ? ob.key : currentKey + '-' + ob.key
ob.deep = deep
if (fixed) {
ob.fixed = fixed
}
if (!ob.children) {
ob.hasChildren = false
obj.list.push(ob)
} else {
ob.hasChildren = true
deep++
if (deep > obj.maxDeep) {
obj.maxDeep = deep
}
obj.list.push({
...ob,
children: []
})
getColumnList(item.children, obj, deep, false, ob.keysStr, ob.fixed || undefined)
}
})
}
export function useColumns(columns: any) {
const columnObj = computed(() => {
const obj: any = {
list: [], // 扁平化数据列表
maxDeep: 1 // 最大深度
}
getColumnList(columns, obj, 1, true, '', undefined)
obj.list.forEach((item: any) => {
item.colSpan = obj.list.filter((ite: any) => ite.keysStr.includes(item.key) && !ite.hasChildren).length
if (!item.hasChildren) {
item.rowSpan = obj.maxDeep - item.deep + 1 // 根据表格得出规律
} else {
item.rowSpan = 1
}
})
return obj
})
return unref(columnObj)
}
useDesign hook
import { reactive, onMounted, nextTick, computed, unref} from 'vue'
import type { Ref } from 'vue'
import { Columms } from '../types/column.ts'
export function useDesign(tableRef: Ref, colList: Array<Columms>, fixedWidthDefault: Ref) {
const defaultConst = reactive({
viewWidth: 360
})
// 屏幕宽度
function getViewWidth(dom: Ref): number {
if (!dom.value) {
return defaultConst.viewWidth
}
const tableBlockStyle = window.getComputedStyle(dom.value)
const width = +tableBlockStyle.width.replace('px', '')
const paddingLeft = +tableBlockStyle.paddingLeft.replace('px', '')
const paddingRight = +tableBlockStyle.paddingRight.replace('px', '')
const widthSum = width - paddingLeft - paddingRight
return !isNaN(widthSum) ? widthSum : defaultConst.viewWidth
}
onMounted(() => {
nextTick(() => {
defaultConst.viewWidth = getViewWidth(tableRef)
})
})
// 要展示得列数
const columnsList = computed(() => {
return colList.filter(item => !item.hasChildren)
})
// 计算行宽
const rowWidth = computed(() => {
return columnsList.value.reduce((pre, cur) => {
pre += cur.width ? +cur.width : fixedWidthDefault.value
return pre
}, 0)
})
// 左侧固定列
const fixedLeftLength = computed(() => {
const arr = columnsList.value.filter(item => item.fixed === 'left')
return arr.length
})
// 中间列长度
const centerColumnLength = computed(() => {
return columnsList.value.filter(item => !['left', 'right'].includes(item.fixed)).length
})
// x轴宽度超出屏幕宽度
const isXScroll = computed(() => {
return rowWidth.value - defaultConst.viewWidth > 0
})
// 数组结构
function unRefArr (obj: any) {
return Object.keys(obj).reduce((pre: any, key) => {
pre[key] = unref(obj[key])
return pre
}, {})
}
return unRefArr({
fixedLeftLength,
centerColumnLength,
isXScroll
})
}
useColumnList hook
import { computed, unref, useSlots } from 'vue'
import type { Ref } from 'vue'
export function useColumnList(colList: any, maxDeep: number, isXScroll: boolean, fixedWidthDefault: Ref ) {
const slots = useSlots()
// 获取title 插槽
function getHeaderSlots(arr: any) {
return arr.filter((item: any) => item.slotHeader && slots[item.slotHeader])
}
// 获取列宽和
function getColWidth(arr: any, defaultWidth: number): number {
return +arr
.reduce((pre: any, cur: any) => {
pre += cur.width ? +cur.width : defaultWidth
return pre
}, 0)
.toFixed(2)
}
// 表头数据
function getColumnArr(list: any, maxDeep: number) {
return list.reduce(
(pre: any, cur: any) => {
pre[cur.deep - 1].push(cur)
return pre
},
Array.from({ length: maxDeep }).map(() => [])
)
}
// 列list
const fixedArr = ['left', 'center', 'right']
const columnArr = computed(() => {
let obj = fixedArr.reduce((pre: any, key) => {
pre[key] = []
return pre
}, {})
colList.forEach((item: any) => {
// fixed 值为 String
if (item.fixed === 'left' || item.fixed === 'right') {
obj[item.fixed].push(item)
} else {
obj.center.push(item)
}
})
const wt = fixedArr.reduce((pre: any, key) => {
pre[key] = getColWidth(obj[key], fixedWidthDefault.value)
return pre
}, {})
const arr: any = []
Object.keys(obj).forEach(key => {
if (obj[key].length > 0) {
arr.push({
titleList: getColumnArr(obj[key], maxDeep),
col: obj[key].filter((item: any) => !item.hasChildren),
fixed: key,
colWidth: wt[key], // 列宽度
// 此处宽度需动态计算,屏幕宽度不一样对应的1rem等于多少px不一样
width: ['left', 'right'].includes(key) ? wt[key] + 'px' : `calc(100% - ${wt.left + wt.right}px)`,
headerSlots: getHeaderSlots(obj[key]),
colSlots: obj[key].filter((item: any) => slots[item.key]),
shadow: key !== 'center' ? isXScroll : false
})
}
})
// 最后一列添加类名
const objeFirst = arr[0]
objeFirst.isFirstCol = true
const objeEnd = arr[arr.length - 1]
objeEnd.isLastCol = true
return arr
})
return { columnArr: unref(columnArr) }
}