# Solid.js基础
[toc]
其实都在文档 (opens new window)。只不过看过之后写一点自己的理解,毕竟官方的例子比较少。
# 1、响应式基础
createSignal、createMemo、createEffect
语法都和React的差不多,jsx绑定也都是一样的。
import { createEffect, createMemo, createSignal, Show } from 'solid-js';
export default function Hello() {
const [count, setCount] = createSignal(0);
createEffect(() => {
// 副作用
console.log('count = ', count());
});
const handleClick = (e: Event) => {
setCount(count() + 1);
};
const tripleCount = createMemo(() => count() * 3);
return (
<div>
<p>{count()}</p>
<p>tripleCount: {tripleCount()}</p>
<p>
<button onClick={handleClick}>add 1</button>
</p>
</div>
);
}
# 2、流程控制
# 2.1 条件判断
Show
import { createSignal, Show } from 'solid-js';
<Show
when={loggedIn()}
fallback={() => <button onClick={toggle}>Log in</button>}
>
<button onClick={toggle}>Log out</button>
</Show>
Switch, Match
import { createSignal, Show, Switch, Match } from "solid-js";
<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>
# 2.2 列表渲染For、Index
<For>
组件是遍历任何非原始值数组的最佳方式。
<For>
的回调函数是非跟踪的,并传递数据项和索引 Signal。index
是一个 Signal,因此它可以在移动行时独立更新.
<For>
的数据项不是一个Signal。 所以针对数据项的修改往往就伴随这Dom的重新创建。
<index>
的数据项是一个Signal, 索引是固定的,索引是列表的实际键。所以针对数据项的修改会单独更新,更加高效。
看下面的例子
// @ts-nocheck
import { createSignal, For, Index } from 'solid-js';
export default function ForS() {
const [list, setList] = createSignal([
{
name: 'a',
id: '1'
},
{
name: 'b',
id: '2'
}
]);
return (
<div>
{/* 每次触发Input的change都会重新渲染一个input */}
<For each={list()}>
{(item, index) => {
return (
<input
type='text'
value={item.name}
onChange={e => {
setList(list => {
return list.map((mapItem, mapIndex) => {
if (mapIndex === index()) {
return { ...mapItem, name: e.target.value };
}
return mapItem;
});
});
}}
/>
);
}}
</For>
<hr />
{/* 每次触发Input的change不会重新渲染一个input,只会更新input本身 */}
<Index each={list()}>
{(item, index) => {
return (
<input
type='text'
value={item().name}
onChange={e => {
setList(list => {
return list.map((mapItem, mapIndex) => {
if (mapIndex === index) {
return { ...mapItem, name: e.target.value };
}
return mapItem;
});
});
}}
/>
);
}}
</Index>
</div>
);
}
个人总结: 遍历显示一些不会变更的内容时,For和Index都可以,甚至数据源都没必要创建Signal。如果数据项需要变更展示,使用Index。 常规使用For即可。
# 2.3 Dynamic动态组件
<Dynamic>
组件类似于Vue的内置<component>
组件,可以根据属性动态渲染
# 2.4 Portal
Portal组件就是把内容渲染到组件外部。
默认情况下,它的元素将在 document.body
下的 <div>
中呈现
# 3、生命周期钩子
onMount
组件被挂载,可以访问Dom
onCleanup
组件被销毁前的钩子
const resizeHandler = () => { }
onMount(() => {
window.addEventListener('resize', resizeHandler)
});
onCleanup(() => {
window.removeEventListener('resize', resizeHandler)
})
# 4、绑定
事件,内链样式,class,Ref DOM获取和转发。
# 5、指令
Solid 通过 use:
命名空间支持自定义指令。但这只是 ref
一个有用的语法糖,类似于原生的绑定,并且可以在同一个元素上有多个绑定而不会发生冲突。这可以让我们更好地利用可重用 DOM 元素行为。
// @ts-nocheck
import { createSignal, Show, onCleanup, onMount } from 'solid-js';
function onClickOutside(el: HTMLElement, accessor: Function) {
console.debug(el, accessor);
const onClick = (e: Event) => {
if (!el.contains(e.target)) {
accessor()?.();
}
};
document.body.addEventListener('click', onClick);
onCleanup(() => {
console.debug('onCleanup');
document.body.removeEventListener('click', onClick);
});
}
function makeRed(el: HTMLElement, accessor: Function) {
console.debug('makeRed');
el.style.color = 'red';
}
function makeColor(el: HTMLElement, accessor: Function) {
console.debug(accessor(), 'makeColor');
el.style.color = accessor() || 'red';
}
function click(el: HTMLElement, accessor: Function) {
const onClick = e => {
accessor()(e);
};
el.addEventListener('click', onClick);
onCleanup(() => {
el.removeEventListener('click', onClick);
});
}
/* 自定义指令 */
export default function directive() {
const [show, setShow] = createSignal(false);
return (
<div>
<Show when={show()} fallback={<button onClick={() => setShow(true)}>show</button>}>
<div
use:onClickOutside={() => setShow(false)}
class='modal'
style={{ border: '1px solid #ccc' }}
>
My Modal
</div>
</Show>
<div use:makeRed={() => {}}>red</div>
<div use:makeColor={'green'}>green</div>
<button use:click={e => console.log('click')}>click</button>
</div>
);
}
# 6、props
默认属性
// @ts-nocheck
import { createSignal, mergeProps, splitProps } from 'solid-js';
interface Props {
name?: string;
age?: number;
}
export default function Person(props: Props) {
// 合并属性用于设置默认值
const mergedProps = mergeProps({ name: '匿名', age: 0 }, props);
// 分离属性用于精准传递子组件
const [selfProps, otherProps] = splitProps(mergedProps, ['name', 'age']);
return (
<ul style={{ width: '200px', border: '1px solid #aaa' }}>
<li>
<h4>个人信息</h4>
</li>
<li>name: {selfProps.name}</li>
<li>age: {selfProps.age}</li>
<li>
other:
<pre>{JSON.stringify(otherProps, null, 2)}</pre>
</li>
</ul>
);
}
// @ts-nocheck
import { createSignal, onMount } from 'solid-js';
import Person from '~/components/Person.tsx';
export default function Props() {
const [name, setName] = createSignal('');
onMount(() => {
setTimeout(() => {
setName('James');
}, 1000);
});
return (
<div>
<Person name='Wade' age='27'></Person>
<Person></Person>
<Person name={name()} style="color: red" title="Person"></Person>
</div>
);
}
# 7、children 函数
主要用于获取所有后代的DOM
import { children, createEffect, createSignal, For } from 'solid-js';
function ColoredList(props: any) {
// const c = children(() => props.children);
const c = children(() => props.children);
// c() 返回的是children的子元素列表
createEffect(() => {
(c() as Array<HTMLElement>).forEach(el => {
el.style.color = props.color;
});
});
return <>{c()}</>;
}
export default function ChildrenX() {
const [color, setColor] = createSignal('red');
return (
<div>
<ColoredList color={color()}>
<For each={['A', 'B', 'C']}>
{(item, index) => (
<p>
{item}-{index()}
</p>
)}
</For>
</ColoredList>
<input
type='color'
value={color()}
onInput={e => {
setColor(e.currentTarget.value);
}}
/>
</div>
);
}
# 8、内嵌响应式
就是可以把Signal 放到数据结构内部, 实现更细颗粒度的更新。
说白了你可以把每一个DOM节点都绑定一个signal。更新肯定快。
# 9、Store
Store 是 Solid 用于处理嵌套响应式。只有单层的时候,使用Signal即可。
// @ts-nocheck
import { createSignal, Accessor, Setter } from 'solid-js';
import { createStore } from 'solid-js/store';
interface Todo {
id: number;
text: string;
completed: boolean;
}
export default function InnerSignal() {
let input: HTMLInputElement;
let todoId = 0;
const [store, setStore] = createStore<{ todos: Todo[] }>({ todos: [] });
const handleAdd = () => {
if (!input.value.trim()) return;
// setTodos([...todos(), { id: todoId++, text: input.value, completed, setCompleted }]);
setStore('todos', todos => [...todos, { id: ++todoId, text: input.value, completed: false }]);
input.value = '';
};
const handleChange = id => {
setStore(
'todos',
t => t.id === id,
'completed',
completed => !completed
);
};
return (
<div>
<input type='text' ref={input} />
<button onClick={handleAdd}>add todo</button>
<ul>
<For each={store.todos}>
{(todo: Todo) => {
console.log(todo.id);
return (
<li>
<input type='checkbox' value={todo.completed} onChange={[handleChange, todo.id]} />
<span style={{ 'text-decoration': todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
);
}}
</For>
</ul>
</div>
);
}
function Demo1() {
const [person, setPerson] = createStore({
name: {
type: 'string',
value: '222'
},
age: {
type: 'number',
value: 10
}
});
const handleClick = () => {
// 最基础的 setter 函数用法会接收一个对象,该对象的属性将与当前状态合并
setPerson({
name: {
value: '333'
}
});
// or
// setter 函数还支持路径语法,以便我们进行嵌套更新
// setPerson('name', 'value', '1111');
};
return (
<div>
<p>
nameValue: {person.name.value}, ageValue: {person.age.value}
</p>
<button onClick={handleClick}>change</button>
</div>
);
}
# 10 配合product 修改Store
function Demo2() {
type Item = { text: string };
const [store, setStore] = createStore<{ list: Item[] }>({
list: []
});
let code = 65;
const add = () => {
setStore(
'list',
produce(list => {
list.push({ text: String.fromCharCode(code++) });
})
);
};
const change = () => {
setStore(
'list',
1,
produce(item => {
item.text = 'B';
})
);
};
const handleDelete = (index: number) => {
console.log(index);
setStore(
'list',
produce(list => {
list.splice(index, 1);
})
);
};
return (
<>
<hr />
<ol>
<li>
<button onClick={add}>add</button>
<button onClick={change}>change</button>
</li>
<Index each={store.list}>
{(item: Accessor<Item>, index) => {
return (
<li>
{item().text}
<button onClick={[handleDelete, index]}>del</button>
</li>
);
}}
</Index>
</ol>
<hr />
</>
);
}
# 11、Context
相当于是全局上下文
import { createSignal, createContext, useContext, mergeProps, Accessor } from 'solid-js';
type Theme = 'dark' | 'light';
type ThemeSignal = [Accessor<Theme>, (a: Theme) => void];
// 提供 全局上下文内容 =======start=====
const ThemeContext = createContext<ThemeSignal>();
function ThemeProvider(props: { theme: Theme; children: any }) {
const mProps = mergeProps({ theme: 'light' }, props);
const [theme, setTheme] = createSignal<Theme>(mProps.theme);
const store: ThemeSignal = [
theme,
(a: Theme) => {
setTheme(a);
}
];
return <ThemeContext.Provider value={store}>{mProps.children}</ThemeContext.Provider>;
}
// 导出 useTheme 给子组件使用
export function useTheme() {
return useContext(ThemeContext);
}
// =======end=====
function Child() {
const [theme, changeTheme] = useTheme() as ThemeSignal;
const change = (theme: Theme) => {
changeTheme(theme);
};
return (
<div>
<p>{theme()}</p>
<h1>child</h1>
<button onclick={[change, 'dark']}>dark</button>
<button onclick={[change, 'light']}>light</button>
</div>
);
}
function Child2() {
const [theme, changeTheme] = useTheme() as ThemeSignal;
return (
<div>
<p>{theme()}</p>
<h1>child2</h1>
</div>
);
}
export default function ContextX() {
return (
<ThemeProvider theme='light'>
<div></div>
<Child></Child>
<hr />
<Child2></Child2>
</ThemeProvider>
);
}
# 12、通过createRoot使用全局的Signal
可以用于替代Context,都可以使用, 效果是一样的。
import { createMemo, createRoot, createSignal } from 'solid-js';
interface Cache {
firstName?: string;
lastName?: string;
age?: number;
}
function createCache() {
const [cache, setCache] = createSignal<Cache>({
firstName: 'Lebern',
lastName: 'James',
age: 20
});
const changeCache = (newCache: Cache) => {
setCache(cache => {
return {
...cache,
...newCache
};
});
};
const fullName = createMemo(() => cache().firstName + ' ' + cache().lastName);
return {
cache,
changeCache,
fullName
};
}
const cacheIns = createRoot(createCache);
function Demo1() {
const { cache, changeCache, fullName } = cacheIns;
const handleClick = () => {
changeCache({ firstName: 'xxxx' });
};
return (
<div>
<h1>demo1</h1>
<p>{cache().age}</p>
<p>{fullName()}</p>
<button onClick={handleClick}>change</button>
</div>
);
}
function Demo2() {
const { cache, changeCache, fullName } = cacheIns;
return (
<div>
<h1>demo2</h1>
<p>{cache().age}</p>
<p>{fullName()}</p>
</div>
);
}
export default function ContextNo() {
return (
<div>
<Demo1></Demo1>
<hr />
<Demo2></Demo2>
</div>
);
}
# 13、batch 手动批处理更新
批处理 点击后, 函数C只会被执行一次
import { batch, createSignal } from 'solid-js';
export default function SimpleDemo() {
const [A, setA] = createSignal('A');
const [B, setB] = createSignal('B');
const C = () => {
console.log('rebuild C');
return A() + ':' + B();
};
const handleC = () => {
batch(() => {
setA(A() + 'n');
setB(B() + '!');
});
};
return (
<div>
<p>C: {C()}</p>
<button onClick={handleC}>change</button>
</div>
);
}
# 14、untrack 解除依赖被收集
// @ts-nocheck
import { createEffect, createSignal, untrack } from 'solid-js';
export default function UntrackX() {
const [A, setA] = createSignal(1);
const [B, setB] = createSignal(1);
createEffect(() => {
// untrack之后B的更新不会被副作用收集
console.log(A(), untrack(B));
});
createEffect(() => {
console.log(B());
});
const addA = () => {
setA(A() + 1);
};
const addB = () => {
setB(B() + 1);
};
return (
<div>
<p>
A: {A()}, B: {B()}
</p>
<button onClick={addA}>increment A</button>
<button onClick={addB}>increment B</button>
</div>
);
}
# 15、响应性 on
指定哪些依赖被收集。
设置显式依赖。这主要用来更明确地简洁地声明跟踪哪些信号。然而,它也允许计算不立即执行而只在第一次更改时运行。可以使用defer
选项启用此功能。
import { createEffect, createSignal, untrack, on } from 'solid-js';
export default function UntrackX() {
const [A, setA] = createSignal(1);
const [B, setB] = createSignal(1);
createEffect(
on(
A,
a => {
console.log(a, B());
},
{ defer: true }
)
);
const addA = () => {
setA(A() + 1);
};
const addB = () => {
setB(B() + 1);
};
return (
<div>
<p>
A: {A()}, B: {B()}
</p>
<button onClick={addA}>increment A</button>
<button onClick={addB}>increment B</button>
</div>
);
}
# 16、lazy懒加载
const Comp1 = lazy(() => import('~/components/Comp1'));