1. 虚拟 DOM 的问题
React、Vue 等主流框架都采用了虚拟 DOM(Virtual DOM)技术。虚拟 DOM 的思路是:每次状态变化时,在内存中构建一棵新的虚拟树,然后与旧树做 diff(差异比较),最后只把差异应用到真实 DOM。
这个设计在 2013 年很有革命性,但随着 Web 应用越来越复杂,它的缺点也逐渐暴露:
虚拟 DOM 的代价
- 每次更新都要重新执行整个组件函数
- 即使只改了一个数字,也要 diff 整棵子树
- diff 算法本身有时间复杂度开销
- 内存中常驻大量虚拟节点对象
- Fiber 调度等复杂机制增加运行时体积
SolidJS 的方案
- 组件函数只执行一次(编译时)
- Signal 变化时直接更新对应 DOM 节点
- 无 diff,无调度,无运行时开销
- 编译器将 JSX 转为精确的 DOM 操作
- 运行时极小(约 7KB gzipped)
核心洞见:虚拟 DOM 不是目的,而是手段。真正的目的是"状态变化时高效更新 DOM"。SolidJS 证明了不需要虚拟 DOM 也能(甚至更好地)实现这个目的。
2. SolidJS 工作原理:编译时转换
SolidJS 使用 JSX,但它对 JSX 的处理方式与 React 完全不同。React 把 JSX 转为 React.createElement() 调用(生成虚拟节点),而 SolidJS 把 JSX 编译为直接的 DOM 操作。
你写的代码(JSX)
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
编译器生成的代码(简化)
// SolidJS 编译器生成的实际代码(简化版)
function Counter() {
const [count, setCount] = createSignal(0);
// 1. 一次性创建真实 DOM 结构(模板克隆)
const _tmpl = template(`<div><p>Count: </p><button>+1</button></div>`);
const _el = _tmpl.cloneNode(true);
const _p = _el.firstChild;
const _text = _p.firstChild; // "Count: " 文本节点之后的占位
// 2. 建立响应式连接:count 变化时只更新这个文本节点
createEffect(() => {
_text.data = count(); // 直接操作 DOM,无 diff
});
// 3. 绑定事件(只执行一次)
_el.querySelector('button').onclick = () => setCount(c => c + 1);
return _el;
}
// 组件函数只执行一次!之后 count 变化只触发 createEffect 内的一行代码
关键差异:React 每次 setCount 都会重新执行整个 Counter 函数(re-render)。SolidJS 的 Counter 函数只执行一次,之后 count 变化只触发那一行 _text.data = count()。这就是"细粒度"的含义。
3. 细粒度响应式系统
SolidJS 的响应式系统由三个核心原语(primitives)构成:
-
Signal(信号)最基本的响应式数据单元。
createSignal(value)返回一个读函数和一个写函数。读取 Signal 时自动建立依赖追踪,写入时通知所有订阅者。 -
Effect(副作用)
createEffect(fn)立即执行 fn,并追踪 fn 执行期间读取的所有 Signal。当这些 Signal 变化时,Effect 自动重新执行。 -
Memo(派生状态)
createMemo(fn)创建一个缓存的计算值。只有当依赖的 Signal 变化时才重新计算,其他任何时候读取都返回缓存值。
追踪依赖的工作方式
SolidJS 使用一个全局的"当前追踪上下文"变量。当 Effect 或 Memo 执行时,SolidJS 将自身设为当前上下文。期间任何 Signal 的读取(调用读函数)都会把当前上下文注册为自己的订阅者。这是自动依赖追踪,不需要手动声明依赖数组。
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// Effect 执行时读取了 a() 和 b(),所以同时订阅两者
createEffect(() => {
console.log(`a=${a()}, b=${b()}`); // 输出: a=1, b=2
});
setA(10); // Effect 重新执行: a=10, b=2
setB(20); // Effect 重新执行: a=10, b=20
4. 性能对比
以下是 js-framework-benchmark(一个被广泛引用的前端框架性能基准)的典型结果。分数越低越好(代表耗时越短)。
| 框架 | 创建 1000 行 | 更新每第 10 行 | 选中一行 | 交换 2 行 | 内存 |
|---|---|---|---|---|---|
| SolidJS | ~1.03x | ~1.05x | ~1.04x | ~1.02x | 最低 |
| Vanilla JS | 1.00x(基准) | 1.00x | 1.00x | 1.00x | 基准 |
| Svelte 5 | ~1.12x | ~1.08x | ~1.10x | ~1.09x | 低 |
| Vue 3 | ~1.25x | ~1.20x | ~1.18x | ~1.22x | 中 |
| React 18 | ~1.44x | ~1.38x | ~1.35x | ~1.42x | 较高 |
| Angular 17 | ~1.52x | ~1.45x | ~1.40x | ~1.48x | 高 |
性能不是一切:基准测试只反映极端场景的渲染性能。实际项目中网络请求、业务逻辑、开发者体验往往比这几毫秒更重要。选择框架还需综合考虑生态、团队熟悉度等因素(第10章会详细讨论)。
5. 第一个 SolidJS 示例:createSignal
让我们写第一个有意义的 SolidJS 组件——一个带加减按钮的计数器,感受 Signal 的工作方式。
import { createSignal } from "solid-js";
function Counter() {
// createSignal(初始值) 返回 [读函数, 写函数]
// 注意:读取值需要调用函数 count(),不是直接 count
const [count, setCount] = createSignal(0);
// 派生状态:count 变化时自动重新计算
const doubled = () => count() * 2;
return (
<div class="counter">
<h2>Count: {count()}</h2>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c - 1)}>-</button>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
export default Counter;
最常见的 SolidJS 错误:不要解构 Signal!
const { x } = props 或 const value = count(不加括号)会失去响应性——你拿到的只是一个普通值,不是响应式 Signal。
正确写法:始终通过调用函数来读取 Signal:count()、props.name。
6. 安装:使用 Vite 模板
SolidJS 官方提供了 Vite 模板,一行命令即可创建项目。
- 确保已安装 Node.js 18+ 和 npm/pnpm
-
创建新项目(选择 solid 或 solid-ts 模板):
# 使用 npm npm create vite@latest my-solid-app -- --template solid-ts # 或使用 pnpm(推荐) pnpm create vite my-solid-app --template solid-ts -
进入目录并安装依赖:
cd my-solid-app pnpm install -
启动开发服务器:
pnpm dev # 访问 http://localhost:5173 -
项目结构:
my-solid-app/ ├── src/ │ ├── App.tsx # 根组件 │ ├── index.tsx # 入口文件(render 挂载) │ └── app.css ├── index.html ├── tsconfig.json └── vite.config.ts # 包含 solid Vite 插件
vite.config.ts 配置
import { defineConfig } from 'vite';
import solid from 'vite-plugin-solid'; // 关键:SolidJS 的 Vite 插件
export default defineConfig({
plugins: [solid()],
});
入口文件 index.tsx
import { render } from 'solid-js/web';
import App from './App';
// render 函数:第一个参数是组件工厂函数,第二个是挂载点
render(() => <App />, document.getElementById('root')!);
7. 名词解释
-
响应式(Reactivity)当数据变化时,依赖该数据的计算或 UI 自动更新的能力。SolidJS 的响应式是"推送模型":Signal 变化主动通知订阅者,而不是订阅者轮询检查。
-
Signal(信号)SolidJS 的最基本响应式单元。类似 React 的 useState,但 Signal 是真正的响应式容器——任何读取它的地方都会自动建立订阅关系,无需手动声明依赖。
-
追踪依赖(Dependency Tracking)SolidJS 在运行 Effect/Memo 时自动记录访问了哪些 Signal,形成依赖图。这个过程完全透明,开发者无需手动写依赖数组(不同于 React 的 useEffect([deps]))。
-
细粒度(Fine-grained)更新粒度极小——只有真正依赖某个 Signal 的 DOM 节点才会被更新,而不是整个组件树。例如 `<p>{count()}</p>` 只会更新 p 标签内的文本节点,不会重新渲染整个组件。
-
编译时转换(Compile-time Transform)SolidJS 在构建阶段(而非运行时)将 JSX 转为高效的 DOM 操作代码。这意味着运行时不需要任何解析 JSX 的逻辑,运行时体积极小。
本章小结:SolidJS 通过放弃虚拟 DOM、采用细粒度响应式 + 编译时转换,实现了接近原生 JS 的性能。它使用熟悉的 JSX 语法,但内部运作方式与 React 截然不同。核心口诀:组件只执行一次,Signal 驱动 DOM 直接更新。