1. 细粒度更新原理深讲
理解 SolidJS 的更新机制,才能写出真正高性能的代码。让我们深入了解 Signal 的内部工作方式。
订阅者图(Subscriber Graph)
SolidJS 在内存中维护一个有向图:Signal 是节点,Effect/Memo 是订阅者。每当 Signal 被读取时,当前的追踪上下文被添加到该 Signal 的订阅列表;当 Signal 被写入时,通知所有订阅者重新执行。
// 内部模型(伪代码)
class Signal {
value: T;
subscribers: Set<Effect> = new Set();
read() {
if (currentEffect) {
this.subscribers.add(currentEffect);
currentEffect.dependencies.add(this);
}
return this.value;
}
write(newValue: T) {
if (this.value !== newValue) {
this.value = newValue;
this.subscribers.forEach(effect => effect.schedule());
}
}
}
// 每次 Effect 执行时:
// 1. 清除旧的依赖关系
// 2. 设置 currentEffect = this
// 3. 执行 fn(),期间所有 Signal 读取都会注册订阅
// 4. 恢复 currentEffect = null
为什么 SolidJS 不需要 useMemo 和 useCallback
React 需要 useMemo/useCallback 是因为每次 re-render 都会重新创建所有值和函数。SolidJS 的组件只运行一次,所以:
- 函数在组件初始化时创建,以后不会重新创建——不需要 useCallback
- 派生值用 createMemo 缓存,Signal 不变就不重算——不需要 useMemo
- 引用永远稳定,不需要担心子组件因引用变化而"重渲染"
2. 避免响应式追踪陷阱
SolidJS 的追踪系统非常智能,但有几个常见陷阱需要注意:
陷阱一:解构 Signal 或 props
function BadComponent() {
const [count, setCount] = createSignal(0);
// ❌ 解构后 value 是普通数字,不是响应式的
const { value } = { value: count() }; // 解构时就求值了
return <p>{value}</p>; // value 永远是初始值 0
}
function GoodComponent() {
const [count, setCount] = createSignal(0);
// ✅ 在 JSX 中直接调用 Signal getter
return <p>{count()}</p>;
}
陷阱二:在追踪上下文外读取 Signal
function TimerComponent() {
const [count, setCount] = createSignal(0);
setInterval(() => {
// ❌ setTimeout/setInterval 回调不在追踪上下文中
// 读取 count() 不会建立订阅,但写入 setCount 仍会触发更新
console.log("current:", count());
setCount(c => c + 1); // ✅ 写入仍然有效
}, 1000);
return <p>{count()}</p>; // ✅ JSX 中的读取是响应式的
}
陷阱三:提前求值
function Component(props: { items: string[] }) {
// ❌ 在组件顶层(非响应式上下文)提前求值
const length = props.items.length; // 只读取一次
// ✅ 在 JSX 或 createMemo 中读取(建立追踪)
const length = createMemo(() => props.items.length);
return <p>共 {length()} 项</p>;
}
3. lazy() — 代码分割
lazy() 与 Suspense 配合实现组件级别的代码分割:
import { lazy } from "solid-js";
import { Suspense } from "solid-js";
// Vite 会将每个 lazy 模块打包成独立 chunk
const HeavyChart = lazy(() => import("./components/HeavyChart"));
const RichEditor = lazy(() => import("./components/RichEditor"));
function App() {
const [tab, setTab] = createSignal("chart");
return (
<div>
<button onClick={() => setTab("chart")}>图表</button>
<button onClick={() => setTab("editor")}>编辑器</button>
<Suspense fallback={<p>加载组件...</p>}>
<Show when={tab() === "chart"}>
<HeavyChart />
</Show>
<Show when={tab() === "editor"}>
<RichEditor />
</Show>
</Suspense>
</div>
);
}
4. Web Components 支持
SolidJS 可以将组件编译为原生 Web Components(Custom Elements),方便在任何框架或纯 HTML 中使用:
import { customElement } from "solid-element";
// npm install solid-element
// 将 SolidJS 组件注册为 Custom Element
customElement(
"my-counter", // 自定义元素标签名(必须含连字符)
{ initialCount: 0 }, // 默认属性
(props) => {
const [count, setCount] = createSignal(props.initialCount);
return (
<div>
<span>{count()}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
);
// 在任意 HTML 中使用
// <my-counter initial-count="5"></my-counter>
5. 测试:Vitest + @solidjs/testing-library
# 安装测试依赖
pnpm add -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
// vite.config.ts — 配置测试环境
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test-setup.ts"],
},
});
// Counter.test.tsx — 组件测试示例
import { render, screen, fireEvent } from "@solidjs/testing-library";
import { describe, it, expect } from "vitest";
import Counter from "./Counter";
describe("Counter", () => {
it("初始值为 0", () => {
render(() => <Counter />);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("点击 + 按钮后增加", () => {
render(() => <Counter />);
fireEvent.click(screen.getByText("+"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
it("支持自定义初始值", () => {
render(() => <Counter initialCount={5} />);
expect(screen.getByText("Count: 5")).toBeInTheDocument();
});
});
6. SolidJS 生态概览
| 类别 | 推荐库 | 说明 |
|---|---|---|
| 路由 | @solidjs/router | 官方路由库 |
| 全栈框架 | SolidStart | 官方元框架(类 Next.js) |
| 样式 | Tailwind CSS / vanilla-extract | 均支持 SolidJS |
| UI 组件库 | SUID(Material)/ Hope UI / Kobalte | 原生 SolidJS 组件库 |
| 表单 | @modular-forms/solid | 类型安全表单库 |
| 数据请求 | createResource(内置)/ solid-query | tanstack query 的 Solid 版 |
| 动画 | solid-transition-group / motion | CSS 过渡 / 物理动画 |
| 国际化 | @solid-primitives/i18n | 响应式 i18n |
| 测试 | vitest + @solidjs/testing-library | 官方测试方案 |
7. 与 React 的最终选型建议
选择 SolidJS 当...
- 性能是首要需求(游戏、实时数据、复杂交互)
- 团队愿意投入学习细粒度响应式思维
- 项目是新建的,没有历史包袱
- 希望最小的运行时体积(~7KB)
- 需要构建 Web Components
- 团队已有 RxJS/MobX 背景
坚持 React 当...
- 团队已有大量 React 经验和历史代码
- 需要最大的生态系统(npm 包、UI 库、招聘)
- 业务性能要求不极端(React 已经足够快)
- 需要 React Native 跨平台
- 团队规模大,统一技术栈更重要
- 使用 Next.js 全栈功能
务实建议:SolidJS 在技术上确实更先进,性能更好,心智模型更简单(一旦理解了 Signal)。但 React 的生态和社区规模仍然远大于 SolidJS。
如果你是个人项目或新团队,SolidJS 是值得尝试的优秀选择。如果是大型商业项目,先评估团队学习曲线和生态成熟度再决定。
8. SolidJS vs React 概念对照表
| React | SolidJS | 关键差异 |
|---|---|---|
| useState | createSignal | Signal 是函数调用,不触发 re-render |
| useEffect | createEffect | 自动追踪依赖,无需声明数组 |
| useMemo | createMemo | createMemo 永远正确缓存,无依赖陷阱 |
| useCallback | 普通函数 | 不需要,组件只执行一次 |
| useRef | let ref! / createSignal | let 变量即可(组件只跑一次) |
| useContext | useContext | API 类似 |
| Redux/Zustand | createStore / Context | 内置 Store 更强大 |
| array.map() | <For> | For 更高效,正确处理列表 diff |
| condition && | <Show> | Show 正确处理清理 |
| React.lazy | lazy() | API 相同 |
| Next.js | SolidStart | 类似设计,Server Functions 更简洁 |
课程总结:恭喜完成 SolidJS 全部 10 章的学习!你现在掌握了:细粒度响应式原理(Signal/Effect/Memo)、组件规范(不解构 props)、控制流组件(Show/For/Switch)、Store 状态管理、Context 依赖注入、SolidRouter 路由、异步资源管理、SolidStart 全栈开发,以及性能优化和测试。下一步,建议动手构建一个完整的 SolidStart 全栈应用来巩固所有知识!