什么是 Solid?
- 一个 JavaScript 框架
- 局部更新,只更新改动的内容
- 当依赖的数据发生变化时更新
- 类似 react 语法
创建 Solid 应用程序
- 前提: 安装 Node.js 或 Deno
- 创建:
- JS版:pnpm dlx degit solidjs/templates/js my-app
- TS版:pnpm dlx degit solidjs/templates/ts my-app
- 导航:cd my-app
- 安装:pnpm install
- 运行:pnpm run dev
反应性
- 含义:系统自动响应数据或状态变化的能力,确保应用程序与底层数据保持同步
- 示例:
1 | function Counter() { |
- 可以看到跟 react hook 的相似处
- createSignal() ==> useState()
- count() ==> count
反应原理
信号 Signals
- 反应式系统的核心元素,在数据管理和系统响应能力中发挥着重要作用。
- 由两个主要功能组成:
- getter:访问存储在组件内 signal 的数据,用于获取 signal 当前值的函数。
- setter:触发反应式更新,用于修改 signal 的函数。
- 通过使用 getter 和 setter 来负责存储和管理数据,以及触发整个系统的更新。
- createSignal:该函数执行两个主要任务
- 初始值
- 返回一个包含两个元素的数组:getter和setter函数
- 示例:createSignal的原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14function createSignal(initialValue) {
// 这个阶段不存在反应性
let value = initialValue;
function getter() {
return value;
}
function setter(newValue) {
value = newValue;
}
return [getter, setter];
}
订阅者 Subscribers
- 反应式系统的核心元素。
- 负责跟踪信号的变化并相应地更新系统。
- 是自动响应程序,使系统与最新的数据更改保持同步。
- 订阅者基于两个主要行为:
- Observation:订阅者的核心是观察信号。能及时捕抓正在跟踪的信号变化
- Response:信号改变,订阅者会收到通知。触发响应信号的改变
状态管理
- 作用:是处理和处理影响 Web 应用程序的行为和表示的数据的过程。涉及存储和更新数据
- 实现方式:通过信号和订阅者来处理。信号用于存储和更新数据,订阅者用于响应数据的更改
- 三要素:
- State:用于确定要向用户显示的内容的数据
- View:状态对用户的只管表示
- Actions:修改状态的事件
- 这些元素协同工作创建”单向数据流”。当修改状态行为,视图将更新当前状态并展示。
管理基本状态
- 状态是应用程序的实施来源,用于确定要向用户显示的内容。
- 状态由信号表示,创建和使用过程与信号一样
跟踪更改
- 通过订阅者来监控数据的任何更新,并作出响应
- 响应式原句可用于创建订阅者
- 注意:要跟踪信号,必须在订阅者的范围内访问。否则不会触发
- 示例:
1
2
3
4
5const [count, setCount] = createSignal(0)
createEffect(() => {
console.log(count())
})
setCount(1)
在UI中呈现状态
- 使用JSX语法
派生信号
- 基于现有state值计算新的state值
- 注意:只可计算简单计算,而且每次使用都会重新计算
- 频繁使用或高昂计算可以使用createMemo替代,因为memo仅在值更新时运行一次,并且可以多次访问
- 示例:
1
2const [count, setCount] = createSignal(0);
const doubleCount = () => count() * 2 // 这个就是派生信号
提升状态
- 含义:将state提升到一个共同的祖先组件
- 做法:就是在一个父组件定义state,然后这个父组件调用使用这个state的多个组件
- 在组件之间共享 state 时,可以通过props。
- 注意:
- 从父组件向下传递的 props 值是只读的。
- 从父组件向下传递 setter 函数,子组件就可以间接修改父组件的状态。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import { createSignal, createEffect, createMemo } from "solid-js";
function App() {
const [count, setCount] = createSignal(0);
const [doubleCount, setDoubleCount] = createSignal(0);
const squaredCount = createMemo(() => count() * count());
createEffect(() => {
setDoubleCount(count() * 2);
});
return (
<>
{/*父组件传递setter函数 */}
<Counter count={count()} setCount={setCount} />
<DisplayCounts
count={count()}
doubleCount={doubleCount()}
squaredCount={squaredCount()}
/>
</>
);
}
function Counter(props) {
const increment = () => {
// 子组件可以使用setter函数修改
props.setCount((prev) => prev + 1);
};
return <button onClick={increment}>Increment</button>;
}
function DisplayCounts(props) {
return (
<div>
<div>Current count: {props.count}</div>
<div>Doubled count: {props.doubleCount}</div>
<div>Squared count: {props.squaredCount}</div>
</div>
);
}
export default App;
管理复杂状态
- 使用store
同步 vs 异步
同步反应性
- 默认模式,系统以直接和线性的方式相应变化。
- 当信号发生变化时,任何相应的订阅者都会立即以有序的方式更新。
异步反应性
- 系统以延迟或非线性方式响应变化。
- 当信号变化时,相应的订阅者不会立即更新。系统会等待特定事件或任务完成在更新
- 这在订阅者依赖多个信号的情况下非常重要,避免信号更新不同步导致数据不一致
- 注意:当存在异步响应性时,延迟非常重要。batch 可用于延迟更新。
关键概念(简单总结)
- 信号负责存储和管理数据。
- 由于 getter 和 setter,信号既可读又可写。
- 订阅者是自动响应者,可以跟踪信号的变化并相应地更新系统。
- Signals 和 subscribers 协同工作,以确保系统与最新的数据更改保持同步。
- 反应式系统建立在数据驱动反应性原则之上。意味着系统的反应性是由它所基于的数据驱动。
- 反应式系统可以是同步的,也可以是异步的。
组件
- 基本跟 react 一样,省略
classList
- 作用:多个类应用于同一个元素时使用,处理多个条件类更有效
- 可以传递字符串或对象,其中 key 为类名,值为布尔表达式,当值为 false 删除
- 示例:
1 | const [current, setCurrent] = createSignal('foo') |
- 注意:
- 与 class 一起用时,都是动态的情况下,应用 class 删除 classList
- 解决方式:class 设置为静态或动态的计算值,然后放在 classList 前
- classList 是一个伪属性,不适用于
<div {...props} />
或<Dynamic>
中的 prop 跨页
- 与 class 一起用时,都是动态的情况下,应用 class 删除 classList
事件处理程序
- on:__: 将事件侦听器添加到元素,这也称为本机事件
- on__: 向 document 添加事件侦听器并将其调度到元素,这称为委托事件
- 注意:委托事件不区分大小写,本机事件区分大小写
绑定事件
- 避免使用 js 的 bind 方法和添加额外闭包的开销
- 以数组作为事件处理程序传递,数组第二项作为处理程序的第一个参数
- 示例:
1 | const handler = (data, event) => {} |
动态处理程序
- 事件处理程序不构成响应式系统的一部分。
- 事件不会动态更新,并且绑定不是反应性的
- 如果要将处理程序当作 signal 传递,将不会响应
- 示例:
<div onClick={() => props.handleClick?.()} />
- 示例:
活动委托
- 通过 on__形式
- 支持的事件:看下方的委托事件列表
- 如果需要将事件侦听器附加到事件委托不支持的元素,如自定义元素,使用 on:__
注意
- 事件委托是为了通过 JSX 树而不是 DOM 树进行事件传播而设计的
- 委托事件侦听器按事件类型添加一次,并处理该类型的所有未来事件。 这意味着,即使删除了添加委托事件侦听器的元素及其处理程序,委托事件侦听器仍保持活动状态。
- 例:如果 div 监听 mousemove 并在稍后被删除,则事件仍将被分派给 document,以防其他元素也在监听鼠标移动。
- 对于不经常发生的事件使用on:__
- event.stopPropagation()未按预期工作,因为事件附加到 document 而不是 element。
- 使用 on:__ 解决
- onChange 和 onInput 事件根据其本机行为工作:
- onInput 将在值更改后立即触发
- 在
<input>
字段中,onChange 仅在字段失去焦点后触发。
委托事件列表
- beforeinput
- click
- dbclick
- contextmenu
- focusin
- focusout
- input
- keydown
- keyup
- mousedown
- mousemove
- mouseout
- mouseover
- mouseup
- pointerdown
- pointermove
- pointerout
- pointerover
- poinyerup
- touchend
- touchmove
- touchstart
Props
- 将 state 从父组件传递到子组件的方法
- 使用方法跟 react 一样
mergeProps
- 一个实用函数
- 作用:把多个潜在的反应性对象合并在一起。行为类似于 Object.assign 但将保留正在合并的属性的响应性。
- 合并 props 时,如果 props 没有该值,则将使用第一个对象的值。
- 示例:
1 | import { mergeProps } from 'solid-js' |
解构 props
- 在Solid中,不建议使用解构 props,会破坏响应性。正确使用方式看示例
- 示例:
const name = () => props.name
splitProps
- 实用函数,将单个 props 对象拆分成多组 props,同时保留其响应性
- 含义:定义一个或多个 key 数组,并能提取到单独的 props 对象中,同时保留各个属性的响应性。
- 返回:一个与每组键相关的 props 对象数组,以及一个包含任何剩余键的附加 props 对象。
- 用途:当 props 传递给子组件时,使用 splitProps 将 props 分成多个组,然后将每个组传递给相应的子组件:
- 示例:
1 | import { splitProps } from 'solid-js' |
将 props 传递给 children
- 多数情况下,直接使用props。
- 避免重复创建子组件或元素
- 示例:
1 | import { children } from 'solid-js' |
螺旋桨钻孔 Prop Drilling
- 含义:用于描述将 prop 传递多个组件的过程
- 由于 Solid 中的组件不拥有 state,因此不需要 props 在组件之间传递 state,但可以使用 props。因此,有时可能需要通过多层组件传递 props。
- 多层级传递的缺点:props 难以管理,组件收到它们不需要的 props、不必要的重渲染和麻烦的重构
避免多层级传递props
- 常见解决方案:使用 Context 将 state 传递给深度嵌套的组件
条件渲染 Show标签
- 属性值
- when:判断是否渲染 children
- fallback:当结果为 false,展示失败时的渲染 children
- 有多个条件需要处理,采用嵌套的方式
- 示例:
1 | import { Show } from 'solid-js' |
Switch 标签和 Match 标签
- 处理多个条件,类似于 switch/case
- 属性值:
- fallback:当所有条件都不符合时渲染
- when:判断是否渲染这个Match
- 示例:
1 | import { Switch, Match } from 'solid-js' |
Dynamic 标签
- 允许根据数据动态渲染组件
- 比
<Switch>
和<Match>
简洁 - 属性值:
- component:传递一个动态的事件
- 示例:
1 | import { createSignal, For } from "solid-js" |
列表呈现
- 渲染列表
For 标签
- 循环组件,根据数组或对象的内容呈现元素
- 使用场景:与复杂的数据结构一起使用,如列表的顺序和长度会频繁更改的情况使用
- 唯一的属性:
- each:指定要循环访问的数据收集,接受一个数组或者使用 Object.entries()或 Object.value 处理的对象
- 标签之间,组件需要有一个回调函数,类似于 map
- item:表示正在渲染的数据收集中的当前项
- index:当前项在数据中的索引,index 是一个信号,必须使用函数调用才能检索
- 示例:
1 | <For each={data()}> |
Index 标签
- 循环组件,与
<For>
类似 - 使用场景:列表顺序和长度保持稳定,但内容会频繁更改时使用
<Index>
更关注元素在数组中的索引,所以回调中 index 固定- item 是信号,必须使用函数调用才能检索
- 示例:
1 | import { Index } from 'solid-js' |
Index 标签 vs For 标签
- For:
- 当列表的顺序和长度可能频繁更改时使用。
- 当列表值更改时,将刷新整个列表。但是,如果数据发生变化,如元素位置移动,只会修改列表中元素的索引,而不是重新渲染整个列表
- 最佳使用场景:在不需要信号、嵌套循环或动态列表的情况下
- Index:
- 当列表的顺序和长度保持稳定,但内容可能会频繁更改时使用。
- 当列表值更改时,只会更新指定索引处的内容,而列表的其余部分保持不变。
- 最佳使用场景:处理信号,JavaScript 基元(如字符串和数字)或 input 字段时
Portal 标签
<Portal>
通过将元素放在文档中的其他位置,将元素引入文档流中- 默认情况下,嵌套的内容将呈现并放置在正文末尾
- 可以通过 prop 传递给
<Protal>
来更改当前内容的挂载点。prop 接受一个 DOM 节点 - 使用场景:
- 弹窗
- 父元素溢出等行为影响到
<Protal>
的内容时,可以将其放在父元素外 - 元素需要在文档流外进行渲染时,堆叠内容和 z-index影响到视图
- 示例:
1 | import { Portal } from 'solid-js/web' |
- 注意:
<Protal>
将呈现包装,除非针对 document.head- 事件会根据组件层次结构而不是元素层次结构
- 默认情况下,子项包裹在 div。如果应用在 SVG 上,必须使用 isSVG 属性来避免子项包裹在 div 中
ErrorBoundary 标签
- 含义:用于创建错误边界。它会捕获在渲染或更新其子项期间发生的任何错误
- 注意: 在渲染过程之外发生的错误(如在事件处理程序中或在 setTimeout 之后)不会被捕获
- 属性值:
- fallback:可用于在发生错误时显示用户友好的错误消息或通知。
- 如果 fallback 是回调函数:
- error:error 对象
- reset:重新渲染其子项并重置错误状态,为用户提供一种从错误中恢复的方法。
- 如果 fallback 是回调函数:
- fallback:可用于在发生错误时显示用户友好的错误消息或通知。
Effects
- 管理副作用,当它们所依赖的信号发生变化时触发的函数
- 场景:DOM、数据获取和订阅
- createEffect: 传入一个函数,该函数立即调用其中的函数
- 示例:
1
2
3function createEffect(fn){
fn()
}
- 示例:
管理依赖项
- Solid 会自动跟踪效果的依赖关系,因此无需手动指定依赖项。
- signals、变量、props、context 或任何其他响应式值,任何一个更改,都会重新运行
- 初始化后,将运行一次,无论它是否有任何依赖项。当依赖性更改才会再次运行
订阅信号
- 当 Effect 设置为观察信号事,它会创建对该信号的订阅
- 一个 Effect 具有观察多个信号的能力,多个 Effect 可以跟踪单个信号
- 注意:当信号更新时,会按顺序通知所有订阅者,虽然可以保证 effect 在信号更新时运行,但执行可能不是即时的。这意味 Effect 的执行下顺序无法保证
嵌套效果
- 允许每个效果单独跟踪自己的依赖项,而不会影响它嵌套在其中的效果
- 执行顺序很重要。内部效果不会影响外部效果。
- 内部效果器中访问的信号不会注册为外部效果器的依赖项。
- 示例:
1 | import { createSignal, createEffect } from 'solid-js' |
生命周期函数
onMount
- 只运行一次副作用,类似于 Effect,但不跟踪依赖项。
- 一旦组件初始化,回调将被执行且不会再次执行
- 比较适合只调用一次的 API
- 示例:
1 | import { onMount } from 'solid-js' |
onCleanup
- 在不需要任务时清理任务。
- 将在组件卸载时运行,并删除 Effect 具有的所有订阅
- 可以避免内存泄漏
- 适合清除定时器
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { onCleanup } from 'solid-js'
function App() {
const [count, setCount] = createSignal(0)
const timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000)
onCleanup(() => {
clearInterval(timer)
})
return <div>Count: {count()}</div>
}
memo
- 是一种响应式值,可用于记忆派生状态或昂贵的计算
- 仅对其依赖项的每次更改执行一次
- 示例:
1 | import { createMemo, createSignal } from 'solid-js' |
Memo vs Effect
Memo | Effect | |
---|---|---|
返回值 | 返回计算或派生状态结果的 getter | 不返回,但执行代码块以响应更改 |
缓存结果 | 是 | 不 |
行为 | 参数应该是干净的,没有反应式的副作用 | 可能会导致 UI 更新或数据获取等副作用 |
依赖项跟踪 | 是 | 是 |
示例用例 | 转换数据结构、计算聚合值、派生状态或其他昂贵的计算 | UI 更新、网络请求或外部集 |
实践
纯函数
- 不会引起任何副作用的函数。意味着函数的输出取决于其输入
- 在memo中引入Effect,会导致无限循环,应改用createEffect()
将逻辑保留在memo中
- 当派生state,使用memo
Context
何时使用?
- 需要共享state的大型组件树时,可以避免prop钻探
- 共享全局数据或应用程序组件树的多个组件定期访问的信息
创建和使用
- 在全局中创建/content/create.js
1
2import { createContext } from "solid-js"
const MyContext = createContext() - 在全局中创建/context/component.jsx
1
2
3
4
5
6
7
8import { MyContext } from "./create.js"
export function Provider (props) {
return (
<MyContext.Provider>
{props.children}
</MyContext.Provider>
)
} - 传递单个值,直接在MyContext.Provider传递
- 示例:
<MyContext.Provider value="new value"></MyContext.Provider>
- 示例:
- 传递多个值(如数组或Object)使用store
自定义上下文实用程序
- 当app包含多个上下文对象时,很难追踪正在使用的上下文对象。解决方式:创建自定义app
在app的不同区域访问Provider,导入组件并封装组件树
- 结合·创建和使用·的代码一起看,步骤相似
- 示例:
1
2
3
4
5
6
7
8
9
10import { CounterProvider } from "./counterProvider";
export function App() {
return (
<CounterProvider count={1}>
<h1>Welcome to Counter</h1>
<NestedComponents />
</CounterProvider>
);
}
创建自定义实用程序来访问上下文
- 可以更轻松访问所需值,而不是在使用它的组件上导入和传入上下文对象
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 封装
export function useCounter() {
return useContext(CounterContext);
}
// 应用
import { useCounter } from "./counter";
export function CounterProvider(props) {
const count = useCounter();
return (
<>
<div>{count()}</div>
</>
);
}
更新上下文
- 信号提供一种方式使用上下文去同步和管理组件之间共享的数据方法。
- 是一种跨组件管理状态的方法,中间不必通过中间元素传递props
- 可以将信号直接传递给 Provider 组件的 value属性,对信号的任何更改都将反映在所有使用上下文的组件中。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54// 创建Context.jsx
import { createSignal, useContext } from "solid-js";
export function CounterProvider(props) {
const [count, setCount] = createSignal(props.initialCount || 0);
const counter = [
count,
{
increment() {
setCount(prev => prev + 1);
},
decrement() {
setCount(prev => prev - 1);
}
}
];
return (
<CounterContext.Provider value={counter}>
{props.children}
</CounterContext.Provider>
);
}
export function useCounter() { return useContext(CounterContext); }
// 应用到全局App.jsx
import { CounterProvider } from "./Context";
import { Child } from "./Child";
export function App() {
return (
<CounterProvider count={1}>
<h1>Welcome to Counter App</h1>
<Child />
</CounterProvider>
)
}
// 在这个组件单独使用上下文Child.tsx
import { useCounter } from "./Context";
export function Child(props) {
const [count, { increment, decrement }] = useCounter();
return (
<>
<div>{count()}</div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};
使用上下文进行调试
- createContext接受可选的 default 值,如果未提供,则可能会返回undefined
- 解决TS报错:
- 1.指定默认值
- 2.使用自定义app处理好后在使用(错误在自定义app中处理,使用处理好后的自定义app)
createContext 和 useContext 常见问题
- 如果没有将默认值传递给 createContext,则 useContext 可能会返回 undefined。
- 解决TS报错:将 useContext 的所有使用包装在一个函数中,如果上下文未定义,该函数将显式地抛出一个有用的错误。
- 示例: 跟createContext的处理方式一样
1
2
3
4
5
6
7function useCounterContext() {
const context = useContext(CounterContext)
if (!context) {
throw new Error("can't find CounterContext")
}
return context
}
仓库 Store
- store 可以生成一组反应式信号,每个信号对应于一个特定的属性,这在处理复杂状态时可能很有用
创建Store
- 可以管理多种数据类型,包括对象、数组、字符串和数字
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import { createStore } from "solid-js/store"
// Initialize store
const [store, setStore] = createStore({
userCount: 3,
users: [
{
id: 0,
username: "felix909",
location: "England",
loggedIn: false,
},
{
id: 1,
username: "tracy634",
location: "Canada",
loggedIn: true,
},
{
id: 2,
username: "johny123",
location: "India",
loggedIn: true,
},
],
})
访问Store
- 语句:store.xxx(xxx指仓库的属性)
- Store初始状态不会跟踪更改,此时去更改会报错。这些信号是惰性创建的,这意味着只有在响应式上下文中访问时才会形成(例如在组件函数、计算属性或效果的 return 语句中)
- 创建完后放在createEffect,会建立起跟踪,就可以访问到。
修改Store值
- 语句:setStore(key, newValue),会自动更新的
- 也可以使用嵌套store来设置属性
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const [store, setStore] = createStore({
userCount: 3,
users: [ ... ],
})
const [users, setUsers] = createStore(store.users)
setUsers((currentUsers) => [
...currentUsers,
{
id: 3,
username: "michael584",
location: "Nigeria",
loggedIn: false,
},
])
Path语法灵活性
- 初始参数用于指定导致要修改的目标值的键,而最后一个参数提供新值。
- 不仅可以使用字符串键,还可以选择使用键数组
- 示例:setState([1,3], user => user.loggedln, false) // 把users数组中索引为1和3的user.loggedln状态改为false
修改数组中的值
- path语法不依赖于发现单个索引,而是引入几种强大的数组作技术。
追加新值
- 要将新元素追加到 store 中的数组,请指定目标数组并将索引设置为所需位置。
- 例如: 如果要将新元素追加到数组的末尾,则可以将索引设置为:array.length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18setStore("users", (otherUsers) => [
...otherUsers,
{
id: 3,
username: "michael584",
location: "Nigeria",
loggedIn: false,
},
])
// 变成这样
setStore("users", store.users.length, {
id: 3,
username: "michael584",
location: "Nigeria",
loggedIn: false,
})
- 例如: 如果要将新元素追加到数组的末尾,则可以将索引设置为:array.length
修改多个元素
- 使用路径语法,可以定位数组的元素子集,或对象的属性,通过指定数组或索引范围。
- 如果store.users是对象数组,可以一次设置多个索引对应的属性
- 示例:setStore(“users”, [2, 7, 10], “loggedIn”, false)
- 如果store.users是对象将对象名映射到对象,可以一次设置多个用户的属性
- 示例:setStore(“users”, [“me”, “you”], “loggedIn”, false)
- 特别是对于数组,可以通过from 和 to指定索引范围(包括from 和to值)
- 示例:setStore(“users”, {from: 1, to: store.users.length - 1}, “loggedIn”, false) // 除了索引0都改变
- 在 range 对象中包含一个键来指定步长
- 示例:setStore(“users”, { from: 0, to: store.users.length - 1, by: 2 }, “loggedIn”, false) 每2的倍数改变
动态值分配
- 函数接收旧值作为参数,允许您根据现有值计算新值
- 示例:setStore(“users”, 3, “loggedIn” , (loggedIn) => !loggedIn)
筛选值
- 使用函数充当过滤器,该函数接收旧值和索引作为参数
- 除了.startsWith,还可以使用其他数组方法,例如.find以筛选所需的值。
- 示例:setStore(“users”, (user) => user.username.startsWith(“t”), “loggedIn”, false) // username开头为t的改变
修改对象
- 如果新值是对象,则会与现有值进行浅层合并。如果新对象中的值于旧对象有重叠,把旧值改为新值。
- 可以直接对 store 进行更改,而无需展开现有对象的属性。
- 示例:setStore(“users”, 0, { id: 109,})
Store 的实体函数
存储更新 produce
- 提供了一种处理数据的方法,就好像它是可变的 JavaScript 对象一样。
- 还提供了一种同时更改多个属性的方法,无需多次调用。
- 注意:它是专门为处理数组和对象而设计的。其他集合类型与此实用程序不兼容。
- produce vs setStore
- 两者都可用于修改状态,但关键区别在于它们如何处理数据。
- produce:允许使用 State 的临时 Draft,应用更改,然后生成新的 Store 不可变版本。
- setStore:提供了一种更直接的方式来直接更新 store,而无需创建新版本。
- 示例:
1
2
3
4
5
6
7
8
9
10
11import { produce } from "solid-js/store"
// 修改users的0号元素的值
setStore(
"users",
0,
produce((user) => {
user.username = "newUsername"
user.location = "newLocation"
})
)
数据集成reconcile
- 当需要将新信息合并到现有store时,reconcile可能会很有用。
- reconcile将确定新数据和现有数据之间的差异,并仅在值发生更改时启动更新,从而避免不必要的更新
- 示例:
1
2
3
4
5
6
7
8const { createStore, reconcile } from "solid-js/stores"
const [data, setData] = createStore({
animals: ['cat', 'dog', 'bird', 'gorilla']
})
const newData = getNewData() // eg. contains ['cat', 'dog', 'bird', 'gorilla', 'koala']
setData('animals', reconcile(newData))
提取原始数据unwrap
- 将 store 转换为标准对象的方法
- 示例:
1
2
3
4
5
6
7import { createStore, unwrap } from "solid-js/store"
const [data, setData] = createStore({
animals: ["cat", "dog", "bird", "gorilla"],
})
const rawData = unwrap(data)
Refs
- 以附加到任何元素,用于引用 DOM 元素或组件实例
访问DOM元素
- 不建议通过元素选择器访问 DOM 元素
- 由于 Solid 中的元素可以根据state在 DOM 中添加或删除,因此需要等到元素附加到 DOM 后才能访问它。可以通过使用onMount等到元素附加到 DOM 后再访问它
JSX作为值
- 可以在直接访问 DOM 元素时分配给变量
- 优点:可以多次使用,而不用担心重复
- 缺点:将元素和任何子元素与 JSX 结构的其余部分分开。难阅读
- 示例:
1
2
3
4
5function Component() {
const myElement = <p>My Element</p>
return <div>{myElement}</div>
}
Solid 中的 Refs
- 可以直接在 JSX 模板中访问 DOM 元素,从而保持元素的结构不变
- 先赋值后再添加DOM
- 示例:
1
2
3
4
5
6
7
8
9function Component() {
let myElement;// 先定义一个变量(赋值发生在将元素添加到 DOM 之前的创建时)
// TS声明定义:let myElement!: HTMLDivElement;
return (
<div>
<p ref={myElement}>My Element</p>
</div>
)
}
- 示例:
- 如果在将元素添加到 DOM 之前需要访问该元素,则可以使用以下回调形式:
- 示例:
1
2
3
4
5<p ref={(el) => {
myElement = el // el已经创建但不能添加到DOM
}}>
My Element
</p>
- 示例:
信号也可以用作 refs
- 直接访问元素时有用,但在组件首次呈现时该元素可能不存在,或者可能在某个时候从 DOM 中删除。
- 可以嵌套在条件判断中,条件添加成立才显示
转发refs
- 作用:允许将 ref 从父组件传递到子组件的技术
- 过程:将 ref 传递给子组件,然后将 ref 分配给子组件的元素,子组件获取props拿到该值
- 类似于react中的组件传递值
指令
- 作用:允许将可重用的行为附加到 DOM 元素
- 功能:
- 在一个元素上有多个指令
- 将响应式数据传递给回调
- 本质:具有特定签名的函数:
function directive(element: Element, accessor: () => any): void
- element: 应用指令的DOM元素
- accessor:一个函数,用于访问传递给指令的值
- 指令函数在渲染时调用,但在将元素添加到DOM之前调用
- 用途:
- 创建信号
- 启动Effect
- 添加事件侦听器等
细粒度反应性
- 反应性确保对数据更改的自动响应,无需手动更新用户界面 (UI)。 通过将 UI 元素连接到基础数据,更新变得自动化。 在细粒度的反应式系统中,应用程序现在将能够进行高度针对性和特定的更新。
- Solid vs React
- 在 Solid 中,对需要更改的目标属性进行更新,从而避免更广泛的更新,有时甚至是不必要的更新。
- React 会重新执行整个组件来更改单个 attribute,这可能效率较低。
构建反应式系统
- 示例:
1
2
3
4
5
6
7
8
9function createSignal() {}
function createEffect() {}
const [count, setCount] = createSignal(0);
// 按照观察者模式:signals 将维护订阅者effect的
createEffect(() => {
console.log("The count is " + count());
});
响应式原语
- 在Solid的响应性系统中,有两个关键元素:信号和观察者。
- 响应式功能的基础,也是核心元素:
- Stores:这些代理在后台创建、读取和写入信号
- Memo:类似于Effects,但区别在于它们返回信号并通过缓存优化计算。它们根据效果的行为进行更新,但更适合计算优化。
- resources:基于 memo 的概念,将网络请求的异步性转换为同步性,其中结果嵌入到 signal 中。
- 渲染效果是一种立即启动的定制效果,专为管理渲染过程而设计。
制作系统反应式
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29let currentSubscriber = null // 初始化订阅
function createSignal(initValue) { // 创建信号
let value = initValue
const subscribers = new Set()
function getter() {
if(currentSubscriber) { // 添加订阅者
subscribers.add(currentSubscriber)
}
return value
}
function setter(newValue) {
if(value === initValue) return // 相同不处理
value = newValue // 更新变量
for(const subscribers of subscribes) { // 通知所有订阅者
subscriber()
}
}
return [getter, setter]
}
function createEffect(fn) {
const preSubscriber = currentSubscriber
currentSubscriber = fn
fn() // 注册signals
currentSubscriber = preSubscriber // 一旦函数运行,重置订阅者
}
验证反应式系统
- 可以使用定时器验证每隔n秒是否有响应更改
管理反应式系统中的生命周期
- 在反应式系统中,各种元素(同称为节点)是相互关联的。
- 这些节点可以是signal、effect或其他反应式基元。
- 它们充当共同构成系统反应行为的各个单元。
效果跟踪的同步性质
- 系统注册订阅者,运行effect函数,然后取消注册订阅者。所有的这些都是以线性、同步的顺序进行
- 在createEffect中创建setTimeout。由于系统式同步的,因此它不会等待完成。在setTimeout中触发getter式,全局范围不在具有已注册的订阅者。跟踪会出现问题
处理异步效果
- 虽然基本的响应式系统式同步的。但是像Solid这样的框架提供了处理异步的功能。
- on:提供了手动指定效果的依赖关系方法
- resource: 将网络请求的异步性转换为同步性,并将结果嵌入信号中
Router
基础使用
- 安装:npm install @solidjs/router
- 基础设置:组件将匹配URL以显示所需的页面
1
render(() => <Router />, document.getElementById("root"))
- 提供根级布局:不会在页面更改时更新,是顶级导航和上下文提供程序的理想位置
- 把 router 的 根节点挂载在App上
1
2
3
4
5
6
7
8
9
10import { render } from "solid-js/web"
import { Router } from "@solidjs/router"
const App = (props) => {
<>
{props.children}
</>
}
render(() => <Router root={App} />, document.getElementById("root"))
- 把 router 的 根节点挂载在App上
- 添加路由: 在Router嵌入Route,Route可以指定一个path和一个组件
- catchall路由:用于路由器找不到的页面,如404页面。
- 使用方式:*参数名,参数名为可选
- 创建指向路由的链接
- 途径1:可以使用原生锚点标签(
<a>
) - 途径2:使用
<A>组件
:提供路由的导航,可以使用CSS、inactiveClass和activeClass属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { render } from "solid-js/web";
import { Router, Route, A } from "@solidjs/router";
import Home from "./pages/Home";
import Users from "./pages/Users";
import NotFound from "./pages/NotFound";
const App = (props) => (
<>
{/* 创建指向路由的链接: href指向path一样的路由 */}
<nav>
<A href="/">Home</A>
<A href="/users">Users</A>
</nav>
<h1>Site Title</h1>
{props.children}
</>
);
render(
() => (
<Router root={App}>
{/* 添加路由 */}
<Route path="/" component={Home} />
<Route path="/users" component={Users} />
{/* catchall路由示例,NotFound为遇到404是显示的页面组件 */}
<Route path="*paramName" component={NotFound} />
</Router>
),
document.getElementById("root")
);
- 途径1:可以使用原生锚点标签(
延迟加载路由组件
- lazy函数:推迟组件的加载,直到导航到该组件为止
- 示例:组件引入时使用
1
2
3
4import { lazy } from "solid-js";
const Users = lazy(() => import("./pages/Users"));
const Home = lazy(() => import("./pages/Home"));
动态路由
- 使用冒号:,后面可以是任意字符串,只要url符合该模式,组件都会显示
- 示例:
<Route path="/users/:id" component={User} />
- 关于动画/过渡的注意事项:共享相同路径的路由将被视为同一路由。
- 如果想强制重新渲染,可以将组件包装在一个带键的
<Show>
: - 示例:
1
2
3<Show when={params.something} keyed>
<MyComponent>
</Show>
- 如果想强制重新渲染,可以将组件包装在一个带键的
访问参数 useParams()
- 含义:使用useParams访问后,可以在组件中使用它们
- useParams对于createResource和createSignal,可以基于路由参数创建动态行为
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import { createResource } from "solid-js";
import { useParams } from "@solidjs/router";
async function fetchUser(id) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${id}`
);
return response.json();
}
const User = () => {
const params = useParams();
const [data] = createResource(() => params.id, fetchUser); // 通过参数去创建resource
return (
<div>
<Show when={!data.loading} fallback={<p>Loading...</p>}>
<div>
<p>Name: {data().name}</p>
<p>Email: {data().email}</p>
<p>Phone: {data().phone}</p>
</div>
</Show>
</div>
);
};
验证路由 matchFilters
- 含义:传递一个每个参数对应的验证规则的对象
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const filters: MatchFilters = { // 任何一个不符合都不匹配
parent: ["mom", "dad"], // 只允许是这两个字符串
id: /^\d+$/, // 只允许数字
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html"), // 只允许v的长度大于5,且后缀为.html
};
render(() => (
<Router>
<Route
path="/users/:parent/:id/:withHtmlExtension"
component={User}
matchFilters={filters}
/>
</Router>
), document.getElementById("root"));
可选参数 ?
- 在参数名称的末尾添加?,将指定参数设置为可选参数
- 示例:
<Route path="/stories/:id?" component={Stories} />
id可有可无都不影响
通配符路由 *
- *必须是路径的最后一部分
- 示例:
<Route path="foo/*" component={Foo} />
- 要将通配符部分作为参数公开给组件,可以将其命名:
<Route path="foo/*any" component={Foo} />
多路径
- 使用数组定义路径
- 示例:
<Route path={["login", "register"]} component={Login} />
嵌套路由
- component 属性可以直接传递同页面的函数组件,也可以直接使用箭头函数返回一个Dom
- 只有叶节点(最里面的组件)才会被赋予
- 路由可以无限嵌套
- 示例: 两个指向同一个URL并渲染同一组件
1
2
3
4
5
6
7
8
9<Route path="/users" component={Users} /> // 让 parent 成为自己的路由,你必须单独指定它
<Route path="/users/:id" component={User} />
{/* 嵌套组件 */}
<Route path="/users"> // 可以在这里添加一个component={函数组件},该组件中<A>指向/,还使用props.children ,返回的节点应该是声明props.children的父节点,而不是下面单独指定的父节点
{/* 必须单独指定父节点,才会成为自己的路由 */}
<Route path="/" component={Users} />
<Route path="/:id" component={User} />
</Route>
预加载函数 preload
- 使用 preload 函数,数据获取与加载 route 并行启动
- preload 函数通过在 Route 加载后调用或者在链接悬停时急切地调用
- 示例:
1
2
3
4function preloadUser({ params, location }) {} // 该函数传递用于访问路由信息的对象
// 定义并传递preload
<Route path="/users/:id" component={User} preload={preloadUser} /> - 可以从专用文件或文件中导出与路由相对应的预加载函数和数据包装器。此模式提供了一种无需加载其他任何内容即可导入 data 函数的方法
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13// src/pages/users/[id].data.js
import { query } from "@solidjs/router";
export const getUser = query(async (id) => {
return (await fetch(`https://swapi.tech/api/people/${id}/`)).json();
}, "getUser");
export function preloadUser({ params, location, intent }) {
return getUser(params.id);
}
// 使用时直接导入
import { preloadUser } from "./pages/users/[id].data.js"; - 在除 preload 之外的任何时间调用时,preload 函数的值都会传递给页面组件。可以初始化页面或使用createAsync
- 注意:要防止多次提取或触发重新提取,使用query
数据获取 createResource
- 专为管理异步数据获取而设计的专用信号。
- 它包装了异步操作,提供了一种处理各种状态的方法:loading、success 和 error。
- 此功能是非阻塞的,这意味着即使在检索信息期间,也可以保证应用程序保持响应。
使用
- createResource需要一个返回 Promise 作为其参数的函数。在调用时返回一个信号,该信号具有反应性属性:
- state:当前状态(unresolved、pending、ready、refreshing 或 errored)
- loading:当前任务是否正在进行,boolean类型
- error:错误的信息。
- latest:返回最新数据或结果。
- 示例:
1
2
3
4
5
6const fetchUser = async (id) => {
const response = await fetch(`https://swapi.dev/api/people/${id}/`);
return response.json();
}
const [user] = createResource(userId, fetchUser);
// 使用 user.loading ...
调用多个异步事件 Suspense
- 同步多个异步事件的显示。允许等待所有异步事件解析时回退占位符,防止显示部分加载内容
- 直接在使用异步语句的最外层添加
- 示例:
1
2
3
4
5
6
7
8
9
10<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Match when={user.error}>
<span>Error: {user.error.message}</span>
</Match>
<Match when={user()}>
<div>{JSON.stringify(user())}</div>
</Match>
</Switch>
</Suspense>
动态数据处理
mutate
- 在即时反馈或响应很重要的情况下,该方法提供“乐观突变”。
- 此功能在任务列表等应用程序中特别有价值。
- 例如: 当用户输入新任务并单击按钮时,无论与服务器正在进行的数据通信如何,列表都将立即刷新。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import { For, createResource } from "solid-js"
function TodoList() {
const [tasks, { mutate }] = createResource(fetchTasksFromServer);
return (
<>
<ul>
<For each={tasks()}>
{(task) => (
<li>{task.name}</li>
)}
</For>
</ul>
<button
onClick={() => {
mutate((todos) => [...todos, "do new task"]); // add todo for user
// make a call to send to database
}}
>
Add Task
</button>
</>
);
}
refetch
- 当需要实时反馈时,该方法可用于重新加载当前查询,而不管任何更改。
- 示例:
1
2
3
4
5
6
7
8
9
10import { createResource, onCleanup } from "solid-js"
function StockPriceTicker() {
const [prices, { refetch }] = createResource(fetchStockPrices);
const timer = setInterval(() => {
refetch()
}, 1000);
onCleanup(() => clearInterval(timer))
}