初识SolidJs

4,382 阅读5分钟

介绍

SolidJs 一个用于构建用户界面,简单高效、性能卓越的JavaScript库。

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

专注于性能

性能仅次于原生js

一个简单的例子

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

const App = () => <div> hello solidjs!</div>

render(() => <App/>document.getElementById("app"))

写过react的应该很熟悉这段代码,jsx片段,render函数。会让你感觉既熟悉又现代。

响应式

createSignal

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

function Counter() {
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    return <div>{count()}</div>;
}
    
render(() => <Counter />document.getElementById('app'));

signal是solid中基本的响应单元,createSignal类似react中的useState,传递给createSignal调用的参数是初始值,createSignal返回一个两个==函数==的数组,第一个getter,第二个是setter,第一个返回的值是一个getter而不是一个值,使用的时候需要调用,框架拦截读取值的任何位置来进行自动跟踪,从而响应式更新,所以调用getter的位置很重要,和react不同的是,例如setState触发更新,react会生成Fiber树,进行diff算法,最后执行dom操作。solid则是直接调用编译好的dom操作方法,没有虚拟dom比较。

createEffect

import { render } from 'solid-js/web';
import { createSignal, createEffect } from 'solid-js';

function Counter() {
    const [count, setCount] = createSignal(0);
    
    createEffect(() => {
        console.log('count is :'count())
    })
    
    
    return <button onClick={() => setCount(count() + 1)}>Click me</button>;
}
    
render(() => <Counter />document.getElementById('app'));

createEffect接收一个函数,监听其执行情况,createEffect会自动订阅在执行期间读取的所有Signal,并在Signal值之一发生改变的时候,重新运行此函数。count更改的时候,createEffect函数就会运行,从而点击一次,就打印一次结果。类似react的useEffect

useEffect(() => {/*....*/}, [count])

衍生Signal

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

function Counter() {
    const [count, setCount] = createSignal(0);
    
    const doubleCount = () => count() * 2
    
    setInterval(() => setCount(count() + 1), 1000);

    return <div>Count: {doubleCount()}</div>;
}

render(() => <Counter />document.getElementById('app'));

这次没有直接使用count(), 而是使用了doubleCount函数包了一层count() * 2, 日志正常打印2倍的count(), 意味着任何包装count()的函数,都是一个Signal,访问JSX中的js表达式也是,只要访问一个signal,就会触发更新。

Props

Props是组件执行的时候传进来的对象,Props是只读的,并且具备对象getter的响应性的,但是响应性,只能通过props.propsName的形式来访问,才能被追踪到。不能解构props,解构就会脱离追踪范围而失去响应。

默认Props

// greeting.jsx
import { mergeProps } from "solid-js";

export default function Greeting(props) {
  return <h3>{props.greeting || "Hi"} {props.name || "John"}</h3>

    <!--const { greeting, name } = props-->
    <!--return <h3>{greeting || 'Hi'} {name || 'John'}</h3>-->
}

// main.jsx

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

import Greeting from "./greeting";

function App() {
  const [name, setName] = createSignal();

  return <>
    <Greeting greeting="Hello" />
    <Greeting name="Jeremy" />
    <Greeting name={name()} />
    <button onClick={() => setName("Jarod")}>Set Name</button>
  </>;
}

render(() => <App />document.getElementById('app'));

当然solid也是提供了一个工具函数mergeProps,使得具有响应性。

// greeting.jsx
import { mergeProps } from "solid-js";

export default function Greeting(props) {
  const merged = mergeProps({ greeting"Hi"name"John" }, props)

  return <h3>{merged.greeting} {merged.name}</h3>
}

分离Props

合并props用到的地方很少,我们经常用到的是解构组件传进来的props,然后将其他props分离出来,再传递下去。

// greeting.jsx

export default function Greeting(props) {
  const { greeting, name, ...others } = props;
  return <h3 {...others}>{greeting} {name}</h3>
}

直接解构分离,设置name的时候,不会更新,也就是解构的时候失去了响应性。但是solid为我们提供了了分离props的函数splitProps,来保持响应性。

// greeting.jsx

export default function Greeting(props) {
  const [local, others] = splitProps(props, ["greeting""name"]);
  return <h3 {...others}>{local.greeting} {local.name}</h3>
}

Store

内嵌式响应

solid可以独立处理嵌套更新,是因为它提供了一细粒度响应式,也就是哪里需要更新,就更新哪里,指哪打哪。

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

const App = () => {
  const [todos, setTodos] = createSignal([])
  let input;
  let todoId = 0;

  const addTodo = (text) => {
    // setTodos([...todos(), { id: ++todoId, text, completed: false }]);
    const [completed, setCompleted] = createSignal(false)
    setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]);
  }
  const toggleTodo = (id) => {
    // setTodos(todos().map((todo) => (
    //   todo.id !== id ? todo : { ...todo, completed: !todo.completed }
    // )));
    const index = todos().findIndex((t) => t.id === id);
    const todo = todos()[index];
    if (todo) todo.setCompleted(!todo.completed())
  }

  return (
    <>
      <div>
        <input ref={input} />
        <button
          onClick={(e) => {
            if (!input.value.trim()) return;
            addTodo(input.value);
            input.value = "";
          }}
        >
          Add Todo
        </button>
      </div>
      <For each={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"));

两种方式,第一种是追踪todos()更新,效果是新增的时候会触发渲染,等到toggleTodo的时候还是会触发渲染,第二种是,嵌套更新,追踪对象的属性completed(), 新增的时候追踪todos()变化,渲染dom,等到toggleTodo的时候,只会触发数据更新,dom已经渲染了,没有必要再次渲染了。

创建store

store是代理对象,属性可以被跟踪,那么是不是可以实现内嵌式响应,createStore接收一个初始值,返回一个类似于signal的读/写的两个元素,第一个是元素只读的store代理,第二个是setter函数。

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

const App = () => {
  let input;
  let todoId = 0;
  const [store, setStore] = createStore({ todos: [] });
  const addTodo = (text) => {
    setStore('todos', (todos) => [...todos, { id: ++todoId, text, completed: false }]);
    };
  const toggleTodo = (id) => {
    setStore('todos', (t) => t.id === id, 'completed', (completed) => !completed);
    };
  return (
    <>
      <div>
        <input ref={input} />
        <button
          onClick={(e) => {
            if (!input.value.trim()) return;
            addTodo(input.value);
            input.value = "";
          }}
        >
          Add Todo
        </button>
      </div>
      <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"));

内置组件

Show

条件渲染

import { render } from 'solid-js/web';
import { createSignal, Show } from 'solid-js';

function App() {
  const [loggedIn, setLoggedIn] = createSignal(false);
  const toggle = () => setLoggedIn(!loggedIn())
  
  return (
    <Show
        when={loggedIn()}
        fallback={() => <button onClick={toggle}>Log in</button>}
        >
        <button onClick={toggle}>Log out</button>
    </Show>
  );
}

render(() => <App />document.getElementById('app'))

For

循环遍历,数据是固定的,index是可追踪的signal,涉及到dom移动的时候,不会触发重新创建dom。只是index独立更新,数据并不会更新。

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

function App() {
  const [cats, setCats] = createSignal([
  { id'J---aiyznGQ'name'Keyboard Cat' },
  { id'z_AbfPXTKms'name'Maru' },
  { id'OUtn3pvWmpg'name'Henri The Existential Cat' }
 ]);
  
  return (
    <ul>
      <For each={cats()}>
  {(cat, i) => (
    <li>
      <a target="_blank" href={`https://www.youtube.com/watch?v=${cat.id}`}>
        {i() + 1}: {cat.name}
      </a>
    </li>
  )}
</For>
    </ul>
  );
}

render(() => <App />document.getElementById('app'))

Index

Index和For不同的是,Index是数据项是signal,索引是固定的。

import { render } from 'solid-js/web';
import { createSignal, Index } from 'solid-js';

function App() {
  const [cats, setCats] = createSignal([
  { id'J---aiyznGQ'name'Keyboard Cat' },
  { id'z_AbfPXTKms'name'Maru' },
  { id'OUtn3pvWmpg'name'Henri The Existential Cat' }
 ]);
  
  return (
    <ul>
      <Index each={cats()}>
  {(cat, i) => (
    <li>
      <a target="_blank" href={`https://www.youtube.com/watch?v=${cat().id}`}>
        {i + 1}: {cat().name}
      </a>
    </li>
  )}
</Index>
    </ul>
  );
}

render(() => <App />document.getElementById('app'))

Switch

处理互斥条件

import { render } from "solid-js/web";
import { createSignal, Show, Switch, Match } from "solid-js";

function App() {
  const [x] = createSignal(7);

  return (
    //使用show实现
    <!--<Show-->
    <!--  when={x() > 10}-->
    <!--  fallback={-->
    <!--    <Show-->
    <!--      when={5 > x()}-->
    <!--      fallback={<p>{x()} is between 5 and 10</p>}-->
    <!--    >-->
    <!--      <p>{x()} is less than 5</p>-->
    <!--    </Show>-->
    <!--  }-->
    <!-->-->
    <!--  <p>{x()} is greater than 10</p>-->
    <!--</Show>-->
    //使用Switch实现
    <Switch fallback={<p>{x()} is between 5 and 10</p>}>
  <Match when={x() > 10}>
    <p>{x()} is greater than 10</p>
  </Match>
  <Match when={5 > x()}>
    <p>{x()} is less than 5</p>
  </Match>
</Switch>
  );
}

render(() => <App />, document.getElementById("app"));

Dynamic

<Show><Switch> 组件更简练

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

const RedThing = () => <strong style="color: red">Red Thing</strong>;
const GreenThing = () => <strong style="color: green">Green Thing</strong>;
const BlueThing = () => <strong style="color: blue">Blue Thing</strong>;

const options = {
  redRedThing,
  greenGreenThing,
  blueBlueThing
}

function App() {
  const [selected, setSelected] = createSignal("red");

  return (
    <>
      <select value={selected()} onInput={e => setSelected(e.currentTarget.value)}>
        <For each={Object.keys(options)}>{
          color => <option value={color}>{color}</option>
        }</For>
      </select>
      <!--<Switch fallback={<BlueThing />}>-->
      <!--  <Match when={selected() === "red"} ><RedThing /></Match>-->
      <!--  <Match when={selected() === "green"}><GreenThing /></Match>-->
      <!--</Switch>-->
      <Dynamic component={options[selected()]} />
    </>
  );
}

render(() => <App />document.getElementById("app"));

生命周期

onMount

挂载

solid有极少的生命周期,请求数据,以及一些逻辑都写到这个地方,这个函数只会被调用一次。

import { render } from "solid-js/web";
import { createSignal, onMount, For } from "solid-js";
import "./styles.css";

function App() {
  const [photos, setPhotos] = createSignal([]);
onMount(async () => {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/photos?_limit=20`
  );
  setPhotos(await res.json());
});
  return <>
    <h1>Photo album</h1>

    <div class="photos">
      <For each={photos()} fallback={<p>Loading...</p>}>{ photo =>
        <figure>
          <img src={photo.thumbnailUrl} alt={photo.title} />
          <figcaption>{photo.title}</figcaption>
        </figure>
      }</For>
    </div>
  </>;
}

render(() => <App />document.getElementById('app'));

onCleanup

卸载时

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

function Counter() {
  const [count, setCount] = createSignal(0);

  const timer = setInterval(() => setCount(count() + 1), 1000);
    onCleanup(() => clearInterval(timer));

  return <div>Count: {count()}</div>;
}

render(() => <Counter />document.getElementById('app'));

总结

  1. 没有虚拟DOM。
  2. 响应式跟踪更新,指哪打哪。
  3. 支持jsx
  4. 写法类似react,上手容易

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~