📑 目录
-
[三、Solid.js 核心:细粒度响应式](#三、Solid.js 核心:细粒度响应式)
-
[四、状态管理:Context + createStore](#四、状态管理:Context + createStore)
-
[六、组件模型:lazy + Suspense + Switch/Match](#六、组件模型:lazy + Suspense + Switch/Match)
-
[七、API 封装:模块化 agent 设计](#七、API 封装:模块化 agent 设计)
-
[八、架构对比:Solid.js vs React vs Vue vs Svelte](#八、架构对比:Solid.js vs React vs Vue vs Svelte)
-
[九、练习 + 部署 + 进阶](#九、练习 + 部署 + 进阶)
一、引言:细粒度响应式------不渲染不更新的新范式
2021 年,Solid.js 的创始人 Ryan Carniato 发布了一个名为 solidjs/solid-realworld 的官方示例项目。这个项目本身并不大------它只是一个遵循 RealWorld 规范的 CRUD 应用,实现了文章管理、用户认证、评论、标签、分页等标准功能。但它的特别之处在于:它是 Solid.js 设计哲学的一次完整实践 ,浓缩了这个框架区别于 React 和 Vue 的核心思想------细粒度响应式。
如果你打开 Solid.js 的官方网站,会看到这样一行描述:
"Solid.js 是一个用于构建用户界面的声明式 JavaScript 库。它不使用虚拟 DOM,而是使用编译器将 JSX 转换为细粒度的 DOM 更新。"
这句话有三个关键词:声明式 、无虚拟 DOM 、细粒度更新。它们共同构成了 Solid.js 的技术宣言。
对比 React,React 的宣言是"组件化+虚拟 DOM+单向数据流";对比 Vue,Vue 的宣言是"模板语法+响应式代理+渐进式"。每个框架都有自己的"第一性原理"------Solid.js 的选择是:放弃虚拟 DOM,直接编译到真实 DOM 操作,在信号级别实现精确更新。
这就引出了本文的核心问题:如果 React 的虚拟 DOM 是"在内存中对比差异,然后批量更新真实 DOM",那么 Solid.js 的"不渲染不更新"究竟是怎么做到的?这种设计带来了哪些实际收益?
在接下来的内容中,我们将通过拆解 solidjs/solid-realworld 的源码,一步步回答这些问题。你将从项目结构、状态管理、路由设计、组件模型、API 封装五个维度,深入理解 Solid.js 的架构设计。
🤔 思考:虚拟 DOM 在 2013 年是一个革命性的想法,但十年后的今天,它仍然是"性能银弹"吗?当框架的更新粒度从"组件级"细化到"信号级",我们是否还需要虚拟 DOM 这个中间层?
二、项目全景:solidjs/solid-realworld
技术栈一览
solidjs/solid-realworld 的项目技术栈非常简洁:
| 技术 | 版本 | 用途 |
|---|---|---|
| Solid.js | 1.3.3 | 核心框架 |
| Rollup | 2.52 | 构建工具 |
| babel-preset-solid | 1.3.3 | JSX 编译 |
| marked | --- | Markdown 渲染 |
| Yarn | --- | 包管理 |
值得注意的是,这个项目使用的是 Rollup 而非 Vite。这是因为当时(2021 年)Solid.js 的 Vite 集成方案尚未成熟,而 Rollup 作为最成熟的库打包工具,配合 babel-preset-solid 实现了 JSX 到 DOM 操作的编译转换。
目录结构
src/
├── index.js # 入口:render + Provider 包装
├── App.js # 根组件:路由匹配 + 懒加载 + Suspense
├── components/ # UI 组件
│ ├── ArticleList.js # 文章列表
│ ├── Comment.js # 评论组件
│ ├── NavBar.js # 导航栏
│ ├── Pagination.js # 分页
│ └── ...
├── pages/ # 页面组件
│ ├── Article/ # 文章详情页
│ ├── Auth.js # 登录/注册页
│ ├── Editor/ # 编辑页
│ ├── Home/ # 首页
│ ├── Profile/ # 用户主页
│ └── Settings.js # 设置页
└── store/ # 状态管理 + API 层
├── index.js # Context + Provider + createStore
├── createAgent.js # fetch 封装工厂
├── createArticles.js # 文章 API
├── createAuth.js # 认证 API
├── createComments.js # 评论 API
├── createCommon.js # 公共 API
├── createProfile.js # 用户资料 API
└── createRouteHandler.js # 自定义路由
这个目录结构清晰地展示了分层思想:components/ 存放共享 UI 组件,pages/ 存放页面级组件,store/ 存放状态管理和 API 逻辑。每一层各司其职,耦合度低。
运行项目
git clone https://github.com/solidjs/solid-realworld.git
cd solid-realworld
yarn install
yarn dev
项目启动后,会连接 https://api.realworld.io/api 作为后端 API。这是一个由 RealWorld 官方维护的演示后端,提供了完整的文章、用户、评论等 RESTful 接口。
🤔 思考:早期 Solid.js 项目选择 Rollup 而非 Vite,反映了新兴框架生态的演进轨迹。在做框架选型时,了解"生态成熟度"有时比"技术先进性"更重要。今天的 Solid.js 已经全面拥抱 Vite,但 Rollup 版本的历史代码反而更清晰地展示了框架本身的特性。
三、Solid.js 核心:细粒度响应式
3.1 createSignal------最基础的信号
createSignal 是 Solid.js 中最基础的响应式原语。它返回一个包含 getter 和 setter 的二元组:
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
// 读取值(注意:count 是函数,不是变量!)
console.log(count()); // 0
// 更新值
setCount(1);
console.log(count()); // 1
// 函数式更新
setCount(prev => prev + 1);
关键区别 :与 React 的 useState 不同,createSignal 返回的 count 是函数 而非变量。这意味着每次读取 count() 时,Solid.js 可以追踪到"谁在读取这个信号",从而实现精确的依赖追踪。
3.2 createStore------深层响应式对象
对于复杂对象,createStore 提供了路径级别的响应式更新:
import { createStore } from "solid-js/store";
const [state, setState] = createStore({
user: { name: "Alice", age: 25 },
articles: []
});
// 路径更新------只更新 name,不影响 age
setState("user", "name", "Bob");
// 批量更新
setState({
articles: [{ title: "Hello" }]
});
createStore 的核心能力是路径更新 。当你调用 setState("user", "name", "Bob") 时,只有 user.name 这个路径上的依赖会被通知更新,其他部分完全不受影响。
3.3 createResource------异步数据管理
在 RealWorld 项目中,createResource 是处理异步数据的关键工具:
import { createResource } from "solid-js";
const [userId, setUserId] = createSignal(1);
const [user] = createResource(userId, fetchUser);
// 当 userId 变化时,createResource 自动重新请求
// user() 返回 { loading: true, data: undefined } 或 { loading: false, data: {...} }
createResource 的第一个参数是"信号源"------当这个信号变化时,资源会自动重新获取。这与 React 的 useEffect + useState 组合不同,不需要手动管理依赖数组。
3.4 createComputed------自动追踪的派生值
import { createSignal, createComputed } from "solid-js";
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Doe");
const fullName = createComputed(() => `${firstName()} ${lastName()}`);
// 当 firstName 或 lastName 变化时,fullName 自动重新计算
细粒度更新 vs 虚拟 DOM 整树更新
下面这张图清晰地展示了 Solid.js 和 React 的更新机制差异:


解读:Solid.js 的更新路径是"信号 → DOM 节点",没有中间环节。React 的更新路径是"状态 → 组件执行 → 虚拟 DOM 创建 → Diff → 补丁 → 真实 DOM",多出了三个中间步骤。
🤔 思考:细粒度响应式是否一定比虚拟 DOM 性能更好?答案是否定的。在简单场景下(如一个按钮点击),两种方案几乎没有差别。但在复杂场景下(如大型数据表格中仅更新一行数据),细粒度响应式的优势会非常明显------它避免了整棵虚拟 DOM 树的 Diff 计算。然而,细粒度响应式的代价是更复杂的内存管理(追踪成千上万个依赖关系)和更高的心智负担。
四、状态管理:Context + createStore
4.1 Provider 设计
RealWorld 项目的状态管理核心在 store/index.js 中:
import { createContext, useContext } from "solid-js";
import { createStore } from "solid-js/store";
import createAgent from "./createAgent";
export function Provider(props) {
const router = createRouteHandler(),
[state, setState] = createStore({
articles: [],
comments: [],
tags: [],
profile: null,
currentUser: null,
page: 0,
totalPagesCount: 0,
token: localStorage.getItem("jwt")
}),
actions = {},
store = [state, actions],
agent = createAgent(store);
// 创建各领域的 action 模块
articles = createArticles(agent, actions, state, setState);
auth = createAuth(agent, actions, setState);
comments = createComments(agent, actions, state, setState);
profile = createProfile(agent, actions, state, setState);
return (
<RouterContext.Provider value={router}>
<StoreContext.Provider value={store}>
{props.children}
</StoreContext.Provider>
</RouterContext.Provider>
);
}
export function useStore() { return useContext(StoreContext); }
export function useRouter() { return useContext(RouterContext); }
这个设计有几个关键点:
-
单一 Store + 模块化 Action :整个应用只有一个
createStore,但 action 函数被拆分到多个模块中。这类似于 Vuex 的单一状态树 + 模块化设计。 -
Context 传递 Store 引用 :
useStore()返回的[state, actions]是同一个引用------state 的更新不会导致 Context 重新传递,而是通过createStore的细粒度响应式机制通知到具体的使用者。 -
Agent 注入 :每个 action 模块都接收
agent实例,这是 API 调用层,实现了状态管理和网络请求的解耦。
4.2 细粒度更新 vs 全局状态刷新
// 在组件中使用
const [store, actions] = useStore();
// 只有 articles 变化时,这个组件才会更新
// 即使 store 中的 token 或 currentUser 变化,也不会影响这里
console.log(store.articles);
关键差异 :在 React 中,useContext 的变化会导致所有消费该 Context 的组件重新渲染。但在 Solid.js 中,useStore 返回的是 createStore 的代理对象------组件只会在它实际读取的字段变化时才会更新。
🤔 思考:React 社区的 Context + useReducer 模式在大型应用中面临着"不必要的重渲染"问题,开发者不得不使用 useMemo、React.memo、useCallback 等手段来优化。而 Solid.js 的 createStore 从设计层面解决了这个问题------组件只订阅它实际读取的字段,精度达到了属性级别。这不是"优化",而是"默认正确"。
五、路由:自定义极简路由
5.1 createRouteHandler 实现
RealWorld 项目没有使用第三方路由库,而是自己实现了一个基于 location.hash 的极简路由:
// store/createRouteHandler.js
export default function createRouteHandler() {
const [location, setLocation] = createSignal(
window.location.hash.replace("#", "")
);
// 监听 hash 变化
window.addEventListener("hashchange", () => {
setLocation(window.location.hash.replace("#", ""));
});
return {
match(name, pattern) {
const match = location().match(pattern);
return match ? { name, params: match.slice(1) } : null;
},
getParams() {
return {};
}
};
}
在 App.js 中使用:
import { Switch, Match } from "solid-js";
const { match } = useRouter();
<Switch>
<Match when={match("editor", /^editor\/?(.*)/)}>
<Editor {...match("editor", /^editor\/?(.*)/).params} />
</Match>
<Match when={match("settings", /^settings/)}>
<Settings />
</Match>
<Match when={match("article", /^article\/(.*)/)}>
<Article {...match("article", /^article\/(.*)/).params} />
</Match>
<Match when={match("profile", /^@([^/]*)\/?(favorites)?/)}>
<Profile {...match("profile", /^@([^/]*)\/?(favorites)?/).params} />
</Match>
<Match when={match("", /^#?$/)}>
<Home />
</Match>
</Switch>
5.2 设计亮点
-
零依赖:整个路由实现不到 30 行代码,不依赖任何第三方库。
-
Hash 路由 :基于
location.hash,简单可靠,无需服务端配置。 -
正则匹配:使用正则表达式进行路由匹配和参数提取,支持动态路由。
🤔 思考 :在 SPA 应用中,路由是一个通用需求,绝大多数项目都会选择 React Router 或 Vue Router。但 RealWorld 项目选择手写路由,说明了一个道理:框架的核心能力足够强时,可以简化周边工具的需求。Solid.js 的细粒度响应式让路由状态管理变得极其简单,不需要复杂的路由库来管理"路由状态"。
六、组件模型:lazy + Suspense + Switch/Match
6.1 App.js 根组件
import { lazy, createSignal, createComputed, Switch, Match, Show, Suspense } from "solid-js";
import { useStore, useRouter } from "./store";
import NavBar from "./components/NavBar";
// 懒加载页面
const Settings = lazy(() => import("./pages/Settings"));
const Auth = lazy(() => import("./pages/Auth"));
export default () => {
const [store, { pullUser }] = useStore(),
[appLoaded, setAppLoaded] = createSignal(false),
{ match, getParams } = useRouter();
// 初始化:检查 token
if (!store.token) {
setAppLoaded(true);
} else {
pullUser();
createComputed(() => store.currentUser && setAppLoaded(true));
}
return (
<>
<NavBar />
<Show when={appLoaded()}>
<Suspense fallback={<div class="container">Loading...</div>}>
<Switch>
<Match when={match("editor", /^editor\/?(.*)/)}>
<Editor {...getParams()} />
</Match>
<Match when={match("settings", /^settings/)}>
<Settings />
</Match>
{/* 其他路由匹配 */}
</Switch>
</Suspense>
</Show>
</>
);
};
6.2 关键特性
-
组件函数只执行一次:Solid.js 的组件函数只在初始化时执行一次,不像 React 那样每次状态更新都重新执行。这意味着 Solid.js 中没有 React 的"闭包陷阱"问题。
-
lazy + Suspense :
lazy实现了代码分割,Suspense配合createResource实现异步数据的加载状态管理。这与 React 的React.lazy + Suspense非常相似,但底层机制不同------Solid.js 的 Suspense 是细粒度的,可以等待多个异步资源。 -
Switch/Match :Solid.js 内置的条件渲染组件,等价于 React 中手动实现的
switch-case模式。
6.3 与 React Suspense 对比
| 特性 | Solid.js Suspense | React Suspense |
|---|---|---|
| 数据源 | createResource | 第三方库(如 Relay、SWR) |
| 粒度 | 细粒度,可等待多个资源 | 组件级 |
| 回退 | fallback 属性 | fallback 属性 |
| 错误处理 | ErrorBoundary | ErrorBoundary |
🤔 思考 :Solid.js 的组件模型揭示了一个重要差异:"组件函数只执行一次"意味着 Solid.js 没有"重新渲染"的概念。组件就像是一个"初始化脚本",在首次运行时完成了所有 DOM 节点的创建和信号绑定,之后只有信号的变化会驱动 DOM 更新,组件本身不会重新执行。这就从根本上消除了 React 中 useEffect 依赖数组、useCallback 记忆化等复杂性。
七、API 封装:模块化 agent 设计
7.1 createAgent 工厂函数
// store/createAgent.js
export default function createAgent(store) {
const [state] = store;
async function request(method, url, body) {
const headers = { "Content-Type": "application/json" };
if (state.token) {
headers["Authorization"] = `Token ${state.token}`;
}
const response = await fetch(`https://api.realworld.io/api${url}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
});
const result = await response.json();
if (!response.ok) throw result;
return result;
}
return {
Auth: {
current: () => request("GET", "/user"),
login: (email, password) => request("POST", "/users/login", { user: { email, password } }),
register: (username, email, password) => request("POST", "/users", { user: { username, email, password } })
},
Articles: {
all: (page) => request("GET", `/articles?offset=${page * 20}&limit=20`),
bySlug: (slug) => request("GET", `/articles/${slug}`),
},
Comments: {
forArticle: (slug) => request("GET", `/articles/${slug}/comments`),
},
Profile: {
get: (username) => request("GET", `/profiles/${username}`)
}
};
}
7.2 createAuth 模块
// store/createAuth.js
import { createSignal, createResource, batch } from "solid-js";
export default function createAuth(agent, actions, setState) {
const [loggedIn, setLoggedIn] = createSignal(false),
[currentUser, { mutate }] = createResource(loggedIn, agent.Auth.current);
Object.assign(actions, {
pullUser: () => setLoggedIn(true),
async login(email, password) {
const { user, errors } = await agent.Auth.login(email, password);
if (errors) throw errors;
setState({ token: user.token });
setLoggedIn(true);
},
logout() {
batch(() => {
setState({ token: undefined });
mutate(undefined);
});
}
});
return currentUser;
}
7.3 设计亮点
-
factory 模式 :
createAgent(store)接收 store 实例,从中读取 token 用于 JWT 认证。这是一种依赖注入的实现方式。 -
模块化组织 :
agent.Auth、agent.Articles等按领域划分,每个模块返回 Promise,与createResource天然配合。 -
batch 批量更新 :
logout()中使用batch()包裹多个状态更新,确保它们在一次更新中完成。
🤔 思考 :React 生态中,Next.js 的 API 封装通常通过
fetch的二次封装 + 环境变量实现。Solid.js 的 agent 模式与之类似,但更简洁------不需要复杂的拦截器机制,因为 Solid.js 的响应式系统天然支持从 store 中读取 token。框架的能力越强,封装工具就越轻量。
八、架构对比:Solid.js vs React vs Vue vs Svelte
8.1 核心维度对比
| 维度 | Solid.js | React | Vue 3 | Svelte |
|---|---|---|---|---|
| 响应式模型 | 信号(编译时+运行时) | 虚拟 DOM Diff | Proxy 代理 | 编译时 |
| 更新粒度 | 细粒度(信号级) | 组件级 | 细粒度(响应式对象) | 细粒度(编译后) |
| 虚拟 DOM | 无 | 有 | 有(或虚拟 DOM + 编译器) | 无 |
| 组件执行 | 一次 | 每次更新 | 一次(setup) | 一次 |
| JSX 支持 | 原生 | 原生 | 需要插件 | 不支持 |
| 学习曲线 | 中(信号概念新) | 中(Hooks 规则) | 低 | 低 |
| 包体积 | ~7KB | ~42KB | ~33KB | ~5KB(编译后) |
| SSR 支持 | 需要 Solid Start | Next.js | Nuxt | SvelteKit |
8.2 响应式模型对比
React:每次状态变化触发整个组件重新执行,生成新的虚拟 DOM 树,然后与旧树进行 Diff,计算出最小 DOM 操作。
Vue 3:通过 Proxy 拦截对象的读写操作,在 getter 时收集依赖,在 setter 时通知更新。组件级别使用虚拟 DOM 进行渲染优化。
Solid.js:通过编译器将 JSX 中的响应式表达式编译为独立的 DOM 更新操作。每个信号独立追踪依赖,更新时直接操作 DOM。
Svelte:通过编译器将声明式代码转换为高效的命令式 DOM 操作代码。编译时分析变量赋值,生成对应的 DOM 更新代码。
8.3 开发者体验对比
// React - 每次 count 变化,整个组件重新执行
function Counter() {
const [count, setCount] = useState(0);
console.log("组件渲染"); // 每次 count 变化都会执行
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Solid.js - 组件只执行一次,只有 count() 所在的 DOM 节点更新
function Counter() {
const [count, setCount] = createSignal(0);
console.log("组件初始化"); // 只执行一次
return <button onClick={() => setCount(c => c + 1)}>{count()}</button>;
}
<!-- Vue 3 - 模板编译,setup 只执行一次 -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
🤔 思考:Solid.js 会成为"下一个 React"吗?从技术层面看,Solid.js 在很多方面确实优于 React------更细粒度的更新、更少的样板代码、更好的性能。但框架的成功不仅取决于技术,还取决于生态、社区、企业支持等因素。Solid.js 目前最大的挑战是生态规模------它的第三方库、工具链、学习资源都远不如 React 丰富。但作为一个"更优的响应式方案",Solid.js 正在稳步增长,尤其适合性能敏感型应用。
九、练习 + 部署 + 进阶
9.1 动手练习
练习 1:暗色模式切换
在
Settings.js中添加一个"暗色模式"开关,使用createSignal管理主题状态,通过 CSS 类切换实现主题切换。重点练习createSignal的使用和状态持久化(localStorage)。
练习 2:文章搜索功能
在
Home.js中添加搜索框,输入关键词后调用agent.Articles.all()的搜索参数,实现文章过滤。重点练习createSignal和createResource的联动。
练习 3:阅读进度条
在
Article.js中添加一个固定在页面顶部的阅读进度条,根据文章内容的滚动位置动态更新进度。重点练习createEffect和 DOM 事件绑定。
9.2 Vercel 部署
# 安装 Vercel CLI
npm i -g vercel
# 在项目根目录创建 vercel.json
echo '{"builds":[{"src":"package.json","use":"@vercel/static-build"}],"rewrites":[{"source":"/(.*)","destination":"/index.html"}]}' > vercel.json
# 部署
vercel --prod
9.3 进阶路径
-
Solid Start:Solid.js 的元框架,类似 Next.js 之于 React,提供文件路由、SSR、SSG、API 路由等功能。
-
Solid Native:基于 Solid.js 的跨平台移动开发方案。
-
生态工具 :探索
solid-js/router(官方路由)、solid-js/universal(自定义渲染器)、solid-testing-library(测试工具)等。
🤔 思考:从 RealWorld 到生产项目,还需要考虑哪些因素?状态管理方案(是否需要更复杂的方案)、类型安全(TypeScript 集成)、测试策略(单元测试 + E2E)、国际化(i18n 方案)、性能监控等。RealWorld 项目是一个优秀的起点,但它不是生产级模板。
十、结语
回到文章开头的问题:Solid.js 的"不渲染不更新"究竟是怎么做到的?
通过拆解 solidjs/solid-realworld 的源码,我们看到了答案:
-
状态层面 :
createSignal和createStore提供了信号级别的精确追踪,每个状态变化都知道"谁在依赖它"。 -
组件层面:组件函数只执行一次,没有"重新渲染"的概念,避免了 React 中"父组件渲染 → 子组件渲染"的级联反应。
-
更新层面:编译器将 JSX 编译为直接的 DOM 操作,没有虚拟 DOM 中间层,信号变化直接映射到 DOM 更新。
Solid.js 的设计哲学可以用一句话概括:"精确知道需要更新什么,然后只更新那一个"。这听起来简单,但实现起来需要编译器、运行时和响应式系统的紧密配合。
前端框架的演进经历了从"直接操作 DOM"(jQuery)到"声明式描述 UI"(React/Vue)再到"精确响应式更新"(Solid.js/Svelte)的过程。Solid.js 不是对 React 的简单改进,而是一种范式转换------它用不同的方式回答了"如何高效更新 UI"这个问题。
对于前端开发者来说,学习 Solid.js 的意义不仅在于掌握一个新框架,更在于理解"响应式"的另一种可能性。它让你意识到:虚拟 DOM 不是 UI 框架的必然选择,而只是一个历史阶段的产物。