Solid.js学习记录

299 阅读7分钟

大部分来源于官方教程,加一点自己理解

入门

Effect

监听数据变化,相当于vue的watch

组件

Solid 中的组件名称遵循 Pascal 命名约定,其中组件名称中每个单词的第一个字母大写。该命名可以在 JSX 中用作标签,比如 。如果有多个要返回的元素,需要包裹在同一个Fragment中。

function App() {
  return (
    <>
      <h1>This is a Header</h1>
      <Nested />
    </>
  );
}

Signal

Signal是最核心的响应式基本要素(primitive)。Singal包含随时间变化的值。

衍生Signal

任何包装访问Signal的函数实际上都是一个Signal并且也是可跟踪的。这就是衍生Signal

Memo

会将结果缓存,减少了执行消耗性能的操作,类似于vue的computed。 在如下例子中,未用Memo每次点击按钮会进行50次计算,使用Memo包裹后每次点击按钮只会计算一次。

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

function fibonacci(num) {
  if (num <= 1) return 1;

  return fibonacci(num - 1) + fibonacci(num - 2);
}

function Counter() {
  const [count, setCount] = createSignal(10);
//   const fib = () => {console.log("Calculating Fibonacci")
//  return fibonacci(count())};
const fib=createMemo(()=>{ console.log("Calculating Fibonacci");
  return fibonacci(count());})
  return (
    <>
      <button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
      <div>1. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>2. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>3. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>4. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>5. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>6. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>7. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>8. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>9. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
      <div>10. {fib()} {fib()} {fib()} {fib()} {fib()}</div>
    </>
  );
}

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

流程控制

Show

相当于vue的v-show。when属性是if判断条件,fallback属性充当else,在when不为true的时候显示。

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'))

相当于

<button onClick={toggle} v-show="loggedIn()">Log in</button>
<button onClick={toggle}  v-show="!loggedIn()">Log out</button>

For

v-for="(item,index) in list"

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

和For很像

For VS Index

For如果设置的新值里元素存在于旧值中,只是位置不同,则不会调用回调each,只移动DOM节点以反映元素位置的更改。如果该元素之前不在数组中,则调用each回调来呈现已更改的元素。 Index则只是比较每个索引处新旧值,如果不同,则调用回调函数each中的参数item()信号,注意不会调用回调本身。回调函数中依赖于参数item()信号的内容会被就地更新。只在有新元素被添加到数组末尾时才调用each回调。

For的回调each接收参数是一个item值和item位置信号。 Index的回调each接收参数是一个item信号和item位置值。

建议对于对象数组使用For,对于简单基元数组使用Index。 Index会重新渲染移动或改变的数组元素,所以如果数组元素是字符串等简单值,它的DOM只是一个文本节点,渲染不会消耗太大。 而如果元素是对象,那么它的Dom结构往往会更复杂,包含显示对象所需的任何内容。For不会重新渲染改变和移动的数组元素,只移动dom或执行嵌套更新,这比重新渲染整个对象更高效

对比例子:

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

function ForCats() {
  const [cats, setCats] = createSignal([
    'Keyboard Cat',
    'Maru',
    'Henri The Existential Cat'
  ]);
  
     setTimeout(() => setCats(['Maru', 'Keyboard Cat', 'Keyboard Cat', 'New Cat']), 2000)

  return (
    <ul>
    <For each={cats()}>{name => {

        console.log(`For: rendered ${name} whole cat`);

      return <li>
        <a target="_blank" href="">
          1: {name}
        </a>
      </li>
    }}</For>
    </ul>
  );
}


function IndexCats() {
  const [cats, setCats] = createSignal([
    'Keyboard Cat',
    'Maru',
    'Henri The Existential Cat'
  ]);
  
     setTimeout(() => setCats(['Maru', 'Keyboard Cat', 'Keyboard Cat', 'New Cat']), 2000)

  return (
    <ul>
    <Index each={cats()}>{name => {

        console.log(`Index: rendered ${name()} whole cat`);

      return <li>
        <a target="_blank" href="">
          1: {name()}
        </a>
      </li>
    }}</Index>
    </ul>
  );
}

render(() => <><ForCats /> <IndexCats/ ></>, document.getElementById('app'))

输出结果: For: rendered Keyboard Cat whole cat For: rendered Maru whole cat For: rendered Henri The Existential Cat whole cat Index: rendered Keyboard Cat whole cat Index: rendered Maru whole cat Index: rendered Henri The Existential Cat whole cat For: rendered Keyboard Cat whole cat For: rendered New Cat whole cat Index: rendered New Cat whole cat

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>

相当于:

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

Dynamic

根据数据渲染对应组件。通常比编写多个Show和Switch组件更简练。

<Dynamic component={options[selected()]} />
//相当于
 <component :is="options[selected()]" />

Portal

的子内容将被插入到选择的位置。相当于antdv的Modal,默认挂载到document.body下的div中。 eg:

<Portal>
  <div class="popup">
    <h1>Popup</h1>
    <p>Some text you might need for something or other.</p>
  </div>
</Portal>

ErrorBoundary

UI渲染错误不应破坏整个应用程序。错误边界是一个可以捕获子组件树任何位置产生的的JS错误的组件,它可以记录这些错误,并显示回退UI而非崩溃的组件树。

<ErrorBoundary fallback={(err) => err}>
  <Broken /> 
</ErrorBoundary>

生命周期

onMount

在Solid中一切的存活销毁都由响应系统控制。响应系统是同步创建和更新的,因此唯一的调度就是将逻辑写到更新结束的Effect中。onMount是一个非跟踪的createEffect调用,即不会重新运行。它只是一个Effect调用,一旦所有初始渲染完成,它只会运行一次。 生命周期仅在浏览器中运行,因此将代码放在onMount中运行的好处是SSR期间不在服务器上运行。

onCleanup

绑定

事件

const handler = (data, event /*...*/) => (
  <button onClick={[handler, data]}>Click Me</button>
);

所有on绑定不区分大小写,所以事件名需要小写。如果需要支持其他大小写或不使用事件委托可以使用on:namespace来匹配冒号后面的事件处理程序。

<button on:WierdEventName={() => /* Do something */} >Click Me</button>

样式

键需要采用破折号的形式,如background-color而不是backgroundColor。这也意味着可以设置CSS变量。 <div style={{ "--my-custom-color": themeColor() }} />

<div
 style={{
   color: `rgb(${num()}, 180, ${num()})`,
   "font-weight": 800,
   "font-size": `${num()}px`,
 }}
>
 Some Text
</div>

ClassList

Solid支持同时使用class和className来设置元素的类名。它还提供了一个内置的JSX属性classList,它接收一个对象,其中键是类名,值是布尔值,为true时应用该class,false时该class被移除。 eg:

<button
  class={current() === "foo" ? "selected" : ""}
  onClick={() => setCurrent("foo")}
>
  foo
</button>

可以替换为:

<button
  classList={{ selected: current() === "foo" }}
  onClick={() => setCurrent("foo")}
>
  foo
</button>

Ref

对Ref进行赋值是在元素创建时,在DOM被追加前发生的。只需声明一个变量,元素引用就会赋值给该变量。

let myDiv;

<div ref={myDiv}>My Element</div>;

Ref转发

父组件可以直接调用到子组件 父组件:

let canvas;
return <Canvas ref={canvas} />

子组件:

export default function Canvas(props) {
  return <canvas ref={props.ref} width="256" height="256" />;
}

扩展

当组件和元素接收可变数量的属性时:

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

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

function App() {
  return (
    // <Info
    //   name={pkg.name}
    //   version={pkg.version}
    //   speed={pkg.speed}
    //   website={pkg.website}
    // />
    <Info {...pkg} />
  );
}

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

指令

Solid通过use:namespace支持自定义指令。 在元素上使用指令:

<div class="modal" use:clickOutside={() => setShow(false)}>
  Some Modal
</div>

定义自定义指令,可以在单独的jsx文件中定义然后在使用时导入:

export default function clickOutside(el, accessor) {
  const onClick = (e) => !el.contains(e.target) && accessor()?.();
  document.body.addEventListener("click", onClick);

  onCleanup(() => document.body.removeEventListener("click", onClick));
}

此指令函数接收的两个参数中el是使用指令的dom元素,而accessor是使用指令时传入的函数,在这里就是()=>setShow(false)

Props

默认Props

Props对象是只读的,并且含有封装为对象getter的响应式属性。只需通过props.propName访问。 注意:不能直接解构props,否则会导致解构的值脱离追踪范围而失去响应性。除了解构之外,扩展运算符和Object.assign这样的函数也会导致失去响应性。在Solid的primitive或JSX之外访问props对象上的属性可能会失去响应性。

Solid提供的mergeProps函数可以将潜在的响应式对象合并而不会失去响应性。最常见的情况就是为组件设置默认props。

import { mergeProps } from "solid-js";

export default function Greeting(props) {
  // return <h3>{props.greeting || "Hi"} {props.name || "John"}</h3>
  const merged = mergeProps({ greeting: "Hi", name: "John" }, props);

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

分离Props

Solid提供了splitProps用于分离一些props出来传递给子组件,它接收一个props对象以及一个props对象的键数组。返回一个对象数组,数组第一个对象元素里包含接收的键数组对应的属性。数组的第二个对象元素里是剩下的props属性,类似于剩余参数。

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

Children

Solid提供了children工具函数,此方法既会根据children访问创建memo缓存,还会处理任何嵌套的子级响应式引用,以便可以直接与children交互。

当想给动态列表设置他们的color样式属性时,如果直接和props.children交互,会多次创建节点。

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

export default function ColoredList(props) {
  createEffect(() => c().forEach(item => item.style.color = props.color));

 const c = children(() => props.children);
  return <>{c()}</>
}

嵌套响应式

有一个用户列表,当更新其中一个名字时,大部分框架都是使用克隆对象替换旧的对象,重新进行列表差异化对比并重新创建Dom元素。而Solid提供了细粒度响应式,可以处理嵌套更新,当更新一个名字时只更新DOM中的一个位置,而不会对列表本身进行差异对比。 (似乎就是把需要修改的值创建为Signal) 更改前:

const toggleTodo = (id) => {
  setTodos(
    todos().map((todo) => (todo.id !== id ? todo : { ...todo, completed: !todo.completed })),
  );
};

更改后:

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

更改前每次修改选中状态都是用了一个新对象去替换匹配到的元素,是不存在于原数组中的新增元素。更改后并没有改变元素而是调用了嵌套更新。而For组件只有在新增元素的时候调用each,所以更改前变更选中状态时each被调用并在控制台打印,而更改后只有创建时打印一次。

创建Store

Store是Solid对于嵌套响应式的方案。它是Proxy对象,其属性可以被跟踪,并且可以包含其他对象,这些对象都会自动被包装在Proxy中。Solid只为在跟踪范围内访问的属性创建底层Signal,因此Store中所有的Signal都是根据要求延迟创建的。

createStore函数类似于createSignal。第一个元素是只读的store代理,第二个元素是setter函数。将值设置为 undefined 可以把它们从 store 中删除。

最基础的setter函数用法会接收一个对象,该对象的属性将于与当前状态合并

const [state, setState] = createStore({
  firstName: "John",
  lastName: "Miller",
});

setState({ firstName: "Johnny", middleName: "Lee" });
// ({ firstName: 'Johnny', middleName: 'Lee', lastName: 'Miller' })

setState((state) => ({ preferredName: state.firstName, lastName: "Milner" }));
// ({ firstName: 'Johnny', preferredName: 'Johnny', middleName: 'Lee', lastName: 'Milner' })

setter 函数还支持路径语法,以便我们进行嵌套更新。它支持的路径包括 key arrays、object ranges 和 filter 函数。 setState 还支持嵌套设置,你可以在其中指明要修改的路径。在嵌套的情况下,要更新的状态可能是非对象值。对象仍然合并,但其他值(包括数组)将会被替换。

const [state, setState] = createStore({
  counter: 2,
  list: [
    { id: 23, title: 'Birds' }
    { id: 27, title: 'Fish' }
  ]
});

setState('counter', c => c + 1);
setState('list', l => [...l, {id: 43, title: 'Marsupials'}]);
setState('list', 2, 'read', true);
// {
//   counter: 3,
//   list: [
//     { id: 23, title: 'Birds' }
//     { id: 27, title: 'Fish' }
//     { id: 43, title: 'Marsupials', read: true }
//   ]
// }

路径可以是 string keys、array 的 keys、迭代对象({from、to、by})或 filter 函数。这为描述状态变化提供了令人难以置信的表达能力。

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 }
//   ]
// }

修改后同样each只在addTodo时调用一次

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);
};

Store修改

修改方式好多,看得眼花= =

与Signal相比,使用Store适用范围更广。Solid提供了一个受Immer启发的produce store修饰符,它可以让你在setStore调用中改变Store对象的可写代理版本,在不放弃控制的情况下允许小范围的突变。