SolidJS

SolidJS 是一个高性能轻量前端框架,其以 JSX 为核心,为 React 做了许多减法,少了很多难记的 Hooks,所以上手非常快。不过本文假设读者已经熟悉了 JSX 与 React 基础。

SolidJS 的哲学是 small and simple,优势就是打包体积小和性能好,不过很多地方需要自己手操低级逻辑。如果你深入理解了 SolidJS(因为内容不太多所以不难),也不容易被各种隐含逻辑坑到。

SolidJS 的一个缺点是生态比较差,包数量和可用性都要比 React 差上许多 (ref)。

props 与响应性

在 React 中,有一个核心理念:UI = f(state)。这意味着整个组件的输出(UI)被看作是其当前 state 和 props 的一个函数。当 state 或 props 改变时,React 的做法不是去找到是哪个具体的值变了,而是重新执行整个组件函数,生成一个新的 Virtual DOM,然后通过比对新旧 VDOM,最后只把差异部分更新到真实的 DOM 上。

然而,SolidJS 的组件函数本身永远只运行一次。要让子组件随父组件的 signal 改变而重新渲染,需要传入 Accessor<T> 保持响应性。但是在任何一层子组件使用 const { ... } = props 解构,都会丢失 props 的响应性,导致子组件无法正确重新渲染。

要在保证响应性的同时解构 props,需要使用 SolidJS 提供的 splitProps。举个例子:

tsx
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
  children?: JSX.Element;
  variant?: "primary" | "secondary" | "square" | "operator";
}
const Button: Component<ButtonProps> = (props) => {
  // 'local' 对象会包含这些 props,并且它们仍然是响应式的
  // 'rest' 对象会包含所有剩余的 props
  const [local, rest] = splitProps(props, ["children", "variant"]);
  ...
}

如果你在调试 SolidJS 项目时发现子组件没有正确渲染,请优先检查响应性。

Hooks

createSignal

就是 useState,最基本的响应式组成部分。

createMemo

跟其他语言一样,createMemo 是一个 input -> output 的缓存。组件监听 createMemo 的 output,如果 input 变了但是 output 不变,就不会触发重新渲染,节省性能。

createStore

创建一个深层响应式对象,实现细粒度响应性(修改深层属性只会影响依赖了这个属性的节点)。一般用来存一些生命周期比较长、贯穿多个页面的东西,例如全局配置。

createContext

用来跨组件传输数据,不需要通过 props。只要在 provider 内都能拿到 context。

createResource

一个 async 执行器的简单包装,可以跟踪执行状态。当然直接用 signal + 回调也是没有任何问题的。好像现在更多还是直接 signal,并没有太麻烦,还能减少学习成本。就是代码乱一些。

createResource 可以跟 Suspence 组件配合使用。其实就是包装了一下 res.loading 的判断而已。

onMount

因为每个组件函数只会执行一次,你可以直接将相关逻辑放在组件里;当然 SolidJS 也提供了 onMount hook 用于分离逻辑。

杂

components

SolidJS 也提供了一些内置组件,让写 Vue 的人倍感熟悉。

functions

SolidJS 还提供一些实用函数。

splitProps & mergeProps

ts/js 辅助函数。splitProps 从对象里分离出某些字段,mergeProps 用于合并对象字段(从左到右合并,右侧优先)。

splitProps / mergeProps 存在的意义在于保持子对象的响应性,在开头说过了。

reconcile

性能拯救者,相当魔法的一个函数。

reconcile 解决了细粒度的响应式更新问题,把整个对象销毁 + 创建变成每个子对象与深层对象的比对与替换。基本上只要你用了 createStore,然后有任何后端响应/前端重新构造对象,需要整体替换这个 store 的场景,直接套一个 reconcile 即可。

reconcile 如果用于数组细粒度更新,则最好指定 key,否则可能退化回原先的全量构造。

深入 SolidJS

React 的 FC (JSX 概念,FunctionComponent) 既是 Setup,也是 Render。状态每次改变,整个函数都会重新执行一遍,生成新的 Virtual DOM,然后进行 Diff。

SolidJS 的 FC 仅仅是 Setup 函数,且只执行一次。生成代码的示例如下:

因为只执行一次,所以 FC 不允许 early return。如果 FC 进行了 early return,则 return 之后的 DOM 可能根本不会被创建,也就无从渲染与更新。也同时因为只执行一次,UI 的插入、移动和销毁必须通过直接操作真实 DOM 来完成,所以 SolidJS 才会提供一堆内置的 components,用来管理响应式依赖的生命周期(变化监听、条件计算、清理等)。

比如下面的代码:

jsx
<Show when={isLoggedIn()} fallback={<Login />}>
  <Dashboard />
</Show>;

Dashboard 内可能有一些副作用,所以需要用 Show 在挂载/卸载组件的时候也顺便处理这些副作用。

配套设施

SolidJS 虽然相对小众,但仍有着完善的生态系统。

这些基本都是事实标准,意味着如果你发现需要啥东西,不需要纠结用哪家的实现,用就完了。

但是很可惜,solid 用的人较少,导致很多库的 bug 都没人修。