漫谈SolidJS和响应式框架

介绍

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

image.png

  • 性能强大
  • 支持JSX,工程上相对灵活
  • 接口设计贴近React

BenchMark

image.pngimage.png

Count Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => {
setCount(count() + 1);
};


return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}


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

Palyground https://playground.solidjs.com/

为什么SolidJS可以既要又要🐶

现在大多数的框架都基本实现了数据驱动的能力。即满足以下函数要求

$UI = F(state)$
Ajax技术产生之后,我们一直在追求如何更加高效的去根据新数据来精准更新DOM结构Jquery等工具库的产生就是在这样的背景之下。

1
2
3
4
5
6
7
8
9
10
11
12
13
const template = document.createElement('template')
template.innerHTML = '<div><button id="button">1</button></div>'
let count = 0;
const increment = () => {
count++
document.querySelector('#button').firstChild.data = count
};
const node = template.content.cloneNode(true);
document.body.appendChild(node)
document.querySelector('#button').addEventListener('click',()=>{
increment()
console.log('click')
})

数据驱动的能力让我们更加专注业务逻辑产生的State变化,而不用考虑State变化之后视图如何发生变化。更加具体一点,从State变化到UI产生变动过程,我们把他称为**响应式Reactivity**

SolidJS能既要又要的核心就是独特的响应式的实现

预编译和约束性的JSX

预编译通常和模板语法一起出现,因为模板的静态可以更好的分析结构和依赖State细节,让框架更加好的追踪到 **细粒度的变量和视图的依赖关系****. **Svelte 就是如此来做到所称的

外科手术一样更新 DOM

Svelte3.0版本甚至还将预编译用到了State的生成过程,将依赖链做的更长。
Vue 也是使用模板语法,但是却没有做依赖的分析去提供响应式,只是利用模板语法的静态性来做运行时的性能优化。一个比较突出的优化特点,就是Vue在的循环元素的更新上性能远远超越React。
image.png
因为VueDOM DIFF过程中已经从模板预编译中已经得知了标签的确定性,减少了许多对比过程。
那么VueSvelte不同的依赖收集方案谁更加优劣呢?将在后续的章节进行介绍。

如上我们可以看到预编译通常都是从模板语法中获得收益,** **那么**SolidJS**是如何从JSX中获取到响应式的特性呢?

一方面和Svelte通过JSX静态的部分来提取节点和变量的绑定关系,对于变量标签和代码块SolidJS则会选择降级处理,当然框架本身并不推荐使用代码块来形描述UI结构。而是推荐使用[控制流](https://www.solidjs.com/docs/latest/api#control-flow)来写JSX中的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { render } from "solid-js/web";
import { createSignal,For } from "solid-js";

function Counter() {
const [count, setCount] = createSignal(1);
const [arr,setArr] = createSignal([1,2,3])
const increment = () => {
console.log(1);
setCount(count() + 1);
};

return (
<div>
<button type="button" onClick={increment}>
{count()}{count()}

</button>
<div>
{
arr().map(item=>{
return (
<div>{item}</div>
)
})
}

<For each={arr} >
{(item) => <div>{item}</div>}
</For>


</div>

</div>
);
}

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

No Virtual Dom

SolidJS没有采用 Virtual DOM的方案一个明显的好处就在大幅降低内存占用。框架不用去维护整个应用的DOM结构,以及Diff过程中的一些临时变量的内存占用。内存指标在移动端应用中尤为重要。这对于移动端的应用是一个重大的利好。
没有Virtual DOM的同时也就缩短了 State变化到视图更新的步骤和堆栈调用所需要的时间。
那为什么Vue2.x之后也要采用了VDOM呢,明明他已经能够获取到State和视图模板中的细粒度绑定关系了。
其中一个很原因还是性能问题,Vue1.x的响应式原理中大量采用了Observer``Dep``Watcher等观察者模式元件,这些元件的创建就需要很大的成本,尤其是遇到深层次的大对象,其次元件要不断地根据组件的状态,去维护之间的关联,比如取消已经消失组件的订阅。这就要耗费大量的堆栈来做这件事情。所以细粒度的去建立绑定响应关系会遇到性能瓶颈。
image.png
Vue2.x中将绑定的粒度提升到组件级别,减少了Wathcer,在通过Virtual DOM Diff完成细粒度的更新。

SolidJS中处理信号订阅的一个性能优化方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 维护订阅关系
if (Listener) {
const sSlot = this.observers ? this.observers.length : 0;
if (!Listener.sources) {
Listener.sources = [this];
Listener.sourceSlots = [sSlot];
} else {
Listener.sources.push(this);
Listener.sourceSlots!.push(sSlot);
}
if (!this.observers) {
this.observers = [Listener];
this.observerSlots = [Listener.sources.length - 1];
} else {
this.observers.push(Listener);
this.observerSlots!.push(Listener.sources.length - 1);
}
}


// 取消订阅关系
// cleanNode
if ((node as Computation<any>).sources) {
while ((node as Computation<any>).sources!.length) {
const source = (node as Computation<any>).sources!.pop()!,
index = (node as Computation<any>).sourceSlots!.pop()!,
obs = source.observers;
if (obs && obs.length) {
const n = obs.pop()!,
s = source.observerSlots!.pop()!;
if (index < obs.length) {
n.sourceSlots![s] = index;
obs[index] = n;
source.observerSlots![index] = s;
}
}
}
}

所以SolidJS 为什么可以抛弃VDOM呢?

一方面SolidJS 已经从JSX中获取到了节点级细粒度的绑定关系,能力上可以抛弃VDOM直接更新视图。所以我们主要从性能上讲。

不可变性

SolidJS将一个对象整体作为一个Signal,这其实是借鉴了React的思想。React Component State作为一个整体来进行更新,所以SolidJS也是一个单向数据流动
我们需要通过 setXX方式来主动触发信号更新,相对比与Vue为每一个变量设立Observer还需要递归遍历属性的的方案来说,大幅度降低了内存和成本。虽然是单向数据流动,但是“无规则限制的Hook“在工程上和双向数据流动也不缺少便利性。

函数式组件仅执行一次

不同于React的函数式组件,组件状态会引起函数不断执行,SolidJS的函数式组件仅会被执行一次,节点和响应式变量的依赖关系从一开始就确定了,通过减少创建响应式绑定关系的次数来缩减细粒度节点的性能成本。

闭包实现的状态缓存

还是回到响应式的依赖链的创建方式上来说。不同于React使用链表和强制Hook规则来实现的状态缓存,SolidJS采用了闭包这样的形式去缓存一些的响应式绑定关系。一方面降低了不必要的复杂结构和引用关系,另一方面也接触了状态和某个组件视图强绑的问题,让State变成一个不和视图生命周期绑定的纯响应式变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function createSignal<T>(
value?: T,
options?: SignalOptions<T | undefined>
): Signal<T | undefined> {
options = options ? Object.assign({}, signalOptions, options) : signalOptions;

const s: SignalState<T | undefined> = {
value,
observers: null,
observerSlots: null,
comparator: options.equals || undefined
};
const setter: Setter<T | undefined> = (value?: unknown) => {
return writeSignal(s, value);
};

return [readSignal.bind(s), setter];
}

“No Component”

React``Vue``Svelte等框架中都有组件概念,从框架设计角度上来说一个很重要的原因是为了组件给响应式依赖找一个容器或者说是边界。而组件的生命周期则可以来更新响应式的订阅关系。生命周期一定带来了重复的运算过程,变量进出栈,等等。这是一个重要的性能瓶颈。而SolidJS在运行时没有组件概念,只维护了响应式依赖关系。

useMemo 真的能提高性能嘛?链接

SolidJS选择将响应式依赖的维护交给开发者手动维护,像RX.JS一样提供响应式的API去建立Reactive Graph

延迟计算

SolidJS的框架中充斥了大量延迟计算用法,尤其是在组件传递属性的过程当中。由于SolidJS不允许解构Component Props,也使得开发者最后才回去引用父组件给到值。
image.png

VS Svelte

相对比Svelte纯静态编译框架来说,SolidJS在性能上也强出一头。那么强在哪里呢。

JSX连续块的分析

对于JSX中连续块的分析,可以加快创建速度和减少引用。
SolidJS
image.png
Svelete
image.png
可以明显的看出Svelte没有对于JSX连续块的分析,没有办法使用cloneNode来加快创建速度,同时也增加了对于无用层级的闭包引用。这是不优于SolidJS的。

依赖收集方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
let name = 'world';
let disheng = 'disheng'
let isWorld = false
function change(){
name = 'a'
}
</script>

<h1>Hello {isWorld ? name : disheng}!</h1>

<button on:click={change}>
change
</button>

https://svelte.dev/examples/hello-world
这是Svelte非常简单的的Demo,这是静态编译框架的缺点,就是依赖绑定关系的确定完全由静态分析得出。这样对于逻辑表达式中的所有变量都进行了收集。
像上面的Demo一样,虽然没有用到name变量,但是改变name引用依旧会引发一系列的响应,这都是在进行无效的堆栈进出。反观SolidJS或者Vue在运行时去收集依赖,只会收集isWorlddisheng这两个变量,name变量的更改并不会去触发无效过程。

总结

SolidJS是一个没有历史包袱的框架,大量的借鉴了各种优秀框架的经验,通过一系列的局部优化达到了工程上和性能上的最优解。这十分值得我们借鉴和学习。


漫谈SolidJS和响应式框架
https://disheng.site/2022/11/12/rambling-on-about-reactive-frameworks/
作者
笛声
发布于
2022年11月12日
许可协议