3

SolidJS硬气的说:我比React还react

 2 years ago
source link: https://segmentfault.com/a/1190000040275257
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SolidJS硬气的说:我比React还react

大家好,我是卡颂。

最近刷推时,有个老哥经常出现在前端框架相关推文下。

我想:“老哥你哪位?”

一查,原来是个框架作者,作品叫SolidJS

翻翻框架介绍,这句话成功吸引我的注意:

支持现代前端特性,例如:JSX, Fragments, Context, Portals, Suspense, Streaming SSR, Progressive Hydration, Error Boundaries和Concurrent Rendering

我琢磨您不会是React在逃公主吧?这不能说和React类似,只能说完全一样吧?

作为传统中国人,秉承来都来了思想,我试用了一天,又看了下源码,结果发现这个框架真是个宝藏框架。

本文会比较SolidJSReact的异同,阐述他的独特优势,看完后不知道你会不会和我发出同样的感叹:

这简直比React还react(react译为响应)

相信看完本文后,不仅能认识一个新框架,还能对React有更深的认识。

初看很相似

让我们从一个计数器的例子看看与React语法的差异:


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

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

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

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

React不同的地方:

  • useState改名成createSignal
  • 获取count状态从React中直接使用count变为通过方法调用,即:count()

难道仅仅是一个类React框架?

别急,让我们从编译时运行时响应原理三方面来看看。

编译时大不同

React的编译时很,基本只是编译JSX语法。

SolidJS则采用了类似Svelte的方案:在编译时,将状态更新编译为独立的DOM操作方法。

这样做有什么好处?主要有两点。

一定条件下的体积优势

你不需要为你没使用的代码付出代价

使用React时,即使没有用到Hooks,其代码也会出现在最终编译后的代码中。

而在SolidJS中,未使用的功能不会出现在编译后的代码中。

举个例子,上面计时器的例子中,编译后的代码有一行是这样:

delegateEvents(["click"]);

这行代码的目的是在document上注册click事件代理。

如果在计时器中没有使用onClick,那么编译后代码中就不会有这一行。

有热心网友对比了类似编译时方案的SvelteReact之间源代码编译后代码的体积差异。

其中横轴代表源代码体积,纵轴代表编译后代码体积,红色线条代表Svelte,蓝色代表React

可见,在临界值(业务源代码体积达到120kb)之前,编译时方案有一定体积优势。

由于SolidJS使用JSX描述视图,比Svelte使用类似Vue的模版语法更灵活,所以在编译时没法做到Svelte一样的极致编译优化,使得其相比Svelte运行时更重一点。

这为他带来了额外的好处:在真实项目(>120kb)中,SolidJS的代码体积比Svelte小25%左右。

还真是,因祸得福?

更快的更新速度

我们知道,在ReactVue中存在一层虚拟DOMReact中叫Fiber树)。

每当发生更新,虚拟DOM会进行比较(Diff算法),比较的结果会执行不同的DOM操作(增、删、改)。

SolidJSSvelte在发生更新时,可以直接调用编译好的DOM操作方法,省去了虚拟DOM比较这一步所消耗的时间。

举个例子,上文的计时器,当点击后,从触发更新到视图变化的调用栈如下:

触发事件,更新状态,更新视图,一路调用走到底,清晰明了。

同样的例子放到React中,调用栈如下:

左中右红、绿、蓝框调用栈分别对应:

  • 对比并生成Fiber
  • 根据对比结果执行DOM操作

可见,SolidJS的更新路径比React短很多。

你问凭什么?这还得从其特殊的响应原理聊起。

假设有个状态name,初始值为KaSong。我们希望根据name渲染一个div

SolidJS编译后的代码类似:

const [name, setName] = createSignal("KaSong");

const el = document.createElement("div");
createEffect(() => el.textContent = name());

其中createEffect类似ReactuseEffect

由于其回调内依赖了name,所以当name改变后会触发createEffect回调,改变el.textContent,造成DOM更新。

类似React的:

useEffect(() => {
  el.textContent = name;
}, [name])

首屏渲染结果:

<div>KaSong</div>

接下来,触发更新:

setName("XiaoMing") 

更新后结果:

<div>XiaoMing</div>

为什么更新name后会触发createEffect

这里也没有什么黑魔法,就是订阅发布

createEffect回调依赖name,所以会订阅name的变化。

由于篇幅有限,实现细节咱下回细聊。

这里的关键在于,SolidJS的状态具有原子性

即状态互相之间有依赖关系,他们形成局部的依赖图。当改变一个状态后,依赖图中的其他状态也会改变。

createEffect中如果使用了这些依赖,就会订阅他们的变化。

当状态改变后,createEffect回调会执行,进而执行具体的DOM方法,更新视图。

响应式更新,指哪打哪,李云龙直呼内行。

有同学会问,React不是这样么?

那我问你个问题:

为什么Hooks会有调用顺序不能变的要求?

为什么useEffect回调会有闭包问题?

答案已经呼之欲出了:React只有在这些限制下才能实现响应式

辛劳苦干React

有一个可能反直觉的知识:React并不关心哪个组件触发了更新。

React中,任何一个组件触发更新(如调用this.setState),所有组件都会重新走一遍流程。因为需要构建一棵新的Fiber树。

为了减少无意义的renderReact内部有些优化策略用来判断组件是否可以复用上次更新的Fiber节点(从而跳过render)。

同时,也提供了很多API(比如:useMemoPureComponent...),让开发者告诉他哪些组件可以跳过render

如果说,SolidJS的更新流程像一个画家,画面中哪儿需要更新就往哪儿画几笔。

那么React的更新流程像是一个人拿相机拍一张照片,再拿这张照片和上次拍的照片找不同,最后把不同的地方更新了。

今天,我们聊了SolidJSReact的差异,主要体现在三方面:

不知道你喜欢这款:没有Hooks顺序限制、没有useEffect闭包问题、没有Fiber树、比Reactreact的框架么?

如果你问我选哪个?当然,哪个给工资高我用哪个。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK