Solid-js 基礎(chǔ)教程

Solid-js

用于構(gòu)建用戶界面的聲明式、高效且靈活的 JavaScript 庫
您可以在 官方教程 中嘗試下面提到的部分例子,本文引用并簡化了官方教程中的部分例子

本文講述部分 solid 主要內(nèi)容,更多詳細內(nèi)容,移步 Solid API 文檔

Solid 使用了和 React 相似的語法和類似 Svelte 的預編譯
Solid 使用上類似于 React,使用 JSX 語法,但不同于 React, 組件只會初始化一次,并不是 state 改變就重新運行渲染整個組件,這類似于 Vue3 的 setup

為什么選擇 Solid

Solid 官網(wǎng) 給出了以下理由

  • 高性能 - 始終在公認的 UI 速度和內(nèi)存利用率基準測試中名列前茅
  • 強大 - 可組合的反應式原語與 JSX 的靈活性相結(jié)合
  • 務實 - 合理且量身定制的 API 使開發(fā)變得有趣而簡單
  • 生產(chǎn)力 - 人體工程學和熟悉程度使構(gòu)建簡單或復雜的東西變得輕而易舉

主要優(yōu)勢

高性能 - 接近原生的性能,在 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>
  );
}
// 組件聲明也可以直接用箭頭函數(shù)
/*
const App = ()=> (<div>Solid My App</div>);
*/

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

是不是看起來非常熟悉,就和 React 一樣,非常舒服

導入組件,傳遞組件,props

與 React 類似的使用方法, 但不能解構(gòu) 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 中最基本的反應性單元,此函數(shù)類似于 React 的 useState,但返回函數(shù)用于獲取調(diào)用它獲取值,而不是像 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,與上面的相同,也需要通過調(diào)用來獲取值

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 一般用于副作用,在狀態(tài)改變的時候運行副作用
它類似于 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 都會更新。在大多數(shù)情況下,這完全沒問題,因為 Solid 的粒度渲染只是反應式系統(tǒng)中更新的傳播?!颁秩尽眱纱螣o關(guān)的更改實際上并不意味著浪費工作。
如果更改是相關(guān)的怎么辦?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"));

在這個例子中,我們在按鈕點擊時更新了兩個狀態(tài),它觸發(fā)了兩次更新,您可以在控制臺中看到日志,因此,讓我們修改 updateNames 將 set 調(diào)用打包成一個批處理。

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

現(xiàn)在,對于同一個元素只會觸發(fā)一次更新

樣式

先創(chuàng)建一個樣式文件以便下面使用

/* 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

用于設(shè)置給定的 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 實現(xiàn)相同功能,但是使用其則具有高于 JSX 的性能,Solid 可以對其進行更多優(yōu)化
fallback 是在失敗后的顯示

For

簡單的引用鍵控循環(huán)控制流程。

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

非索引迭代循環(huán)控制流程,如果要迭代的不是數(shù)組,而是類似對象這類,使用 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 作用相同
用于將元素渲染到組件之外的地方,這對于模態(tài)窗,信息提示等是剛需
示例:將元素直接渲染到 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 節(jié)點本身

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

傳遞 ref 則直接在元素上綁定,如
<div ref={props.ref}></div>

spread

有時您的組件和元素接受可變數(shù)量的屬性,將它們作為對象而不是單獨傳遞是有意義的。在組件中包裝 DOM 元素時尤其如此,這是制作設(shè)計系統(tǒng)時的常見做法
為此,我們使用擴展運算符...。
我們可以傳遞一個具有可變數(shù)量屬性的對象:

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

用于創(chuàng)建一個 store,store 可用于精確地嵌套反應
此函數(shù)將創(chuàng)建一個信號樹作為代理,允許獨立跟蹤嵌套數(shù)據(jù)結(jié)構(gòu)中的各個值。create 函數(shù)返回一個只讀代理對象和一個 setter 函數(shù)

前面所說的 For 標簽在這里會很有用,因為直接使用 JSX 則會直接刷新整個表達式,從而無法細粒度更新

先看一個沒有使用 store 的例子
這里使用一些示例數(shù)據(jù)并使用 For 標簽迭代渲染
點擊復選框可以切換其選擇狀態(tài),這里先簡單的映射原始數(shù)據(jù)生成新數(shù)據(jù)在 set 過去
復制代碼運行,并嘗試點擊復選框,查看控制臺輸出

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

const App = () => {
  const [state, setState] = createSignal(
    {
      // 初始化一個具有 id, text, completed 屬性的對象組成的數(shù)組
      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}
      ]
    }
  );

  // 修改點擊的復選框的選擇狀態(tài)
  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"));

會發(fā)現(xiàn)控制臺隨點擊每次輸出,這是因為每次都銷毀重建了元素,我們只是修改了一個屬性,卻要重建元素,這是一種浪費
如果我們使用 store 可以有更精確的反應,而不需要重建元素,只會在原有的位置更新
把上面的代碼修改為以下代碼,再次運行并且點擊,會發(fā)現(xiàn),元素不再被銷毀重建,這保證了高性能

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 屬性的對象組成的數(shù)組
      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}
      ]
    }
  );

  // 修改點擊的復選框的選擇狀態(tài)
  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"));

仔細對比上面兩個例子,我們會發(fā)現(xiàn)主要修改與區(qū)別如下
createSignal 修改為了 createStore
由于 createStore 直接返回只讀代理,而不是 Getter,所以無需調(diào)用,直接使用
signal 設(shè)置值只是簡單的遍歷原始數(shù)據(jù),改變并產(chǎn)生新數(shù)據(jù),在大多數(shù)應用中都是如此,但 Solid 對于這種情況有一定的優(yōu)化策略
設(shè)置 store 的值可以像類似 react setState 一樣,讓對象淺合并
但是此處我們使用了 solid 所支持的另外一種方式,這可以讓 solid 知曉我們詳細變化了哪些東西,從而細粒度地更新
在上面的例子中
我們將 toggleTodo 修改為了這種樣子

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

這種是 Solid 中的路徑語法,參考 官方 API 文檔
路徑可以是字符串鍵、鍵數(shù)組、迭代對象({from、to、by})或過濾器函數(shù)。這為描述狀態(tài)變化提供了令人難以置信的表達能力。

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 強烈建議使用淺層不可變模式來更新狀態(tài)。通過分離讀取和寫入,我們可以更好地控制系統(tǒng)的反應性,而不會在通過組件層時丟失對代理更改的跟蹤的風險
然而,有時候突變更容易理解
為此,受 Immer 啟發(fā)的 Solid 提供了一個 produce,用于讓 store 可變
沿用上面的例子,修改 toggleTodo 為

  const toggleTodo = (id) => {
    setStore(
      "todos", 
      produce((todos) => {
        todos.push({ id: ++todoId, text, completed: false });
      }),
    );
  };
更多 store 相關(guān)內(nèi)容

異步

lazy

在應用中,某些組件只在使用時加載,這些組件會被單獨打包,在某個時間被按需加載,solid 也提供了方法
使用 lazy 替換普通的靜態(tài) import 語句

import Component1 from "./Component1.jsx";

替換為

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

由于 lazy 接接收的參數(shù)只是返回 Solid 組件的 Promise,因此,還可以在加載的時候附加一些行為

createResource

創(chuàng)建一個可以管理異步請求的信號。fetcher 是一個異步函數(shù),它接受sourceif 提供的返回值并返回一個 Promise,其解析值設(shè)置在資源中。fetcher 不是響應式的,因此如果您希望它運行多次,請使用可選的第一個參數(shù)。如果源解析為 false、null 或 undefined,則不會獲取。
官網(wǎng)在線嘗試

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);
// 獲取值
data();
// 檢查其是否加載中
data.loading;
// 檢查是否出錯
data.error;
// 直接設(shè)置值
mutate(optimisticValue);
// 刷新,重新請求
refetch();
Suspense

Suspense 配合異步組件使用
在尚未加載完畢時顯示 fallback 中給定的內(nèi)容

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

export default function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Component1></Component1>
    </Suspense>
  )
}
更多異步內(nèi)容

總結(jié)

  • Solid 具有高性能,并且具有極小的打包體積,適合打包為獨立的模塊嵌入其它項目
  • Solid 上手簡單,貼合 React 或是 Vue3 開發(fā)者的使用習慣
  • Solid 中 JSX 直接返回 DOM 元素,符合直覺,并且很純凈
  • Solid 某些地方需要使用其指定的東西才能達到高性能,高性能并不是毫無代價的
  • Solid 目前使用并不多,生態(tài)有待完善
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容