Solid 是一个用于创建用户界面的反应式 JavaScript 库,无需虚拟 DOM。它将模板编译成真实的 DOM 节点,并将更新封装在细粒度的反应中,以便在状态更新时,只有相关的代码运行。
这样,编译器可以优化初始渲染,而运行时可以优化更新。这种对性能的关注使其成为 最受欢迎的 JavaScript 框架之一。
我对它感到好奇,想尝试一下,所以我花了一些时间创建了一个小型待办事项应用程序,以探索该框架如何处理渲染组件、更新状态、设置存储等等。
如果您迫不及待地想查看最终代码和结果,以下是最终演示
入门
与大多数框架一样,我们可以从安装 npm 包开始。要将框架与 JSX 一起使用,请运行
npm install solid-js babel-preset-solid
然后,我们需要将 babel-preset-solid
添加到我们的 Babel、webpack 或 Rollup 配置文件中,使用
"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()
。
对于一个小型的待办事项列表,这将是
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>
);
};
上面的代码示例将显示一个文本字段,并在单击“添加项目”按钮后,将使用新项目更新待办事项,并在列表中显示它。
这可能看起来与使用 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
需要注意的主要问题是,在设置新的姓氏后,我的名字是...
不会被记录。这是因为此时,没有任何东西在监听 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.log("Count is at", count());
});
计数为...
每次 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
,其返回值是一个只读代理对象和一个 setter 函数。
例如,如果我们将我们的待办事项示例更改为使用存储而不是状态,它将看起来像这样
const [todos, addTodos] = createStore({ list: [] });
createEffect(() => {
console.log(todos.list);
});
onMount(() => {
addTodos("list", [
...todos.list,
{ item: "a new todo item", completed: false }
]);
});
上面的代码示例将首先记录一个带有空数组的代理对象,然后记录一个带有包含 {item: "一个新的待办事项", completed: false}
对象的数组的代理对象。
需要注意的一点是,在不访问其属性的情况下,无法跟踪顶层状态对象——这就是为什么我们记录 todos.list
而不是 todos
的原因。
如果我们只记录了 todo
` 在 createEffect
中,我们将看到列表的初始值,但不会看到在 onMount
中更新后的值。
要更改存储中的值,我们可以使用在使用 createStore
时定义的设置函数更新它们。例如,如果我们想将待办事项列表项更新为“已完成”,我们可以通过这种方式更新存储
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>
演示项目
这是一个对 Solid 基础知识的快速介绍。如果您想尝试一下,我创建了一个入门项目,您可以自动部署到 Netlify 并通过单击下面的按钮将其克隆到您的 GitHub!
该项目包括 Solid 项目的默认设置,以及一个示例待办事项应用程序,其中包含我在本文中提到的基本概念,以帮助您入门!
这个框架远不止我在这里介绍的内容,所以请随时查看文档以获取更多信息!
为什么displayFullName()监听的是firstName而不是lastName?是什么触发了对onwe的监听,而不是其他的?
createEffect
语句监听displayName
函数值的改变。displayName
返回的值只在displayFullName
从true
变为false
或反过来时才会改变。因此,当我们用新值调用
setLastName
时,lastName
变量会发生改变,但这不会更新displayName
的值,因为此时displayFullName
仍然是false
,所以displayName
只返回firstName
,而firstName
没有改变。因此,
createEffect
没有被触发。希望这说得通!
createEffect的例子被截断了
抱歉,我很快就会更新它,谢谢!!
很棒的文章!
很棒的入门文章。内容组织良好,重点突出,涵盖了所有基本功能和语法。谢谢。
这篇文章很棒,因为它清楚地展示了核心概念。然而,很难看到它相对于React的优缺点 - 为什么创建这个库,它解决了什么问题?想法?
更简单的执行模型。在很多方面,它拥有Svelte的“写更少代码”元素,但应用于显式的运行时API。没有像
useRef
或useCallback
这样的概念。Solid首先是一个状态库,它恰好渲染DOM。低级别抽象。在你所写的代码和底层之间,几乎没有抽象。JSX元素是DOM元素,组件实际上只是函数。这消除了抽象带来的税收和执行复杂性。
性能。Solid的性能在浏览器和服务器上都远远领先于其他解决方案。它更接近于纯JS,而不是竞争对手。它还产生了一些最小的包大小。
有关优点的更多信息
介绍SolidJS UI库
看看这个来自React Finland 2021的演讲
老实说,我知道技术上的理由不足以改变人们的想法,但我们所能做的就是使技术方面无懈可击,并希望其他方面也能迎刃而解。
感谢你的文章,Charlie!一个小建议:React中没有componentDidUnmount,正确的名称是componentWillUnmount,请编辑一下。