Solid-js 基础教程

Solid-js

用于构建用户界面的声明式、高效且灵活的 JavaScript 库
您可以在 官方教程 中尝试下面提到的部分例子,本文引用并简化了官方教程中的部分例子

本文讲述部分 solid 主要内容,更多详细内容,移步 Solid API 文档

Solid 使用了和 React 相似的语法和类似 Svelte 的预编译
Solid 使用上类似于 React,使用 JSX 语法,但不同于 React, 组件只会初始化一次,并不是 state 改变就重新运行渲染整个组件,这类似于 Vue3 的 setup

为什么选择 Solid

Solid 官网 给出了以下理由

  • 高性能 - 始终在公认的 UI 速度和内存利用率基准测试中名列前茅
  • 强大 - 可组合的反应式原语与 JSX 的灵活性相结合
  • 务实 - 合理且量身定制的 API 使开发变得有趣而简单
  • 生产力 - 人体工程学和熟悉程度使构建简单或复杂的东西变得轻而易举

主要优势

高性能 - 接近原生的性能,在 js-framework-benchmark 排名中名列前茅
极小的打包体积 - 编译为直接的DOM操作,无虚拟DOM,极小的运行时(类似于 Svelte),适合打为独立的 webComponent 在其它应用中嵌入
易于使用 - 近似 React 的使用体验,便于快速上手

快速开始

新建项目

npx degit solidjs/templates/js my-app
cd my-app
npm i
npm run dev

基本示例

这里将 App 组件渲染到 body 容器中

这里修改默认示例, 从零开始尝试

// App.JSX
import { render } from "solid-js/web";

function App() {
  return (
    <div>Solid My App</div>
  );
}
// 组件声明也可以直接用箭头函数
/*
const App = ()=> (<div>Solid My App</div>);
*/

render(() => <App />, document.querySelector("body"));

是不是看起来非常熟悉,就和 React 一样,非常舒服

导入组件,传递组件,props

与 React 类似的使用方法, 但不能解构 props,否则将失去反应性

// App.JSX
import { render } from "solid-js/web";

import Component1 from "./Component1.jsx";

function App() {
  return (
    <div>
      Solid My App
      <Component1 text={"component1"}>
        <div>children</div>
      </Component1>
    </div>
  );
}

render(() => <App />, document.querySelector("body"));

// Component1.jsx
export default function Component1(props) {
  return (
    <div>
      {props.text}
      {props.children}
    </div>
  )
}

反应性

createSignal

signal 是 Solid 中最基本的反应性单元,此函数类似于 React 的 useState,但返回函数用于获取调用它获取值,而不是像 React 一样直接取得值,下列是一个基本的 Counter 示例

import { createSignal } from "solid-js";

export default function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <button onClick={()=> setCount(count() + 1)}>
      {count()}
    </button>
  )
}
createMemo

createMemo 用于生成只读的派生值,类似于 Vue 中的 computed,与上面的相同,也需要通过调用来获取值

import { createSignal, createMemo } from "solid-js";

export default function Counter() {
  const [count, setCount] = createSignal(0);
  // count 的平方派生自 count,在依赖改变的时候自动更新
  const countPow2 = createMemo(()=> count() ** 2);
  return (
    <button onClick={()=> setCount(count() + 1)}>
      {count()} | {countPow2()}
    </button>
  )
}
createEffect

createEffect 一般用于副作用,在状态改变的时候运行副作用
它类似于 React 中的 useEffect 但其自动收集依赖,无需显式声明依赖,这和 Vue 中的 watchEffect 作用相同

import { createSignal, createEffect } from "solid-js";

export default function Counter() {
  const [count, setCount] = createSignal(0);
  // 每当依赖改变就会重新运行该副作用
  createEffect(()=> console.log(count()));
  return (
    <button onClick={()=> setCount(count() + 1)}>
      {count()}
    </button>
  )
}

如果需要显式声明依赖,参考 Solid createEffect 显式声明依赖

batch

Solid 的反应性是同步的,这意味着在任何更改后的下一行 DOM 都会更新。在大多数情况下,这完全没问题,因为 Solid 的粒度渲染只是反应式系统中更新的传播。“渲染”两次无关的更改实际上并不意味着浪费工作。
如果更改是相关的怎么办?Solid 的batch助手允许将多个更改排队,然后在通知观察者之前同时应用它们。在批处理中更新的信号值直到完成才会提交。
参考以下不使用 batch 的例子

import { render } from "solid-js/web";
import { createSignal, batch } from "solid-js";

const App = () => {
  const [firstName, setFirstName] = createSignal("John");
  const [lastName, setLastName] = createSignal("Smith");
  const fullName = () => {
    console.log("Running FullName");
    return `${firstName()} ${lastName()}`
  } 
  const updateNames = () => {
    console.log("Button Clicked");
    setFirstName(firstName() + "n");
    setLastName(lastName() + "!");
  }
  
  return <button onClick={updateNames}>My name is {fullName()}</button>
};

render(App, document.getElementById("app"));

在这个例子中,我们在按钮点击时更新了两个状态,它触发了两次更新,您可以在控制台中看到日志,因此,让我们修改 updateNames 将 set 调用打包成一个批处理。

 const updateNames = () => {
    console.log("Button Clicked");
    batch(() => {
      setFirstName(firstName() + "n");
      setLastName(lastName() + "!");
    })
  }

现在,对于同一个元素只会触发一次更新

样式

先创建一个样式文件以便下面使用

/* main.module.css */
.container {
  width: 100px;
  height: 100px;
  background-color: green;
}
.text {
  font-size: 20px;
  color: red;
}
基本使用

样式使用也与 React 非常类似,只是使用 class 而不是 className

import style from "./main.module.css";

export default function Container() {
  return (
    <div class={style.container}>
      <span class={style.text}>text</span>
    </div>
  )
}
classList

用于设置给定的 class 是否存在, 也可以绑定响应式
下列是一个点击切换 class 的示例

import style from "./main.css";
import { createSignal } from "solid-js";

export default function Container() {
  const [hasTextClassName, setHasTextClassName] = createSignal(false);
  return (
    <div 
      classList={
        {
          [style.container]: true,
          [style.text]: hasTextClassName()
        }
      } 
      onClick={
        ()=> setHasTextClassName(!hasTextClassName())
      }
    >
    text
    </div>
  )
}

基本的控制流

控制流大多可以用 JSX 实现相同功能,但是使用其则具有高于 JSX 的性能,Solid 可以对其进行更多优化
fallback 是在失败后的显示

For

简单的引用键控循环控制流程。

export default function Container() {
  return (
    <div>
      <For 
        each={[1,2,3,4,5,6,7,8,9,10]} 
        fallback={<div>Failed</div>}
      >
        {(item) => <div>{item}</div>}
      </For>
    </div>
  )
}
Show

Show 控制流用于有条件地渲染视图的一部分。它类似于三元运算符 (a ? b : c),但非常适合模板化 JSX。

import { createSignal } from "solid-js";

export default function Container() {
  const [count, setCount] = createSignal(10);
  return (
    <div>
      {/* 在 count 大于 5 的时候渲染*/}
      <Show 
        when={count() > 5} 
        fallback={<div>Failed</div>}
      >
        <div>content</div>
      </Show>
    </div>
  )
}
Switch

Switch 在有 2 个以上的互斥条件时很有用。可以用来做一些简单的路由之类的事情。

import { createSignal } from "solid-js";

export default function Container() {
  const [count, setCount] = createSignal(10);
  return (
    <div>
      <Switch fallback={<div>Failed</div>}>
        <Match when={count() > 5}>
          <div>count > 5</div>
        </Match>
        <Match when={count() < 5}>
          <div>count < 5</div>
        </Match>
      </Switch>
    </div>
  )
}
Index

非索引迭代循环控制流程,如果要迭代的不是数组,而是类似对象这类,使用 Index

export default function Container() {
  return (
    <div>
      <Index 
        each={{
          name: "name",
          gender: "male",
          age: 100,
          address: "address",
        }} 
        fallback={<div>Failed</div>}
      >
        {(item) => <div>{item}</div>}
      </Index>
    </div>
  )
}
ErrorBoundary

错误边界


function ErrorComponent() {
  // 抛出错误
  throw new Error("component error");
  return (
    <div>content</div>
  )
}

export default function Container() {
  return (
    <ErrorBoundary fallback={<div>Failed</div>}>
      <ErrorComponent></ErrorComponent>
    </ErrorBoundary>
  )
}
Portal

和 React Portal 作用相同
用于将元素渲染到组件之外的地方,这对于模态窗,信息提示等是刚需
示例:将元素直接渲染到 body 下

export default function Container() {
  return (
    <Portal mount={document.querySelector("body")}>
      <div>content</div>
    </Portal>
  )
}
其他控制流

参考 API 文档

生命周期

挂载时:onMount
卸载时:onCleanup

import { onMount, onCleanup } from "solid-js";
export default function Container() {
  onMount(()=> {
    console.log("onMount");
  });
  onCleanup(()=> {
    console.log("onCleanup");
  });
  return (
    <div>content</div>
  )
}

绑定

ref

ref 用于获取 DOM 节点本身

export default function Container() {
  let $container;
  return (
    <div ref={$container}>
      container
    </div>
  )
}

传递 ref 则直接在元素上绑定,如
<div ref={props.ref}></div>

spread

有时您的组件和元素接受可变数量的属性,将它们作为对象而不是单独传递是有意义的。在组件中包装 DOM 元素时尤其如此,这是制作设计系统时的常见做法
为此,我们使用扩展运算符...。
我们可以传递一个具有可变数量属性的对象:

function Info(props) {
  return (
    <div>
      <div>{props.name}</div>
      <div>{props.speed}</div>
      <div>{props.version}</div>
      <div>{props.website}</div>
    </div>
  );
}

const pkg = {
  name: "solid-js",
  version: 1,
  speed: "⚡",
  website: "https://solidjs.com",
};

function Main() {
  return (
    <Info 
      name={pkg.name}
      version={pkg.version}
      speed={pkg.speed}
      website={pkg.website}
    >
    </Info>
  )
}
// 等同于
function Main() {
  return (
    <Info 
      {...pkg}
    >
    </Info>
  )
}

store/嵌套反应

Solid 中细粒度反应性的原因之一是它可以独立处理嵌套更新。你可以有一个用户列表,当我们更新一个名字时,我们只更新 DOM 中的一个位置,而不会对列表本身进行差异化。很少(甚至是反应式)UI 框架可以做到这一点。

createStore

用于创建一个 store,store 可用于精确地嵌套反应
此函数将创建一个信号树作为代理,允许独立跟踪嵌套数据结构中的各个值。create 函数返回一个只读代理对象和一个 setter 函数

前面所说的 For 标签在这里会很有用,因为直接使用 JSX 则会直接刷新整个表达式,从而无法细粒度更新

先看一个没有使用 store 的例子
这里使用一些示例数据并使用 For 标签迭代渲染
点击复选框可以切换其选择状态,这里先简单的映射原始数据生成新数据在 set 过去
复制代码运行,并尝试点击复选框,查看控制台输出

import { render } from "solid-js/web";
import { For, createSignal } from "solid-js";

const App = () => {
  const [state, setState] = createSignal(
    {
      // 初始化一个具有 id, text, completed 属性的对象组成的数组
      todos: [
        {id: 1, text: 1, completed: false},
        {id: 2, text: 2, completed: false},
        {id: 3, text: 3, completed: false},
        {id: 4, text: 4, completed: false}
      ]
    }
  );

  // 修改点击的复选框的选择状态
  const toggleTodo = (id) => {
    setState({
      todos: state().todos.map((todo) => (
        todo.id !== id 
        ? todo 
        : { ...todo, completed: !todo.completed }
      ))
    });
  }

  return (
    <>
      <For each={state().todos}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`)
          return <div>
            <input
              type="checkbox"
              checked={todo.completed}
              onchange={[toggleTodo, id]}
            />
            <span
              style={{ "text-decoration": todo.completed ? "line-through" : "none"}}
            >{text}</span>
          </div>
        }}
      </For>
    </>
  );
};

render(App, document.getElementById("app"));

会发现控制台随点击每次输出,这是因为每次都销毁重建了元素,我们只是修改了一个属性,却要重建元素,这是一种浪费
如果我们使用 store 可以有更精确的反应,而不需要重建元素,只会在原有的位置更新
把上面的代码修改为以下代码,再次运行并且点击,会发现,元素不再被销毁重建,这保证了高性能

store 的具体使用在下文具体解释

import { render } from "solid-js/web";
import { For } from "solid-js";
import { createStore } from "solid-js/store";

const App = () => {
  const [store, setStore] = createStore(
    {
      // 初始化一个具有 id, text, completed 属性的对象组成的数组
      todos: [
        {id: 1, text: 1, completed: false},
        {id: 2, text: 2, completed: false},
        {id: 3, text: 3, completed: false},
        {id: 4, text: 4, completed: false}
      ]
    }
  );

  // 修改点击的复选框的选择状态
  const toggleTodo = (id) => {
    setStore(
      "todos", 
      (t) => t.id === id, 
      'completed', 
      (completed) => !completed
    );
  };

  return (
    <>
      <For each={store.todos}>
        {(todo) => {
          const { id, text } = todo;
          console.log(`Creating ${text}`)
          return <div>
            <input
              type="checkbox"
              checked={todo.completed}
              onchange={[toggleTodo, id]}
            />
            <span
              style={{ "text-decoration": todo.completed ? "line-through" : "none"}}
            >{text}</span>
          </div>
        }}
      </For>
    </>
  );
};

render(App, document.getElementById("app"));

仔细对比上面两个例子,我们会发现主要修改与区别如下
createSignal 修改为了 createStore
由于 createStore 直接返回只读代理,而不是 Getter,所以无需调用,直接使用
signal 设置值只是简单的遍历原始数据,改变并产生新数据,在大多数应用中都是如此,但 Solid 对于这种情况有一定的优化策略
设置 store 的值可以像类似 react setState 一样,让对象浅合并
但是此处我们使用了 solid 所支持的另外一种方式,这可以让 solid 知晓我们详细变化了哪些东西,从而细粒度地更新
在上面的例子中
我们将 toggleTodo 修改为了这种样子

  (id) => {
    setStore(
      "todos", 
      (t) => t.id === id, 
      'completed', 
      (completed) => !completed
    );
  };

这种是 Solid 中的路径语法,参考 官方 API 文档
路径可以是字符串键、键数组、迭代对象({from、to、by})或过滤器函数。这为描述状态变化提供了令人难以置信的表达能力。

const [state, setState] = createStore({
  todos: [
    { task: 'Finish work', completed: false }
    { task: 'Go grocery shopping', completed: false }
    { task: 'Make dinner', completed: false }
  ]
});

setState('todos', [0, 2], 'completed', true);
// {
//   todos: [
//     { task: 'Finish work', completed: true }
//     { task: 'Go grocery shopping', completed: false }
//     { task: 'Make dinner', completed: true }
//   ]
// }

setState('todos', { from: 0, to: 1 }, 'completed', c => !c);
// {
//   todos: [
//     { task: 'Finish work', completed: false }
//     { task: 'Go grocery shopping', completed: true }
//     { task: 'Make dinner', completed: true }
//   ]
// }

setState('todos', todo => todo.completed, 'task', t => t + '!')
// {
//   todos: [
//     { task: 'Finish work', completed: false }
//     { task: 'Go grocery shopping!', completed: true }
//     { task: 'Make dinner!', completed: true }
//   ]
// }

setState('todos', {}, todo => ({ marked: true, completed: !todo.completed }))
// {
//   todos: [
//     { task: 'Finish work', completed: true, marked: true }
//     { task: 'Go grocery shopping!', completed: false, marked: true }
//     { task: 'Make dinner!', completed: false, marked: true }
//   ]
// }
produce

Solid 强烈建议使用浅层不可变模式来更新状态。通过分离读取和写入,我们可以更好地控制系统的反应性,而不会在通过组件层时丢失对代理更改的跟踪的风险
然而,有时候突变更容易理解
为此,受 Immer 启发的 Solid 提供了一个 produce,用于让 store 可变
沿用上面的例子,修改 toggleTodo 为

  const toggleTodo = (id) => {
    setStore(
      "todos", 
      produce((todos) => {
        todos.push({ id: ++todoId, text, completed: false });
      }),
    );
  };
更多 store 相关内容

异步

lazy

在应用中,某些组件只在使用时加载,这些组件会被单独打包,在某个时间被按需加载,solid 也提供了方法
使用 lazy 替换普通的静态 import 语句

import Component1 from "./Component1.jsx";

替换为

const Component1 = lazy(() => import("./Component1"));

由于 lazy 接接收的参数只是返回 Solid 组件的 Promise,因此,还可以在加载的时候附加一些行为

createResource

创建一个可以管理异步请求的信号。fetcher 是一个异步函数,它接受sourceif 提供的返回值并返回一个 Promise,其解析值设置在资源中。fetcher 不是响应式的,因此如果您希望它运行多次,请使用可选的第一个参数。如果源解析为 false、null 或 undefined,则不会获取。
官网在线尝试

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);
// 获取值
data();
// 检查其是否加载中
data.loading;
// 检查是否出错
data.error;
// 直接设置值
mutate(optimisticValue);
// 刷新,重新请求
refetch();
Suspense

Suspense 配合异步组件使用
在尚未加载完毕时显示 fallback 中给定的内容

const Component1 = lazy(() => import("./Component1"));

export default function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Component1></Component1>
    </Suspense>
  )
}
更多异步内容

总结

  • Solid 具有高性能,并且具有极小的打包体积,适合打包为独立的模块嵌入其它项目
  • Solid 上手简单,贴合 React 或是 Vue3 开发者的使用习惯
  • Solid 中 JSX 直接返回 DOM 元素,符合直觉,并且很纯净
  • Solid 某些地方需要使用其指定的东西才能达到高性能,高性能并不是毫无代价的
  • Solid 目前使用并不多,生态有待完善
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354