探索SolidJS和TanStack Table开发之旅

契机

最近参加了兄弟公司的一个项目,该项目使用快速开发平台进行开发,可以在平台中上传 HTML,并嵌入页面。任务是开发几个简单的表格展示页面。之前的技术栈是基于 jQuery,但由于已有 6 年未使用过 jQuery,感觉不太熟悉。 在了解到自己负责的几个页面都是简单的表格展示后,决定选择solidjsTanStack Table这个 Headless UI 库。以下是为什么选择 SolidJS 的原因:

  1. 语法和 React 相似:SolidJS 的语法和 React 很相似,这使得上手和迁移现有 React 组件相对容易,减少了学习成本。
  2. 小体积:SolidJS 体积小,这对于项目性能和加载速度是一个优势。
  3. 高性能:SolidJS 在底层实现上与 React 有很大的差异,它没有虚拟 DOM,而是直接修改 DOM,这使得 SolidJS 在性能方面表现优异。
  4. 不依赖构建工具:SolidJS 不需要构建工具就可以使用,可以简化了项目配置和构建的复杂性。

Solid语法虽然和React相似,但是实际上底层原理差异很大。 SolidJS 和 React 的不同点主要体现在以下几个方面:

API差异

javascript 复制代码
import { createSignal, type Component, createEffect } from "solid-js";

const App: Component = () => {
  const [count, setCount] = createSignal(0);
  createEffect(() => {
    console.log(count());
  });
  console.log('--App', count())
  return (
    <div style={{ "z-index": count() }}>
      <button onClick={() =>setCount(count => count + 1)}>{count()}</button>
    </div>
  );
};

const root = document.getElementById('root');
render(() => <App />, root!);

SolidJS 使用 createSignal 来声明状态,不同于 React 的 useState。而且 SolidJS 的 createSignal 在循环、条件或嵌套函数中使用没有限制,可以在任意地方调用。createSignal 返回的数组的第一个值是一个函数,而不是状态本身。 createEffect作用和useEffect类似,但是createEffect会自动跟踪相关依赖,不需要指定依赖关系。

更新差异

React 组件状态变化后,所有相关组件都会走一遍更新流程,因为需要重新构建虚拟 DOM(Fiber 树)。而 SolidJS 没有虚拟 DOM,它直接修改 DOM,因此性能较好。SolidJS 是原子性更新的,组件函数本身只会执行一次。 举个例子,在上面的demo中,点击按钮后,只有第6行会发生更新, 重新设置z-index以及button的子节点 Solid是怎么做到的?从源代码中看不出秘密,这就需要讲到Solid的编译了

编译差异

SolidJS 在编译时需要为响应式添加一些函数,因此编译过程中相对复杂一些,而 React 编译相对简单,只需将 JSX 转换为 JavaScript。

javascript 复制代码
const _tmpl$ = /*#__PURE__*/template(`<div><button></button></div>`);
const App = () => {
  const [count, setCount] = createSignal(0);
  createEffect(() => {
    console.log(count());
  });
  return (() => {
    const _el$ = _tmpl$.cloneNode(true),
      _el$2 = _el$.firstChild;
    // 添加点击事件
    _el$2.$$click = () => setCount(count => count + 1);
    // 插入节点, insert可以理解为插入节点,并且调用createEffect,动态更新节点
    insert(_el$2, count);
    // 动态更新style z-index,createRenderEffect和createEffect调用的是同一个函数
    createRenderEffect(() => _el$.style.setProperty("z-index", count()));
    return _el$;
  })();
};
delegateEvents(["click"]);

const root = document.getElementById('root');
render(() => createComponent(App, {}), root);

响应原理

javascript 复制代码
const observers: Function[] = [];

function createEffect(effect: Function) {
  const execute = () => {
    // 保存在observers中
    observers.push(execute);
    try {
      // 执行effect函数
      effect();
    } finally {
      // 释放
      observers.pop();
    }
  };

  // 副作用函数立即执行
  execute();
}

interface StateData<T> {
  value: T;
  subscribers: Set<Function>;
  comparator: any;
}

export type Accessor<T> = () => T;

export type Setter<T> = (undefined extends T ? () => undefined : {}) &
  (<U extends T>(value: (prev: T) => U) => U) &
  (<U extends T>(value: Exclude<U, Function>) => U) &
  (<U extends T>(value: Exclude<U, Function> | ((prev: T) => U)) => U);

export type Signal<T> = [get: Accessor<T>, set: Setter<T>];
export interface MemoOptions<T> {
  equals?: false | ((prev: T, next: T) => boolean);
}
export interface SignalOptions<T> extends MemoOptions<T> {
  internal?: boolean;
}

const signalOptions: { equals: false } = {
  equals: false,
};

export function createSignal<T>(): Signal<T | undefined>;
export function createSignal<T>(
  value: T,
  options?: SignalOptions<T>
): Signal<T>;
export function createSignal<T>(
  value?: T,
  options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
  // 初始化options
  options = options ? Object.assign({}, signalOptions, options) : signalOptions;
  // 创建内部signal
  const s: StateData<T | undefined> = {
    value,
    // 保存订阅者
    subscribers: new Set(),
    comparator: options.equals || undefined,
  };

  // 定义setter
  const setter: Setter<T | undefined> = (value?: any) => {
    if (typeof value === "function") {
      value = value(s.value);
    }
    return writeSignal(s, value);
  };
  return [readSignal.bind(s), setter];
}

// 返回当前内部signal的value
function readSignal(this: StateData<any>) {
  const curr = observers[observers.length - 1];
  curr && this.subscribers.add(curr);
  return this.value;
}

// 更新内部的value,然后返回value
function writeSignal<T>(node: StateData<T>, value: T) {
  if (!node.comparator) {
    node.value = value;
  }
  // 每次写入时执行对应的订阅者
  node.subscribers.forEach((subscriber) => subscriber());
  return value;
}

const [count, setCount] = createSignal(1);
const increment = () => setCount((count) => count + 1);
createEffect(() => console.log("count : ", count()));
increment();

精妙之处在于,执行createEffect函数之后,把effect函数push进入响应数组中,然后执行effect,

TanStack Table 表格组件

TanStack Table是一个Headless UI Table组件库,它和ag-grid是两个极端。很灵活,但是也代表着很多功能都需要自己实现。 遇到的一个问题是实现rowSpan,行合并比较麻烦。下面是实现方案:

javascript 复制代码
  const table = createMemo(() => {
    const table = createSolidTable({
      ...
    });
    // 这里做行合并计算
    const groups = table.getHeaderGroups();
    groups.forEach((group) => {
      group.headers.forEach((header) => {
        header.isPlaceholder = false;
        const fristHeader = table.getFlatHeaders().find((_header) => {
          return _header.column.id === header.column.id;
        });
        if (fristHeader) {
          fristHeader.rowSpan++;
          if (fristHeader.id !== header.id) {
            header.isPlaceholder = true;
          } else {
            header.isPlaceholder = false;
          }
        }
      });
    });
    return table;
  });

<For each={table().getHeaderGroups()}>
  {(headerGroup) => (
    <tr>
      <For each={headerGroup.headers}>
        {(header) => {
          if (header.isPlaceholder) {
            return null;
          }
          return (
            <th
              ref={ref}
              colSpan={header.colSpan}
              rowSpan={header.rowSpan || undefined}
              class={cls("ant-table-cell", {
                "ant-table-cell-fix-left": fixLeft,
                "ant-table-cell-fix-left-last": fixLeftLast,
              })}
            >
              {flexRender(
                header.column.columnDef.header,
                header.getContext()
              )}
            </th>
          );
        }}
      </For>
    </tr>
  )}
</For>

TanStack Table源码写的简单明了,架构清晰。大部分的功能都是通过feature的方式实现,其实就是plugin插件方式拆分功能,比如选择、分组、展开、分页等等都是和核心功能分开的。唯一的缺陷是不支持自定义feature。

总结

在使用 SolidJS 开发过程中,重新思考了 React 的简洁思想,即重新渲染整个 Fiber 树进行更新,颇有一种力大砖飞,重剑无锋,大巧不工的美感。

相关推荐
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy6 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js