SolidJS硬气的说:我比React还react

SegmentFault

共 3775字,需浏览 8分钟

 · 2021-09-14

作者:卡颂
来源:SegmentFault 思否社区


大家好,我是卡颂。

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


我想:“老哥你哪位?”

一查,原来是个框架作者,作品叫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的框架么?
如果你问我选哪个?当然,哪个给工资高我用哪个。


点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报