内嵌响应式
在React里面,如果有一个数组,并且数组内的元素是对象。然后需要仅仅修改其中一个属性,那就是会解构数组,解构对象,然后进行更改。用SolidJS写就比如下面这样
tsx
import {createSignal, For} from "solid-js";
interface Todo{
id: number,
name: string,
completed: boolean,
}
const App = () => {
let todoId = 0;
const [todos, setTodos] = createSignal<Todo[]>([]);
const [inputValue, setInputValue] = createSignal('');
const addTodo = () => {
if(inputValue().trim() === ""){
return
}
setTodos(pre => [...pre, {id: ++todoId, name: inputValue(), completed: false}])
}
const toggleTodo = (id: number) => {
setTodos(pre => pre.map(todo => {
return todo.id !== id ? todo: {...todo, completed: !todo.completed}
}))
}
return (
<div style={{display: "flex", "flex-direction": "column"}}>
<div style={{display: "flex"}}>
<input type="text" value={inputValue()} onInput={e => setInputValue(e.target.value)}/>
<button onClick={addTodo}>添加</button>
</div>
<For each={todos()}>
{
todo => {
console.log(`Creating ${todo.name}`)
return (
<div style={{display: "flex"}}>
<input type="checkbox" checked={todo.completed} onchange={() => toggleTodo(todo.id)}/>
<div>{todo.id}.{todo.name}</div>
</div>
)
}
}
</For>
</div>
)
};
当我们点击checkbox的时候:
如果在React里面这样写是没有问题的,因为React会对比fiber node找到不一样的地方再进行更新,所以更新的时候只会更新input。
但是在SolidJS中没有做这样的对比,todo.completed又不是响应式数据没办法使用effect监听变化然后更新(虽然编译后有使用effect函数包裹),只能重新执行创建For中的函数,也就是重新创建整行todo。所以每次点击checkbox都能在控制台看到Creating xxx。
这种情况我们可以使用内嵌的响应式数据,写法如下
tsx
import {Accessor, createSignal, For, Setter} from "solid-js";
interface Todo{
id: number,
name: string,
completed: Accessor<boolean>,
setCompleted: Setter<boolean>
}
const App = () => {
let todoId = 0;
const [todos, setTodos] = createSignal<Todo[]>([]);
const [inputValue, setInputValue] = createSignal('');
const addTodo = () => {
if(inputValue().trim() === ""){
return
}
const [completed, setCompleted] = createSignal(false)
setTodos(pre => [...pre, {id: ++todoId, name: inputValue(), completed, setCompleted}])
}
const toggleTodo = (id: number) => {
todos().find(todo => todo.id === id)?.setCompleted(pre => !pre)
}
return (
<div style={{display: "flex", "flex-direction": "column"}}>
<div style={{display: "flex"}}>
<input type="text" value={inputValue()} onInput={e => setInputValue(e.target.value)}/>
<button onClick={addTodo}>添加</button>
</div>
<For each={todos()}>
{
todo => {
console.log(`Creating ${todo.name}`)
return (
<div style={{display: "flex"}}>
<input type="checkbox" checked={todo.completed()} onchange={() => toggleTodo(todo.id)}/>
<div>{todo.id}.{todo.name}</div>
</div>
)
}
}
</For>
</div>
)
};
也就是说,把completed改为响应式数据,并且在对象里面添加对应的Setter,在需要的时候取出来更新。改成这样其实就是让编译器在checked={todo.completed()}
这个地方改成了createEffect。
不过说实话,我不太喜欢这样用,也没啥原因,就是太麻烦了。
所以这里其实也暴露出了createSignal的缺点,与Vue相比,它响应式只有浅层的。
Store
使用内嵌响应式除了麻烦还有一个很大的问题,那就是修改了对象,还加了额外的属性setCompleted,这是真的不好。
所以SolidJS提供了深层的响应式createStore,修改上面的代码只会就得到
tsx
import {createSignal, For} from "solid-js";
import {createStore} from "solid-js/store";
interface Todo {
id: number,
name: string,
completed: boolean
}
const App = () => {
let todoId = 0;
const [todos, setTodos] = createStore<Todo[]>([]);
const [inputValue, setInputValue] = createSignal('');
const addTodo = () => {
if (inputValue().trim() === "") {
return
}
setTodos(todos => [...todos, {id: ++todoId, name: inputValue(), completed: false}])
}
const toggleTodo = (id: number) => {
setTodos(t => t.id === id, 'completed', completed => !completed)
}
return (
<div style={{display: "flex", "flex-direction": "column"}}>
<div style={{display: "flex"}}>
<input type="text" value={inputValue()} onInput={e => setInputValue(e.target.value)}/>
<button onClick={addTodo}>添加</button>
</div>
<For each={todos}>
{
todo => {
console.log(`Creating ${todo.name}`)
return (
<div style={{display: "flex"}}>
<input type="checkbox" checked={todo.completed} onchange={() => toggleTodo(todo.id)}/>
<div>{todo.id}.{todo.name}</div>
</div>
)
}
}
</For>
</div>
)
};
使用了createStore之后,todos不再是一个函数,而是一个被Proxy代理后的数组,也就是与Vue中类似,我们使用todos[1]的时候就相当于调用了一个getter函数,此时就可以被CreateEffect这样的函数捕获到,todos[1].completed也同样触发了依赖收集。
不过与Vue不同的是Setter是另外的一个函数,如果你直接复制是不会有效果, 还要setTodos才可以触发ui更新。
这里需要介绍一下Setter函数, 最简单的用法就Singal是一样的
tsx
//直接赋予新值
setTodos([{id: ++todoId, name: 'lisi', completed: false}])
//用函数可以得到当前值
setTodos(todos => [...todos, {id: ++todoId, name: 'lisi', completed: false}])
如果只是想修改数组中对象的某个属性,就要用到路径选择
tsx
setTodos(t => t.id === id, 'completed', completed => !completed)
对于数组来说,路径选择就类似于find函数一样。对于对象来说就是传入属性名称。
最后一个函数就是已经锁定了对应的属性,这个时候就是传入该属性需要的值或者是可以返回该属性的值函数
produce
刚刚也说了,不可以像Vue那样直接复制,但是有些时候又需要复杂逻辑的判断才可以修改,如果使用路径选择setter其实也不是那么的方便,又或者写出来了就一大堆的...之类的。SolidJS提供了produce,这是一个受 Immer 启发的函数。有了它之后可以将addTodo和toggleTodo改成下面这样
tsx
//...省略
import {createStore, produce} from "solid-js/store";
//...省略
const addTodo = () => {
if (inputValue().trim() === "") {
return
}
setTodos(produce(todos => todos.push({id: ++todoId, name: inputValue(), completed: false})))
}
const toggleTodo = (id: number) => {
setTodos(
produce(todos => {
let todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed
}
}
)
)
}
上面代码并没有写得很好,因为我只是为了演示可以produce可以随意操作。
不可变Store
在官方文档里面关于它的介绍是用来与其它库进行交互的,我太菜了看不懂(绝对不是不喜欢用),所以我就不做介绍了,直接略过。