/** * @file 记录和管理每列的宽度信息的 provider * @description 宽度信息用于需要感知到宽度信息的 feature,比如 fixHead */ import { debounce } from 'lodash' import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef } from 'react' import { ColumnWithLooseAccessor } from 'react-table' import { addListener, removeListener } from 'resize-detector' import { useSafeState, useUpdate } from '../../utils/hooks' import { TableColumnWidths } from '../types' import { areColumnWidthsEquals } from '../utils' import { useScrollbarWidth } from './ScrollbarWidthProvider' import { useTable } from './TableProvider' export type WidthsContextValue = { colWidths: TableColumnWidths, reportHeadCellWidth: (cid: string, width: number) => void reportBodyCellWidth: (cid: string, width: number) => void } export const widthsContext = createContext({ colWidths: {}, reportHeadCellWidth: () => undefined, reportBodyCellWidth: () => undefined }) export function useColumnWidths() { return useContext(widthsContext) } type Props = { children: ReactNode columns: Array> // 除了内部信息,可能有一些外部信息改变后也需要调整宽度信息 adjustWidthsWhenThoseChange: any[] // 默认策略是:如果 head 和 body 同时同步上报,则取最短的 // 默认 false preferLonger?: boolean } export default function ColumnWidthsProvider(props: Props) { const { children, columns, adjustWidthsWhenThoseChange, preferLonger = false } = props // debounce 过程中 Table 可能被卸载了,导致出警告,所以这里换成 safe state const [widths, setWidths] = useSafeState(() => createColWidths(columns)) const prevColumns = useRef>>() const { getContainer } = useTable() const scrollbarWidth = useScrollbarWidth().width // 在当前 React 版本(16.9) 经测试,同步的 setState 会自动合并(未来的版本也会,只是当前版本的文档里没说),而不是每次 setState 都 rerender 一次 // 此同步包括 useEffect 里直接调用 setState,或者在其中同步的调用其他方法触发的 setState // Head 和 Body 上报上来的宽度信息是同步(在 useEffect 里同步上报触发这里的 [setWidths])运行的,所有的 effects 都是在 React commit 阶段之后同步执行 // 又因为第二次等后面的上报结果被同步到 [widths] 之前,依赖第一次等之前的上报结果,见取最大值宽度算法 [mergeWidths] const widthsStateQueueRef = useRef([]) // 一次 rerender 完成后,清空队列里待合并的状态 widthsStateQueueRef.current.length = 0 // 当列发生变化,更新宽度用于重新计算宽度 const realWidths = useMemo(() => { if (prevColumns.current == null) { return createColWidths(columns) } if (prevColumns.current !== columns // 如果是通过子组件的方式传配置的,Table 会 render 两次,第二次不需要重新计算宽度,所以这里对比下 && !areColumnWidthsEquals(createColWidths(prevColumns.current), createColWidths(columns))) { return createColWidths(columns) } return widths }, [columns, widths]) prevColumns.current = columns const _setWidths = useCallback((nextWidths: TableColumnWidths) => { if (!areColumnWidthsEquals(widths, nextWidths)) { widthsStateQueueRef.current.push(nextWidths) // 如果队列里有待合并的状态,说明是调用了同步的 _setWidths push 进去的 if (widthsStateQueueRef.current.length > 1) { const mergedWidths = widthsStateQueueRef.current .reduce((previous, _widths) => mergeWidths( previous, _widths, preferLonger ), {}) setWidths(mergedWidths) return } setWidths(nextWidths) } }, [preferLonger, setWidths, widths]) const reportHeadCellWidth = useReportWidth(columns.length, _setWidths) const reportBodyCellWidth = useReportWidth(columns.length, _setWidths) const value = useMemo(() => ({ colWidths: realWidths, reportHeadCellWidth, reportBodyCellWidth }), [realWidths, reportBodyCellWidth, reportHeadCellWidth]) function resetWidths() { _setWidths(createColWidths(columns)) } const resetWidthsRef = useRef(resetWidths) resetWidthsRef.current = resetWidths const debouncedResetWidthsRef = useRef(debounce(() => { resetWidthsRef.current() }, 100)) // 监听 table wrap 的宽度信息,调整宽度 useEffect(() => { const tableContainer = getContainer() if (tableContainer == null) return const handle = debouncedResetWidthsRef.current addListener(tableContainer, handle) return () => { removeListener(tableContainer, handle) } }, [getContainer]) useUpdate(() => { debouncedResetWidthsRef.current() // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrollbarWidth, ...adjustWidthsWhenThoseChange]) return ( {children} ) } function useReportWidth(length: number, setWidths: (widths: TableColumnWidths) => void) { const widths: TableColumnWidths = {} return function reportWidth(colId: string, width: number) { widths[colId] = width if (Object.keys(widths).length === length) { // 每次 rerender 后 effect 都会被执行,那么上报宽度信息的 effect 也会被运行 // 如果在前一次 effect 执行过程中发生了第二次 rerender,导致第二次上报宽度信息的 effect 也开始执行 // 那么对 ColumnWidthsProvider 来说第二次上报的才是最新的结果,第一次上报的如果完成了是可以直接被 drop 掉的 // 但是实现起来可能有复杂度,如果后续有优化的必要可以考虑这个方向 setWidths(widths) } } } function createColWidths(columns: Array>) { const result: TableColumnWidths = {} columns.forEach(column => { result[column.id as string] = column.width as string // column 创建的时候给了默认值,这里一定是有 }) return result } // widthsB 每个字段都覆盖到 widthsA,但是相同的字段默认只会取两者中最短的一个 function mergeWidths(widthsA: TableColumnWidths, widthsB: TableColumnWidths, preferLonger = false) { const result: TableColumnWidths = {} Object.keys(widthsB).forEach(colId => { if (typeof widthsB[colId] === 'number' && typeof widthsA[colId] === 'number') { if (widthsA[colId] >= widthsB[colId]) { if (preferLonger) { result[colId] = widthsA[colId] return } } } result[colId] = widthsB[colId] }) return result }