Solid JavaScript库介绍

541 阅读5分钟

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 ,但也介绍了其他几个基元:createEffectcreateMemo

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 暴露了一些生命周期方法,如onMountonCleanuponError 。如果我们想在初始渲染后运行一些代码,我们需要使用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 来有条件地显示和隐藏元素,SwitchMatch 来显示符合某个条件的元素,以及更多!

这里有一些例子显示如何使用它们。

<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>