solid.js 虚拟列表/表格

使用 react 来做一些复杂的虚拟列表/表格, 还是可以感觉到掉帧, 也不平滑.
换成 solidjs 重新实现一遍功能. 非常的丝滑, 不愧是 性能接近原生js 的框架👍👍👍👍

npm install solidjs-use

"solidjs-use": "^2.3.0"

https://github.com/solidjs-use/solidjs-use

https://solidjs-use.github.io/solidjs-use/core/useVirtualList

传入自己的 array 数据, 并设置大概的 item 高度

let vList = useVirtualList(myDataList, {
  itemHeight: 62,
});

虚拟表格, 两个 div 中放入 table , 在 tbody 中 循环显示 tr 即可

普通的列表, 则省略 table

<div
  ref={vList.containerProps.ref}
  style={vList.containerProps.style}
  onScroll={vList.containerProps.onScroll}
  class="h-full"
>
  <div style={vList.wrapperProps().style}>
    <Table
      style={{
        "table-layout": "fixed",
      }}
      class="w-full"
    >
      <thead>
        <tr>
          <th class="sticky top-0 bg-gray-800 text-white">id</th>
          <th class="sticky top-0 bg-gray-800 text-white">名称</th>
          <th class="sticky top-0 bg-gray-800 text-white"></th>
        </tr>
      </thead>
      <tbody>
        <For each={vList.list()}>
          {(listItem) => {

            let item = listItem.data;

            return (
              <tr>
                <td>{item.keyName}</td>                
              </tr>
            );
          }}
        </For>
      </tbody>
    </Table>
  </div>

小试牛刀, 用 solidjs 实现一个普通的虚拟列表,学习原理

仅供学习, 真实使用请选择 solidjs-use 或者 其他相关库

import {
  batch,
  createEffect,
  createSignal,
  For,
  JSX,
  MergeProps,
  mergeProps,
  on,
  onMount,
} from "solid-js";

type DefaultProps<T, K extends keyof T> = MergeProps<[Required<Pick<T, K>>, T]>;

function useDefaultProps<T, K extends keyof T>(
  props: T,
  defaults: Required<Pick<T, K>>
): DefaultProps<T, K> {
  // eslint-disable-next-line solid/reactivity
  return mergeProps(defaults, props);
}

type OnParameters<T1, T2> = Parameters<typeof on<T1, T2>>;

let useEffectWatch = <T1, T2>(
  a: OnParameters<T1, T2>[0],
  b: OnParameters<T1, T2>[1],
  c?: OnParameters<T1, T2>[2]
) => {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  createEffect(on(a, b, c));
};

let useEffectWatchDefer = <T1, T2>(
  a: OnParameters<T1, T2>[0],
  b: OnParameters<T1, T2>[1]
) => {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  createEffect(on(a, b, { defer: true }));
};

export function VirtualList<T>(inProps: {
  data: T[];
  rowHeight: number;
  renderRow: (row: T) => JSX.Element;
  overscanCount?: number;
  style?: JSX.CSSProperties;
  class?: string;
}) {

  let props = useDefaultProps(inProps, {
    overscanCount: 5,
  });

  let rootElement: HTMLDivElement;

  const [height, setHeight] = createSignal<number>(0);
  const [rowCount, setRowCount] = createSignal<number>(0);

  const [offset, setOffset] = createSignal<number>(0);
  const [start, setStart] = createSignal<number>(0);
  const [end, setEnd] = createSignal<number>(0);
  const [selection, setSelection] = createSignal<T[]>([]);

  //确保容器的高度始终设置为根元素的偏移高度。
  const resize = () => {
    if (height() !== rootElement.offsetHeight) {
      setHeight(rootElement.offsetHeight);
      setRowCount(getRowCount());
    }
  };

  // 获取渲染的第一个元素索引
  const getStart = () => {
    let ret = Number((offset() / props.rowHeight).toFixed(0));
    ret = Math.max(0, ret - (ret % props.overscanCount));

    //console.log("getStart:", ret);

    return ret;
  };

  // 获取展示的行数
  const getRowCount = () => {
    let ret = Number((height() / props.rowHeight).toFixed(0)) + props.overscanCount;

    //console.log("getRowCount:", ret);

    return ret;
  };

  // 获取渲染的最后一个元素索引
  const getEnd = () => {
    let ret = start() + rowCount() + 1;

    //console.log("getEnd:", ret);

    return ret;
  };

  const tick = () => {
    batch(() => {
      setOffset(rootElement.scrollTop);
      setStart(getStart());
      setEnd(getEnd());
      // 筛选渲染范围的数据
      setSelection(props.data?.slice(start(), end()));
      // console.log("tick:", {
      //   offset: offset(),
      //   start: start(),
      //   end: end(),
      //   selection: selection(),
      //   data: props.data,
      // });
    });
  };

  const handleScroll = () => {
    //console.log(offset(), rootElement.scrollTop);

    if (offset() != rootElement.scrollTop) {
      tick();
    }
  };

  onMount(() => {
    resize();
    tick();
    rootElement?.addEventListener?.("resize", resize);
  });

  // 首次不触发, 交给 onMount
  useEffectWatchDefer(
    () => props.data, // 只监听 props.data
    () => {
      tick();
    }
  );
  return (
    <div
      ref={(r) => (rootElement = r)}
      style={{ overflow: "auto", ...props.style }}
      class={props.class}
      onScroll={handleScroll}
    >
      <div
        style={{
          position: "relative",
          overflow: "hidden",
          width: "100%",
          "min-height": "100%",
          height: `${(props.data?.length ?? 0) * props.rowHeight}px`,
        }}
      >
        <div
          style={{
            position: "absolute",
            top: `${start() * props.rowHeight}px`,
            left: 0,
            height: "100%",
            width: "100%",
            overflow: "visible",
          }}
        >
          <For each={selection()}>
            {(row) => {
              return props.renderRow(row);
            }}
          </For>
        </div>
      </div>
    </div>
  );
}

使用

<VirtualList
  style={{height:'100%'}}
  data={dataList()!}
  rowHeight={30}
  renderRow={(row) => {
     return (
      <div style={{ height: "30px" }} >...</div>
     )
  }}
/>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值