diff --git a/readme.md b/readme.md index 4b4d0a20..85a208dd 100644 --- a/readme.md +++ b/readme.md @@ -192,6 +192,8 @@ - 240.精读《React useEvent RFC》 - 242.精读《web reflow》 - 253.精读《pnpm》 +- 254.精读《对前端架构的理解 - 分层与抽象》 +- 255.精读《SolidJS》 ### TS 类型体操 @@ -205,7 +207,6 @@ - 250.精读《Flip, Fibonacci, AllCombinations...》 - 251.精读《Trim Right, Without, Trunc...》 - 252.精读《Unique, MapTypes, Construct Tuple...》 -- 254.精读《对前端架构的理解 - 分层与抽象》 ### 设计模式 diff --git "a/\345\211\215\346\262\277\346\212\200\346\234\257/255.\347\262\276\350\257\273\343\200\212SolidJS\343\200\213.md" "b/\345\211\215\346\262\277\346\212\200\346\234\257/255.\347\262\276\350\257\273\343\200\212SolidJS\343\200\213.md" new file mode 100644 index 00000000..21e46df6 --- /dev/null +++ "b/\345\211\215\346\262\277\346\212\200\346\234\257/255.\347\262\276\350\257\273\343\200\212SolidJS\343\200\213.md" @@ -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 ; +}; +``` + +我们要完全以 SolidJS 心智理解这段代码,而不是 React 心智理解它,虽然它长得太像 Hooks 了。一个显著的不同是,将状态代码提到外层也完全能 Work: + +```jsx +const [count, setCount] = createSignal(0); +const App = () => { + return ; +}; +``` + +这是最快理解 SolidJS 理念的方式,即 SolidJS 根本没有理 React 那套概念,SolidJS 理解的数据驱动是纯粹的数据驱动视图,无论数据在哪定义,视图在哪,都可以建立绑定。 + +这个设计自然也不依赖渲染函数执行多次,同时因为使用了依赖收集,也不需要手动申明 deps 数组,也完全可以将 `createSignal` 写在条件分支之后,因为不存在执行顺序的概念。 + +### 派生状态 + +用回调函数方式申明派生状态即可: + +```jsx +const App = () => { + const [count, setCount] = createSignal(0); + const doubleCount = () => count() * 2; + return ; +}; +``` + +这是一个不如 React 方便的点,因为 React 付出了巨大的代价(在数据变更后重新执行整个函数体),所以可以用更简单的方式定义派生状态: + +```jsx +// React +const App = () => { + const [count, setCount] = useState(0); + const doubleCount = count * 2; // 这块反而比 SolidJS 定义的简单 + return ( + + ); +}; +``` + +当然笔者并不推崇 React 的衍生写法,因为其代价太大了。我们继续分析为什么 SolidJS 这样看似简单的衍生状态写法可以生效。原因在于,SolidJS 收集所有用到了 `count()` 的依赖,而 `doubleCount()` 用到了它,而渲染函数用到了 `doubleCount()`,仅此而已,所以自然挂上了依赖关系,这个实现过程简单而稳定,没有 Magic。 + +SolidJS 还支持衍生字段计算缓存,使用 `createMemo`: + +```jsx +const App = () => { + const [count, setCount] = createSignal(0); + const doubleCount = () => createMemo(() => count() * 2); + return ; +}; +``` + +同样无需写 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 ( + + ); +} +``` + +被编译为: + +```jsx +const _tmpl$ = /*#__PURE__*/ template(``, 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 + +``` + +这段代码编译后的模板结果是: + +```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) =>
{props.userName}
; +// ❎ +const App = ({ userName }) =>
{userName}
; +``` + +虽然也提供了 `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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** + +> 关注 **前端精读微信公众号** + + + +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))