契机
最近参加了兄弟公司的一个项目,该项目使用快速开发平台进行开发,可以在平台中上传 HTML,并嵌入页面。任务是开发几个简单的表格展示页面。之前的技术栈是基于 jQuery,但由于已有 6 年未使用过 jQuery,感觉不太熟悉。 在了解到自己负责的几个页面都是简单的表格展示后,决定选择solidjs和TanStack Table这个 Headless UI 库。以下是为什么选择 SolidJS 的原因:
- 语法和 React 相似:SolidJS 的语法和 React 很相似,这使得上手和迁移现有 React 组件相对容易,减少了学习成本。
- 小体积:SolidJS 体积小,这对于项目性能和加载速度是一个优势。
- 高性能:SolidJS 在底层实现上与 React 有很大的差异,它没有虚拟 DOM,而是直接修改 DOM,这使得 SolidJS 在性能方面表现优异。
- 不依赖构建工具: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 树进行更新,颇有一种力大砖飞,重剑无锋,大巧不工的美感。