Solid是一个反应式 JavaScript 库,用于创建没有虚拟 DOM 的用户界面。它一次将模板编译为真实的DOM节点,并将更新包裹在细粒度的反应中,因此当状态更新时,只运行相关代码。
这样一来,编译器可以优化初始渲染,而运行时则优化更新。这种对性能的关注使它成为评级最高的JavaScript框架之一。
我对它很好奇,想试一试,所以我花了一些时间创建了一个小的待办事项应用程序,以探索这个框架是如何处理渲染组件、更新状态、设置商店等的。
如果你迫不及待地想看到最终的代码和结果,这里是最后的演示。
开始吧
像大多数框架一样,我们可以从安装npm包开始。要用JJSX使用该框架,请运行:
npm install solid-js babel-preset-solid
然后,我们需要在我们的Babel、webpack或Rollup配置文件中添加babel-preset-solid:
"presets": ["solid"]
或者,如果你想搭建一个小的应用程序,你也可以使用他们的一个模板:
# Create a small app from a Solid template
npx degit solidjs/templates/js my-app
# Change directory to the project created
cd my-app
# Install dependencies
npm i # or yarn or pnpm
# Start the dev server
npm run dev
有TypeScript支持,所以如果你想启动一个TypeScript项目,把第一个命令改为npx degit solidjs/templates/ts my-app 。
创建和渲染组件
要渲染组件,其语法与React.js相似,所以可能看起来很熟悉:
import { render } from "solid-js/web";
const HelloMessage = props => <div>Hello {props.name}</div>;
render(
() => <HelloMessage name="Taylor" />,
document.getElementById("hello-example")
);
我们需要从导入render 函数开始,然后我们创建一个带有一些文本和道具的div,并调用render ,传递组件和容器元素。
然后,这段代码会被编译成真正的DOM表达式。例如,上面的代码样本,一旦被Solid编译,看起来就是这样的:
import { render, template, insert, createComponent } from "solid-js/web";
const _tmpl$ = template(`<div>Hello </div>`);
const HelloMessage = props => {
const _el$ = _tmpl$.cloneNode(true);
insert(_el$, () => props.name);
return _el$;
};
render(
() => createComponent(HelloMessage, { name: "Taylor" }),
document.getElementById("hello-example")
);
Solid Playground非常酷,显示了Solid有不同的渲染方式,包括客户端、服务器端和带有水化功能的客户端。
用信号跟踪变化的值
Solid 使用一个名为createSignal 的钩子,它返回两个函数:一个 getter 和一个 setter。如果您习惯于使用像React.js这样的框架,这可能看起来有点奇怪。你通常会期望第一个元素是值本身;然而在Solid中,我们需要明确地调用getter来拦截值的读取位置,以便跟踪其变化。
例如,如果我们正在写下面的代码:
const [todos, addTodos] = createSignal([]);
日志todos ,将不会返回该值,而是返回一个函数。如果我们想使用该值,我们需要调用该函数,如todos() 。
对于一个小的todo列表,这将是:
import { createSignal } from "solid-js";
const TodoList = () => {
let input;
const [todos, addTodos] = createSignal([]);
const addTodo = value => {
return addTodos([...todos(), value]);
};
return (
<section>
<h1>To do list:</h1>
<label for="todo-item">Todo item</label>
<input type="text" ref={input} name="todo-item" id="todo-item" />
<button onClick={() => addTodo(input.value)}>Add item</button>
<ul>
{todos().map(item => (
<li>{item}</li>
))}
</ul>
</section>
);
};
上面的代码示例将显示一个文本字段,在点击 "添加项目 "按钮后,将用新的项目更新todos,并在列表中显示。
这似乎与使用useState 相似,那么使用getter有什么不同?考虑一下下面的代码示例:
console.log("Create Signals");
const [firstName, setFirstName] = createSignal("Whitney");
const [lastName, setLastName] = createSignal("Houston");
const [displayFullName, setDisplayFullName] = createSignal(true);
const displayName = createMemo(() => {
if (!displayFullName()) return firstName();
return `${firstName()} ${lastName()}`;
});
createEffect(() => console.log("My name is", displayName()));
console.log("Set showFullName: false ");
setDisplayFullName(false);
console.log("Change lastName ");
setLastName("Boop");
console.log("Set showFullName: true ");
setDisplayFullName(true);
运行上述代码的结果是:
Create Signals
My name is Whitney Houston
Set showFullName: false
My name is Whitney
Change lastName
Set showFullName: true
My name is Whitney Boop
主要需要注意的是My name is ... ,在设置了新的姓氏后,没有记录。这是因为在这一点上,没有任何东西在监听lastName() 上的变化。只有当displayFullName() 的值发生变化时,displayName() 的新值才会被设置,这就是为什么当setShowFullName 被设置回true 时,我们可以看到新的姓氏显示。
这给了我们一个更安全的方式来跟踪值的更新。
反应性基元
在最后一个代码示例中,我介绍了createSignal ,但也介绍了其他几个基元:createEffect 和createMemo 。
createEffect
createEffect 追踪依赖关系,并在每次渲染后运行依赖关系的变化:
// Don't forget to import it first with 'import { createEffect } from "solid-js";'
const [count, setCount] = createSignal(0);
createEffect(() => {
console
Count is at... 每次 的值发生变化时,都会记录下来。count()
createMemo
createMemo 创建一个只读信号,每当执行的代码的依赖关系更新时,就重新计算其值。当你想缓存一些值,并在依赖关系发生变化之前不重新评估它们就可以使用它。
例如,如果我们想显示一个计数器100次,并在点击一个按钮时更新其值,使用createMemo 将允许每次点击只发生一次重新计算。
function Counter() {
const [count, setCount] = createSignal(0);
// Calling `counter` without wrapping it in `createMemo` would result in calling it 100 times.
// const counter = () => {
// return count();
// }
// Calling `counter` wrapped in `createMemo` results in calling it once per update.
// Don't forget to import it first with 'import { createMemo } from "solid-js";'
const counter = createMemo(() => {
return count()
})
return (
<>
<button onClick={() => setCount(count() + 1)}>Count: {count()}</button>
<div>1. {counter()}</div>
<div>2. {counter()}</div>
<div>3. {counter()}</div>
<div>4. {counter()}</div>
<!-- 96 more times -->
</>
);
}
生命周期方法
Solid 暴露了一些生命周期方法,如onMount 、onCleanup 和onError 。如果我们想在初始渲染后运行一些代码,我们需要使用onMount 。
// Don't forget to import it first with 'import { onMount } from "solid-js";'
onMount(() => {
console.log("I mounted!");
});
onCleanup 类似于 React 中的 - 它在对反应式范围进行重新计算时运行。componentDidUnmount
onError 当最近的孩子的作用域出现错误时执行。例如,我们可以在获取数据失败时使用它。
商店
为了创建数据存储,Solid 暴露了createStore ,其返回值是一个只读代理对象和一个设置器函数。
例如,如果我们改变我们的 todo 例子以使用一个存储而不是状态,它将看起来像这样:
const [todos, addTodos] = createStore({ list: [] });
createEffect(() => {
console.log(todos.list);
});
onMount(() => {
addTodos("list", [
...todos.list,
{ item: "a new todo item", completed: false }
]);
});
上面的代码示例将从记录一个带有空数组的代理对象开始,接着是一个带有包含对象的数组的代理对象{item: "a new todo item", completed: false} 。
有一点需要注意的是,如果不访问顶层状态对象上的一个属性,就不能跟踪它--这就是为什么我们要记录todos.list 而不是todos 。
如果我们只记录todo`在createEffect ,我们就会看到列表的初始值,而不是在onMount 中进行更新后的值。
要改变商店中的值,我们可以使用我们在使用createStore 时定义的设置函数来更新它们。例如,如果我们想将一个todo列表项更新为 "已完成",我们可以通过这种方式更新存储。
const [todos, setTodos] = createStore({
list: [{ item: "new item", completed: false }]
});
const markAsComplete = text => {
setTodos(
"list",
i => i.item === text,
"completed",
c => !c
);
};
return (
<button onClick={() => markAsComplete("new item")}>Mark as complete</button>
);
控制流程
为了避免在使用像.map() 这样的方法时,在每次更新时浪费地重新创建所有的 DOM 节点,Solid 让我们使用模板帮助器。
其中有几个是可用的,比如For 来循环浏览项目,Show 来有条件地显示和隐藏元素,Switch 和Match 来显示符合某个条件的元素,以及更多!
这里有一些例子显示如何使用它们。
<For each={todos.list} fallback={<div>Loading...</div>}>
{(item) => <div>{item}</div>}
</For>
<Show when={todos.list[0].completed} fallback={<div>Loading...</div>}>
<div>1st item completed</div>
</Show>
<Switch fallback={<div>No items</div>}>
<Match when={todos.list[0].completed}>
<CompletedList />
</Match>
<Match when={!todos.list[0].completed}>
<TodosList />
</Match>
</Switch>