Chapter 01

SolidJS 简介与工作原理

抛弃虚拟 DOM,拥抱细粒度响应式——理解 SolidJS 为何能成为最快的 UI 框架

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)构成:

追踪依赖的工作方式

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 JS1.00x(基准)1.00x1.00x1.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
SolidJS~103%(接近原生)
React 18~144%
Vue 3~125%
Svelte 5~112%
⚠️

性能不是一切:基准测试只反映极端场景的渲染性能。实际项目中网络请求、业务逻辑、开发者体验往往比这几毫秒更重要。选择框架还需综合考虑生态、团队熟悉度等因素(第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 } = propsconst value = count(不加括号)会失去响应性——你拿到的只是一个普通值,不是响应式 Signal。
正确写法:始终通过调用函数来读取 Signal:count()props.name

6. 安装:使用 Vite 模板

SolidJS 官方提供了 Vite 模板,一行命令即可创建项目。

  1. 确保已安装 Node.js 18+ 和 npm/pnpm
  2. 创建新项目(选择 solid 或 solid-ts 模板):
    # 使用 npm
    npm create vite@latest my-solid-app -- --template solid-ts
    
    # 或使用 pnpm(推荐)
    pnpm create vite my-solid-app --template solid-ts
  3. 进入目录并安装依赖:
    cd my-solid-app
    pnpm install
  4. 启动开发服务器:
    pnpm dev
    # 访问 http://localhost:5173
  5. 项目结构:
    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. 名词解释

本章小结:SolidJS 通过放弃虚拟 DOM、采用细粒度响应式 + 编译时转换,实现了接近原生 JS 的性能。它使用熟悉的 JSX 语法,但内部运作方式与 React 截然不同。核心口诀:组件只执行一次,Signal 驱动 DOM 直接更新。