likes
comments
collection
share

在项目中落地 SolidJS(一)

作者站长头像
站长
· 阅读数 5

在业务中使用 SolidJS 有一段时间了,感觉非常好,在此也推荐给大家使用。

什么是 SolidJS?

一个用于构建用户界面,简单高效、性能卓越的 JavaScript 库

为什么要用 SolidJS?

主要是因为性能好,几乎等价于执行原生 JavaScript 了,比 Vue 和 React 框架快很多,下面是官网给出的对比图:

在项目中落地 SolidJS(一)

如何使用 SolidJS?

如果你会 React,那么恭喜你,已经学会了 Solid 的语法了,以下面的 Counter 组件为例,是不是非常熟悉?

import { createSignal } from "solid-js";

function Counter() {
	const [count, setCount] = createSignal(0);
	setInterval(() => setCount(count() + 1), 1000);
	return (
		<div>
			<p>Count: {count()}</p>
		</div>
	);
}

如果你会 Vue,那么同样恭喜你,已经掌握了 SolidJS 的原理了,因为它的响应式原理和 Vue3 是非常相似的,在底层实现上和 Vue Composition API 一样:

const [name, setName] = createSignal("Keliq");
createEffect(() => console.log(`name: ${name()}`));
setTimeout(() => setName("David"), 5000);

在上面的代码中 createEffect 就自动收集了变量 name 的依赖,当 5s 后调用 setName 更新时,会触发 createEffect 中回调函数重新执行,这不就是 Vue3 中的 reactiveeffect 嘛!

创建项目

如果项目名叫 solid-demo,那么在命令行中执行下面的命令即可:

$ npx degit solidjs/templates/ts solid-demo
Need to install the following packages:
degit@2.8.4
Ok to proceed? (y) y
> cloned solidjs/templates#HEAD to solid-demo

会自动生成一个 pnpm + vite 的项目:

.
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── src
│   ├── App.module.css
│   ├── App.tsx
│   ├── assets
│   │   └── favicon.ico
│   ├── index.css
│   ├── index.tsx
│   └── logo.svg
├── tsconfig.json
└── vite.config.ts

你可能会好奇 degit 这个 npm 包做了什么?其实就是把 GitHub 上的项目下载到本地,仅此而已

接下来只需要进入目录、安装依赖、启动项目即可:

$ cd solid-demo # 进入目录
$ pnpm i # 安装依赖
$ npm start # 启动项目

组件

SolidJS 使用的是 JSX 语法,前端同学一看就懂,例如定义一个组件:

function MyComponent(props) {
  return <div>Hello {props.name}</div>;
}

仅仅是一个函数而已,在使用的时候按照 JSX 语法传递 props 即可:

<MyComponent name="Solid" />;

Props

Props 参数是只读的,当 Props 是一个对象的时候,千万不能解构 Props,否则就失去了响应式:

export default function Greeting(props) {
  const {greeting = 'Hi', name = 'John' } = props
  return <h3>{greeting} {name}</h3> // 无法响应式
  // return <h3>{props.greeting || "Hi"} {props.name || "John"}</h3> // 可以响应式
}

正确的写法是下面这个样子:

export default function Greeting(props) {
  return <h3>{props.greeting || "Hi"} {props.name || "John"}</h3> // 可以响应式
}

除了结构,扩展运算或者 object.assign 也会导致失去响应性。这一点确实影响开发体验,但是 Vue 的同学都知道这个没办法,原始值就是没法实现响应式,否则也不会在有了 reactive 函数之后再搞一个 ref 了。

信号

在 React 中是通过 useState 来创建组件内部的状态,而 SolidJS 中则是用 createSignal 函数,可以翻译为「创建信号」,但语法上和 React 创建内部状态几乎一模一样:

const [loading, setLoading] = createSignal(false)

但是这里一定要注意,loadingsetLoading 都是一个函数!在 JSX 中很多人会错误地写成这样:

{/* 错误的写法!*/ }
<button classList={{ loading }}><button> 
{/* 因为这个 loading 不是 boolean 而是 function!*/ }

正确的写法是:

{/* 取值的时候要用 loading(),下面的写法才是对的 */ }
<button classList={{ loading: loading()}}><button>

这一点非常关键,是很多写惯了 React 的同学经常犯的错误。

其实 createSignal 函数的本质其实就是一个 getter 和 setter 函数,利用闭包特性来更新 value 值:

const createSignal = (value) => {
  // 获取 Signal value
  const getter = () => {
    return value;
  };
  // 修改 Signal value
  const setter = (newValue) => {
    value = newValue;
  };
  return [getter, setter];
};

副作用

在 React 当中使用下面的语法创建副作用:

useEffect(cb, deps) // react 的写法

而在 SolidJS 中,则完全省略了依赖数组 deps,语法上更加简洁:

createEffect(cb) // solid 的写法

但是需要注意,useEffectcreateEffect 有本质区别:

  • useEffect 会让开发者显示声明依赖项,如果依赖发生变化,就会执行回调函数。
  • createEffect 是自动收集依赖,会收集里面用到的响应式变量(createSignal 创建的变量),如果里面没有这些变量,就不会多次执行。

衍生值

如果变量 A 的改变会导致变量 B 发生变化,那么 B 就是 A 的衍生值,这一点非常类似于 Vue 中的计算属性:

const doubleCount = () => count() * 2;

上面的 doubleCount 就是 count 的衍生值,这一点尤其需要注意,千万不能写成:

const doubleCount = count() * 2;

如果写成上面的样子,doubleCount 不会在 count 发生变化的时候及时更新,并非一个响应式变量了,这一点可以通过以下代码进行验证:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
    const [count, setCount] = createSignal(0);
    setInterval(() => setCount(count() + 1), 1000);
    const minusCount = () => count() - 1 // 使用函数来更新
    const plusCount = count() + 1 // 不更新

    return (
        <div>
            <p>Count: {count()}</p>
            <p>Minus Count: {minusCount()}</p>
            <p>Plus Count: {plusCount}</p>
        </div>
    )
}

render(() => <Counter />, document.getElementById('app'));

请始终记住:

SolidJS 的组件只是执行一次的函数,确保更新的唯一方法是将 Signal 包裹在计算或 JSX 中,重复使用的时候,将表达式包装在函数中。

缓存值

在 React 中会通过 useMemo 来缓存复杂计算的结果,避免多次重复计算:

const fib = useMemo(() => fibonacci(count), [count]) // react 的写法

而在 SolidJS 中,则是用 createMemo 来缓存计算结果,但是也不需要写依赖项:

const fib = createMemo(() => fibonacci(count())) // solid 的写法

SolidJS 会对内部的依赖进行标记,如果发生变化(浅比较,引用类型只有变了才算变)才会重新进行计算。

虚拟 DOM

在 SolidJS 中,完全没有虚拟 DOM,这跟 React 有本质上的差异,在 React 中,下面的写法非常常见:

// 这种方式会每次重新创建DOM
const list = cats.map(it => <li>{it.name}</li>)

但是在 SolidJS 中,千万不要这么写,因为 map 方法在每次更新时都要重新创建 DOM 节点,众所周知,创建 DOM 节点是非常昂贵的,没有虚拟 DOM 自然没有 DOM Diff,减少了额外的抽象层,并且可以更精确地控制DOM 更新的过程,这也是 SolidJS 在很多基准测试中显示出非常好的性能表现的原因。

那上面的循环创建 DOM 的场景,应该怎么写呢?类似于 Vue 的模板语法,SolidJS 封装了 For 组件:

// 用 For 标签
<For each={cats()}>
  {(cat,i)=><li>{it.name}</li>}
</For>

<For> 组件是遍历任何非原始值数组的最佳方式。它通过引用自动键引用,以便在数据更新时对其进行优化以更新或移动行而不是重新创建它们。

对于首次接触 SolidJS 的同学,上面的内容已经很多了,需要时间好好消化。强烈建议先去官网体验一下交互式教程,后面再写第二篇(生命周期、事件绑定、Context 上下文等)和第三篇(响应式原理)。