在上一篇文章中,带大家入门了 SolidJS,今天继续讲解进阶知识,包括:
- 生命周期
- 事件绑定
- 样式绑定
- Ref 引用
- Context 上下文
- 嵌套响应式
生命周期
在 SolidJS 中,生命周期非常简单,只有 onMount
和 onCleanup
两个,其中 onMount
相当于 React 中没有任何依赖项的 useEffect
:
jsx
// React
useEffect(() => {
fetch('xxx') // 发起数据请求
}, [])
// SolidJS
onMount(() => {
fetch('xxx') // 发起数据请求
})
onMount
会在组件初始渲染完成后执行,且只会运行一次,而 onCleanup
就相当于 React 中没有任何依赖项的 useEffect
回调中返回的清理函数:
jsx
// React
useEffect(() => {
const timer = setInterval(() => console.log('timer'), 1000)
return () => {
// 这里是清理代码
clearInterval(timer)
}
},[])
// SolidJS
const timer = setInterval(() => setCount(count() + 1), 1000);
onCleanup(() => clearInterval(timer))
可以看到,onCleanup
是一个一级方法,可以在任何范围内调用,它会在该范围被触发重新求值和组件销毁的时候执行。由于响应式系统接管了大部分工作,且是同步创建和更新的,所以开发者无需太过关心组件的生命周期。
事件绑定
语法和 React 是完全一样的:
jsx
// React 事件绑定
<Button onClick={handleClick}>按钮</Button>
// SolidJS 事件绑定
<Button onClick={handleClick}>按钮</Button>
但是除此之外,还支持数组语法:
jsx
<Button onClick={[handleClick, 'some params']}>按钮</Button>
因为这样可以避免创建额外的闭包,例如下面这种:
jsx
<Button onClick={() => handleClick('some params')}>按钮</Button>
另外,如果是用户的自定义事件,可以通过 on:
命名空间语法来监听,例如:
jsx
// 创建一个新的 CustomEvent 对象
const event = new CustomEvent('WierdEventName', {
// 可选:可以在这里携带一些信息
detail: { age: 12 }
});
// 使用 on: 前缀来监听这个自定义的事件。
const btn = <button on:WierdEventName={(evt) => console.log('WierdEvent', evt)} >Click Me</button>
// 在 btn 上触发这个事件
btn.dispatchEvent(event);
当然,对于内置的事件,on:XXX
和 onXXX
效果是一样的,例如:
jsx
<Button onClick={() => /* Do something */}>按钮</Button>
<Button on:click={() => /* Do something */}>按钮</Button>
样式绑定
与 React 不同的是,style 对象中写样式,CSS 属性不需要用驼峰:
jsx
// React
<div style={{ fontWeight: 800, fontSize: 16 }}>
Some Text
</div>
// SolidJS
<div style={{ "font-weight": 800, "font-size": `16px` }}>
Some Text
</div>
CSS 变量也可以直接写:
jsx
<div style={{ "--my-custom-color": themeColor() }} />
而且同时支持用 class
和 className
来设置元素类名:
jsx
<div class="red">红色</div>
<div className="black">黑色</div>
更贴心的是,还内置了 classList
属性,类似于 React 中的 classnames
包的效果,能够接受一个对象,key 是类名,value 是布尔表达式,当 true 时应用该样式,当 false 时移除该样式:
jsx
<button
classList={{ selected: current() === "foo" }}
onClick={() => setCurrent("foo")}
>
foo
</button>
Ref 引用
在 SolidJS 中,想拿到元素的引用非常简单,直接这么写就好了:
jsx
const myDiv = <div>My Element</div>
就是变量赋值啊,没错!当然,它也支持 React 的写法:
jsx
let myDiv
<div ref={myDiv}>My Element</div>
那么 myDiv 也会是 <div>
的引用,只不过初始值是 undefined,赋值行为发生在元素创建后,被添加到 DOM 前进行。SolidJS 同样也支持回调的写法:
jsx
<div ref={el => /* 处理 el... */}>My Element</div>
回调写法的好处就是元素已创建就能拿到 el 了,不需要等到被添加到 DOM 之前。
Context 上下文
跟 React 一样,SolidJS 也提供了 Context API 来解决 props 透传的问题:
jsx
import { createSignal, createContext, useContext } from "solid-js"
const CounterContext = createContext()
export function CounterProvider(props) {
const [count, setCount] = createSignal(props.count || 0),
store = [
count,
{
increment() {
setCount(c => c + 1)
},
decrement() {
setCount(c => c - 1)
}
}
]
return (
<CounterContext.Provider value={store}>
{props.children}
</CounterContext.Provider>
)
}
那么 CounterProvider 的所有后代组件,都可以通过 useContext(CounterContext)
拿到 count 变量和 increment
和 decrement
方法,无论层级有多深。
jsx
const [count, { increment, decrement }] = useCounter()
但实际上,由于 SolidJS 是响应式的,开发者在大部分场景下直接使用全局的 Store/Signal 即可,没必要用 Context,例如:
jsx
import { createSignal, createMemo } from "solid-js"
function createCounter() {
const [count, setCount] = createSignal(0)
const increment = () => setCount(count() + 1)
const doubleCount = createMemo(() => count() * 2)
return { count, doubleCount, increment }
}
export default createCounter()
这样导出一个响应式对象,可以直接在另外一个组件中使用:
jsx
function Counter() {
const { count, doubleCount, increment } = counter
return (
<button type="button" onClick={increment}>
{count()} {doubleCount()}
</button>
)
}
嵌套响应式
jsx
import { For } from 'solid-js'
const Todos = () => {
let input
return (
<>
<div>
<input ref={input} />
<button onClick={add}>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>
</>
)
}
export default Todos
然后创建一个 Signal :
jsx
import { createSignal } from 'solid-js'
function TodoSignal() {
let todoId = 0
const [todos, setTodos] = createSignal([])
const addTodo = (text) => setTodos([...todos(), { id: ++todoId, text, completed: false }])
const toggleTodo = (id) => setTodos(todos().map((todo) => (todo.id !== id ? todo : { ...todo, completed: !todo.completed })))
return { todos, addTodo, toggleTodo }
}
export default TodoSignal()
接下来对 Todos 组件补充逻辑:
jsx
import todoSignal from './signal'
const Todos = () => {
let input
const { todos, addTodo, toggleTodo } = todoSignal
const add = () => {
if (!input.value.trim()) return
addTodo(input.value)
input.value = ''
}
return (
/* 省略代码 */
)
}
这样就能实现响应式了:

但这是以 todo 为粒度的更新,意思是如果 todo 发生变化,下面的 div
元素整体发生更新:
jsx
<div>
<input type="checkbox" checked={todo.completed} onchange={[toggleTodo, id]} />
<span
style={{
'text-decoration': todo.completed ? 'line-through' : 'none',
}}
>
{text}
</span>
</div>
SolidJS 通过 createStore
可以做到值更新 div
里面的 span
,而 input
则不需要动:
jsx
import { createStore } from 'solid-js/store'
function TodoStore() {
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 { store, addTodo, toggleTodo }
}
export default TodoStore()
修改后的 Todos 组件:
jsx
import todoStore from './store'
const Todos = () => {
let input
const { store, addTodo, toggleTodo } = todoStore
const todos = () => store.todos
const add = () => {
if (!input.value.trim()) return
addTodo(input.value)
input.value = ''
}
return (
/* 省略代码 */
)
}
这简直不可思议!再次体现了 SolidJS 的强大之处------通过精确定位做出精细的响应。
总结
掌握了这一节的内容之后,结合上一讲,完全可以用 SolidJS 进行开发了。历史包袱非常少,对于一些简单的页面,追求极致性能的,SolidJS 绝对是不二之选。略有遗憾的是组件库比较少,不过可以选择那些支持多框架的组件库,例如 Ark UI,同时支持 React、Solid 和 Vue:

或者与框架无关的组件库,例如 daisyui,用 SolidJS 结合 tailwind css 也能写出漂亮的组件:
