Skip to content

Commit

Permalink
255
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyuan.1 committed Aug 29, 2022
1 parent 08d75fe commit 4b7d3a3
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@
- <a href="./前沿技术/240.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20useEvent%20RFC%E3%80%8B.md">240.精读《React useEvent RFC》</a>
- <a href="./前沿技术/242.%E7%B2%BE%E8%AF%BB%E3%80%8Aweb%20reflow%E3%80%8B.md">242.精读《web reflow》</a>
- <a href="./前沿技术/253.%E7%B2%BE%E8%AF%BB%E3%80%8Apnpm%E3%80%8B.md">253.精读《pnpm》</a>
- <a href="./前沿技术/254.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E7%9A%84%E7%90%86%E8%A7%A3%20-%20%E5%88%86%E5%B1%82%E4%B8%8E%E6%8A%BD%E8%B1%A1%E3%80%8B.md">254.精读《对前端架构的理解 - 分层与抽象》</a>
- <a href="./前沿技术/255.%E7%B2%BE%E8%AF%BB%E3%80%8ASolidJS%E3%80%8B.md">255.精读《SolidJS》</a>

### TS 类型体操

Expand All @@ -205,7 +207,6 @@
- <a href="./TS 类型体操/250.%E7%B2%BE%E8%AF%BB%E3%80%8AFlip%2C%20Fibonacci%2C%20AllCombinations...%E3%80%8B.md">250.精读《Flip, Fibonacci, AllCombinations...》</a>
- <a href="./TS 类型体操/251.%E7%B2%BE%E8%AF%BB%E3%80%8ATrim%20Right%2C%20Without%2C%20Trunc...%E3%80%8B.md">251.精读《Trim Right, Without, Trunc...》</a>
- <a href="./TS 类型体操/252.%E7%B2%BE%E8%AF%BB%E3%80%8AUnique%2C%20MapTypes%2C%20Construct%20Tuple...%E3%80%8B.md">252.精读《Unique, MapTypes, Construct Tuple...》</a>
- <a href="./前沿技术/254.%E7%B2%BE%E8%AF%BB%E3%80%8A%E5%AF%B9%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E7%9A%84%E7%90%86%E8%A7%A3%20-%20%E5%88%86%E5%B1%82%E4%B8%8E%E6%8A%BD%E8%B1%A1%E3%80%8B.md">254.精读《对前端架构的理解 - 分层与抽象》</a>

### 设计模式

Expand Down
264 changes: 264 additions & 0 deletions 前沿技术/255.精读《SolidJS》.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
[SolidJS](https://github.com/solidjs/solid) 是一个语法像 React Function Component,内核像 Vue 的前端框架,本周我们通过阅读 [Introduction to SolidJS](https://www.loginradius.com/blog/engineering/guest-post/introduction-to-solidjs/) 这篇文章来理解理解其核心概念。

为什么要介绍 SolidJS 而不是其他前端框架?因为 SolidJS 在教 React 团队正确的实现 Hooks,这在唯 React 概念与虚拟 DOM 概念马首是瞻的年代非常难得,这也是开源技术的魅力:任何观点都可以被自由挑战,只要你是对,你就可能脱颖而出。

## 概述

整篇文章以一个新人视角交代了 SolidJS 的用法,但本文假设读者已有 React 基础,那么只要交代核心差异就行了。

### 渲染函数仅执行一次

SolidJS 仅支持 FunctionComponent 写法,无论内容是否拥有状态管理,也无论该组件是否接受来自父组件的 Props 透传,都仅触发一次渲染函数。

所以其状态更新机制与 React 存在根本的不同:

- React 状态变化后,通过重新执行 Render 函数体响应状态的变化。
- Solid 状态变化后,通过重新执行用到该状态代码块响应状态的变化。

与 React 整个渲染函数重新执行相对比,Solid 状态响应粒度非常细,甚至一段 JSX 内调用多个变量,都不会重新执行整段 JSX 逻辑,而是仅更新变量部分:

```jsx
const App = ({ var1, var2 }) => (
<>
var1: {console.log("var1", var1)}
var2: {console.log("var2", var2)}
</>
);
```

上面这段代码在 `var1` 单独变化时,仅打印 `var1`,而不会打印 `var2`,在 React 里是不可能做到的。

这一切都源于了 SolidJS 叫板 React 的核心理念:**面相状态驱动而不是面向视图驱动**。正因为这个差异,导致了渲染函数仅执行一次,也顺便衍生出变量更新粒度如此之细的结果,同时也是其高性能的基础,同时也解决了 React Hooks 不够直观的顽疾,一箭 N 雕。

### 更完善的 Hooks 实现

SolidJS 用 `createSignal` 实现类似 React `useState` 的能力,虽然看上去长得差不多,但实现原理与使用时的心智却完全不一样:

```jsx
const App = () => {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
};
```

我们要完全以 SolidJS 心智理解这段代码,而不是 React 心智理解它,虽然它长得太像 Hooks 了。一个显著的不同是,将状态代码提到外层也完全能 Work:

```jsx
const [count, setCount] = createSignal(0);
const App = () => {
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
};
```

这是最快理解 SolidJS 理念的方式,即 SolidJS 根本没有理 React 那套概念,SolidJS 理解的数据驱动是纯粹的数据驱动视图,无论数据在哪定义,视图在哪,都可以建立绑定。

这个设计自然也不依赖渲染函数执行多次,同时因为使用了依赖收集,也不需要手动申明 deps 数组,也完全可以将 `createSignal` 写在条件分支之后,因为不存在执行顺序的概念。

### 派生状态

用回调函数方式申明派生状态即可:

```jsx
const App = () => {
const [count, setCount] = createSignal(0);
const doubleCount = () => count() * 2;
return <button onClick={() => setCount(count() + 1)}>{doubleCount()}</button>;
};
```

这是一个不如 React 方便的点,因为 React 付出了巨大的代价(在数据变更后重新执行整个函数体),所以可以用更简单的方式定义派生状态:

```jsx
// React
const App = () => {
const [count, setCount] = useState(0);
const doubleCount = count * 2; // 这块反而比 SolidJS 定义的简单
return (
<button onClick={() => setCount((count) => count + 1)}>
{doubleCount}
</button>
);
};
```

当然笔者并不推崇 React 的衍生写法,因为其代价太大了。我们继续分析为什么 SolidJS 这样看似简单的衍生状态写法可以生效。原因在于,SolidJS 收集所有用到了 `count()` 的依赖,而 `doubleCount()` 用到了它,而渲染函数用到了 `doubleCount()`,仅此而已,所以自然挂上了依赖关系,这个实现过程简单而稳定,没有 Magic。

SolidJS 还支持衍生字段计算缓存,使用 `createMemo`

```jsx
const App = () => {
const [count, setCount] = createSignal(0);
const doubleCount = () => createMemo(() => count() * 2);
return <button onClick={() => setCount(count() + 1)}>{doubleCount()}</button>;
};
```

同样无需写 deps 依赖数组,SolidJS 通过依赖收集来驱动 `count` 变化影响到 `doubleCount` 这一步,这样访问 `doubleCount()` 时就不用总执行其回调的函数体,产生额外性能开销了。

### 状态监听

对标 React 的 `useEffect`,SolidJS 提供的是 `createEffect`,但相比之下,不用写 deps,是真的监听数据,而非组件生命周期的一环:

```jsx
const App = () => {
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count()); // 在 count 变化时重新执行
});
};
```

这再一次体现了为什么 SolidJS 有资格 “教” React 团队实现 Hooks:

- 无 deps 申明。
- 将监听与生命周期分开,React 经常容易将其混为一谈。

在 SolidJS,生命周期函数有 `onMount``onCleanUp`,状态监听函数有 `createEffect`;而 React 的所有生命周期和状态监听函数都是 `useEffect`,虽然看上去更简洁,但即便是精通 React Hooks 的老手也不容易判断哪些是监听,哪些是生命周期。

### 模板编译

为什么 SolidJS 可以这么神奇的把 React 那么多历史顽疾解决掉,而 React 却不可以呢?核心原因还是在 SolidJS 增加的模板编译过程上。

以官方 [Playground](https://playground.solidjs.com/) 提供的 Demo 为例:

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

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

被编译为:

```jsx
const _tmpl$ = /*#__PURE__*/ template(`<button type="button"></button>`, 2);

function Counter() {
const [count, setCount] = createSignal(0);

const increment = () => setCount(count() + 1);

return (() => {
const _el$ = _tmpl$.cloneNode(true);

_el$.$$click = increment;

insert(_el$, count);

return _el$;
})();
}
```

首先把组件 JSX 部分提取到了全局模板。初始化逻辑:将变量插入模板;更新状态逻辑:由于 `insert(_el$, count)` 时已经将 `count``_el$` 绑定了,下次调用 `setCount()` 时,只需要把绑定的 `_el$` 更新一下就行了,而不用关心它在哪个位置。

为了更完整的实现该功能,必须将用到模板的 Node 彻底分离出来。我们可以测试一下稍微复杂些的场景,如:

```jsx
<button>
count: {count()}, count+1: {count() + 1}
</button>
```

这段代码编译后的模板结果是:

```jsx
const _el$ = _tmpl$.cloneNode(true),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling;
_el$4.nextSibling;

_el$.$$click = increment;

insert(_el$, count, _el$4);

insert(_el$, () => count() + 1, null);
```

将模板分成了一个整体和三个子块,分别是字面量、变量、字面量。为什么最后一个变量没有加进去呢?因为最后一个变量插入直接放在 `_el$` 末尾就行了,而中间插入位置需要 `insert(_el$, count, _el$4)` 给出父节点与子节点实例。

## 精读

SolidJS 的神秘面纱已经解开了,下面笔者自问自答一些问题。

### 为什么 createSignal 没有类似 hooks 的顺序限制?

React Hooks 使用 deps 收集依赖,在下次执行渲染函数体时,因为没有任何办法标识 “deps 是为哪个 Hook 申明的”,只能依靠顺序作为标识依据,所以需要稳定的顺序,因此不能出现条件分支在前面。

而 SolidJS 本身渲染函数仅执行一次,所以不存在 React 重新执行函数体的场景,而 `createSignal` 本身又只是创建一个变量,`createEffect` 也只是创建一个监听,逻辑都在回调函数内部处理,而与视图的绑定通过依赖收集完成,所以也不受条件分支的影响。

### 为什么 createEffect 没有 useEffect 闭包问题?

因为 SolidJS 函数体仅执行一次,不会存在组件实例存在 N 个闭包的情况,所以不存在闭包问题。

### 为什么说 React 是假的响应式?

React 响应的是组件树的变化,通过组件树自上而下的渲染来响应式更新。而 SolidJS 响应的只有数据,甚至数据定义申明在渲染函数外部也可以。

所以 React 虽然说自己是响应式,但开发者真正响应的是 UI 树的一层层更新,在这个过程中会产生闭包问题,手动维护 deps,hooks 不能写在条件分支之后,以及有时候分不清当前更新是父组件 rerender 还是因为状态变化导致的。

这一切都在说明,React 并没有让开发者真正只关心数据的变化,如果只要关心数据变化,那为什么组件重渲染的原因可能因为 “父组件 rerender” 呢?

### 为什么 SolidJS 移除了虚拟 dom 依然很快?

虚拟 dom 虽然规避了 dom 整体刷新的性能损耗,但也带来了 diff 开销。对 SolidJS 来说,它问了一个问题:为什么要规避 dom 整体刷新,局部更新不行吗?

对啊,局部更新并不是做不到,通过模板渲染后,将 jsx 动态部分单独提取出来,配合依赖收集,就可以做到变量变化时点对点的更新,所以无需进行 dom diff。

### 为什么 signal 变量使用 `count()` 不能写成 `count`

笔者也没找到答案,理论上来说,Proxy 应该可以完成这种显式函数调用动作,除非是不想引入 Mutable 的开发习惯,让开发习惯变得更加 Immutable 一些。

### props 的绑定不支持解构

由于响应式特性,解构会丢失代理的特性:

```jsx
//
const App = (props) => <div>{props.userName}</div>;
//
const App = ({ userName }) => <div>{userName}</div>;
```

虽然也提供了 `splitProps` 解决该问题,但此函数还是不自然。该问题比较好的解法是通过 babel 插件来规避。

### createEffect 不支持异步

没有 deps 虽然非常便捷,但在异步场景下还是无解:

```jsx
const App = () => {
const [count, setCount] = createSignal(0);
createEffect(() => {
async function run() {
await wait(1000);
console.log(count()); // 不会触发
}
run();
});
};
```

## 总结

SolidJS 的核心设计只有一个,即让数据驱动真的回归到数据上,而非与 UI 树绑定,在这一点上,React 误入歧途了。

虽然 SolidJS 很棒,但相关组件生态还没有起来,巨大的迁移成本是它难以快速替换到生产环境的最大问题。前端生态想要无缝升级,看来第一步是想好 “代码范式”,以及代码范式间如何转换,确定了范式后再由社区竞争完成实现,就不会遇到生态难以迁移的问题了。

但以上假设是不成立的,技术迭代永远都以 BreakChange 为代价,而很多时候只能抛弃旧项目,在新项目实践新技术,就像 Jquery 时代一样。

> 讨论地址是:[精读《SolidJS》· Issue #438 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/438)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)

0 comments on commit 4b7d3a3

Please sign in to comment.