SolidJS 与其他 JS 框架的 5 个不同之处
1. 组件不会重新渲染
2. 代理是只读的
3. 没有isSignal
/ isObservable
/isRef
4. 更新同步
5. 没有取消订阅
结论
Solid是一个类似 React 的 JSX 模板化 UI 框架,但它又像 Vue 或 Svelte 一样具有响应式特性。(如果您不熟悉 Solid,可以先阅读简介)。然而,它有一些与其设计密切相关的特殊特性,许多开发者一开始可能会感到有些意外。即使是那些使用过其他“响应式”UI 框架的开发者,情况也是如此。
但请相信我,这种疯狂是有方法的。让我们看看 Solid 有何不同,以及为什么这是一件好事。
1. 组件不会重新渲染
import { createSignal } from "solid-js";
import { render } from "solid-js/web";
function A() {
console.log("A");
const [value, setValue] = createSignal(0);
return <B
value={value() + 1}
onClick={() => setValue(value() + 1)}
/>;
}
function B(props) {
console.log("B");
return <C value={props.value - 1} onClick={props.onClick}/>;
}
function C(props) {
console.log("C");
return <button onClick={props.onClick}>{props.value}</button>;
}
render(() => <A />, document.getElementById("app"));
当我们第一次呈现此代码时,它会记录“ABC”,但是您能猜出当我们单击按钮时我们会记录什么吗?
什么也没有。绝对没有。但我们的计数器仍在增加。
这是 Solid 迄今为止最具决定性的部分。组件不会重新运行,只会重新运行你使用的原语和 JSX 表达式。这意味着对于 React 用户来说,不会再有过时的闭包或 Hook 规则。
与 Vue 或 MobX 类似,我们不希望过早引用响应式变量或解构。但与 React、Vue 或 Svelte 不同,Solid 拥有真正精细的更新能力。这意味着组件实际上在更新后或多或少会消失。
看似简单的绑定,实际上会通过视图代码生成响应式流,精准地跨组件执行更新。您的视图不仅看起来具有声明性,而且其行为也具有声明性。
我们如何实现这一点?只需对所有动态 props 进行惰性求值即可。看看组件 B 编译后的结果:
function B(props) {
console.log("B");
return createComponent(C, {
get value() {
return props.value - 1;
},
get onClick() {
return props.onClick;
}
});
}
它只是将表达式转发到最终使用的地方。完整示例及编译输出请见此处。
2. 代理是只读的
这个问题真的让人费解。响应式不就是为了让事情变得简单,然后就能正常工作吗?的确如此。但如果没有仔细的控制,很容易就会忘记变化是如何传播的。这就是响应式的缺点之一,人们在负面语境中把它描述成“魔法”。
响应式的核心理念是“凡是能够派生的,就应该派生”。因此,依赖关系的自动跟踪通常被认为是问题所在,但其实并非如此。问题在于任意赋值。我们需要明确说明。
我们之前见过这种情况。像 Redux 中的 Reducer 或状态机中的事件定义了一组动作和操作来更新状态。MobX 也拥有动作。通过限制这些动作,我们可以推断出正在发生的事情。
更甚的是,像代理这样的嵌套响应式对象具有侵入性。如果你将它们作为 props 或 partials 传递,它们也会具有响应性。它们可能会被绑定到下游的不同变量,导致应用程序另一端的某个无害赋值发生更新。
function App() {
// create a mutable state object
const state = createMutable({
users: [{
firstName: "John",
lastName: "Smith"
}]
});
return <A users={state.users} />
}
function A(props) {
<B user={props.users[0]} />
}
function B(props) {
createEffect(() => {
const person = props.user;
// do some stuff calculations
Object.assign(person, calculateScore(person))
})
return <div>{person}</div>
}
此时,分配者calculateScore
甚至知道存在哪些新属性,或者我们是否更新了现有属性,或者其他地方是否依赖于用户的某些字段。
我们希望将赋值操作本地化或显式暴露。第一种方式很难通过赋值运算符强制执行,除非像 Svelte 那样编译时忽略响应性,只读代理是不错的第二种选择。关键在于读/写分离。如果你使用 React Hooks,这是一种很常见的模式。现在我们可以传递读取权限,而无需传递更新权限。
const [state, setState] = createState({
users: [{
firstName: "John",
lastName: "Smith"
}]
});
state.users[0].firstName = "Jake"; // nope
// you need be passed the setter
setState("users", 0, { firstName: "Jake" }); // yes
3. 没有isSignal
/ isObservable
/isRef
这是响应式系统的基本组成部分吗?难道你不需要知道你在处理什么吗?我宁愿你不知道。
原因比你想象的要简单。每次导出值时,创建一个响应式表达式,我不希望你把它包装在原语中。Solid 不会将传递给子组件的表达式包装在响应式原语中,你为什么要这么做呢?
// with memo
const fullName = createMemo(() =>
`${user.firstName} ${user.lastName}`
);
return <DisplayName name={fullName()} />
// without memo
const fullName2 = () => `${user.firstName} ${user.lastName}`;
return <DisplayName name={fullName()} />
两者几乎完全相同,只是如果<DisplayName>
多次使用 name 字段,第二个方法会重新创建字符串,而第一个方法会返回相同的字符串,直到 name 发生变化。但第一个方法的开销要大得多,尤其是在创建时。除非你正在进行昂贵的计算,否则不值得这么做。
大多数响应式系统都鼓励过度记忆。响应式节点会存储每个原子(包括派生)的值的引用,这包括传递给子组件的表达式。这通常非常浪费。你不需要总是换行。
您可能想知道组件如何处理信号,但我们之前看到过这种情况:
<>
<DisplayName name={fullName()} />
<DisplayName name={state.fullName} />
<DisplayName name={"Homer Simpson"} />
</>
// compiles to:
[createComponent(DisplayName, {
get name() {
return fullName();
}
}), createComponent(DisplayName, {
get name() {
return state.fullName;
}
}), createComponent(DisplayName, {
name: "Homer Simpson"
})];
无论它是否动态,始终都是props.name
如此。根据您的需求编写组件,剩下的交给 Solid 处理。完整示例请见此处。
4. 更新同步
好吧,这或许是意料之中的。毕竟,你希望你的响应式库是同步且无故障的。比如,当你更新一个值时,你希望它以一致的方式反映所有更新。你肯定不希望最终用户与不同步的信息进行交互。
function App() {
let myEl;
const [count, setCount] = createSignal(0);
const doubleCount = createMemo(() => count() * 2);
return (
<button
ref={myEl}
onClick={() => {
setCount(count() + 1);
console.log(count(), doubleCount(), myEl.textContent);
}
}>
{doubleCount()}
</button>
);
}
事实证明,不同的框架处理这个问题的方式不同。当你点击时,它们都会记录不同的内容**。
React:“0 0 0”
Vue:“1 2 0”
Svelte:“1 0 0”
Solid:“1 2 2”
哪个符合你的预期?这里只有两个库是一致的。只有 React 和 Solid 显示的数据没有不同步。React 在提交其批处理异步操作之前不会读取更新的值。Solid 在下一行之前就已经更新了 DOM。另外两个库在独立的响应式执行时间(Vue)和典型的 JS 执行(Svelte)之间进行选择。但它们并非没有故障。
您可能会想,如果 Solid 有多个更新,效率会不会很低?即使粒度更新可以最大限度地减少这种情况,Solid 仍然可能出现。我们有一个batch
助手程序可以记录所有更新,并在最后进行回放。SolidsetState
会自动批量处理其更改,并且在效果执行期间也会对更改进行批量处理。
onClick={() => {
batch(() => {
setCount(count() + 1);
console.log(count(), doubleCount(), myEl.textContent);
});
}
您问这记录了什么?
“0 0 0”。在批次内部,Solid 的工作原理与 React 类似,能够实现无故障的一致性。点击此处查看实际效果。
5. 没有取消订阅
对于使用过其他响应式库的人来说,最后一个肯定不太常见。Solid 的响应式系统虽然独立于渲染,但也有一些限制。
首先,Solid 的设计使其能够在重新评估时自动处理其所拥有的嵌套原语的嵌套订阅处置。这样,我们就可以自由嵌套,而不会出现内存泄漏。
就像这个例子一样。提取重要部分:
const [s1, setS1] = createSignal(0);
const [s2, setS2] = createSignal(0);
createEffect(() => {
console.log("Outer", s1());
createEffect(() => {
console.log("Inner", s2());
onCleanup(() => console.log("Inner Clean"));
});
onCleanup(() => console.log("Outer Clean"));
})
更新s1
实际上会清除内部和外部的效果,并重新运行外部并重新创建内部。这是 Solid 渲染的核心。组件清理只是清除其嵌套的反应式上下文。
其次,Solid 是同步的,但它仍然会安排更新。我们会在其余响应式计算完成后再执行 effect。这样,我们既可以处理诸如挂载钩子之类的操作,而不必依赖于 DOM,又可以执行诸如并发渲染之类的操作,在这种情况下,我们会推迟应用副作用,直到所有异步更新都提交为止。为了同步排队和执行,我们需要一个包装器。
我们使用 来实现这一点createRoot
。你可能永远不需要它,因为render
它会帮你调用,复杂的控制流会在后台处理这个问题。但如果你想在响应式代码树之外创建订阅机制,只需创建另一个根即可。Solidsubscribe
的辅助方法如下所示:
function subscribe(fn, callback) {
let dispose;
createRoot((disposer) => {
dispose = disposer;
createEffect(() => callback(fn()));
})
return dispose;
}
// somewhere else
subscribe(() => state.data, (data) => console.log("Data updated"));
结论
Solid 或许主要因为其高性能而引人注目,但它的设计和身份也经过了深思熟虑。它或许看起来很熟悉,但它建立在前作的基础上。乍一看,它确实有点不寻常,但我希望你也能像我一样爱上它。
在 github 上查看 Solid:https://github.com/ryansolid/solid
** 封面图片来自Elena11/Shutterstock
** 此分析是在开发新版本的 MarkoJS时进行的。
文章来源:https://dev.to/ryansolid/5-ways-solidjs-differs-from-other-js-frameworks-1g63