每日一博 | 聊聊前端框架的未来 Signals
Signals 在目前前端框架的选型中遥遥领先!
国庆节前最后一周在 Code Review 新同学的 React 代码,发现他想通过 memo 和 useCallback 只渲染被修改的子组件部分。事实上该功能在 React 中是难以做到的。因为 React 状态变化后,会重新执行 render 函数。也就是在组件中调用 setState 之后,整个函数将会重新执行一次。
React 本身做不到。但是基于 Signals 的框架却不会这样,它通过自动状态绑定和依赖跟踪使得当前状态变化后仅仅只会重新执行用到该状态代码块。
个人当时没有过多的解释这个问题,只是匆匆解释了一下 React 的渲染机制。在这里做一个 Signals 的梳理。
优势
对比 React,基于 Signals 的框架状态响应粒度非常细。这里以 Solid 为例:
import { createSignal, onCleanup } from "solid-js"; const CountingComponent = () => { // 创建一个 signal const [count, setCount] = createSignal(0); // 创建一个 signal const [count2] = createSignal(666); // 每一秒递增 1 const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); // 组件销毁时清除定时器 onCleanup(() => clearInterval(interval)); return ( <div> <div> count: {count()} {console.log("count is", count())} </div> <div> count2: {count2()} {console.log("count2 is", count2())} </div> </div> ); };
上面这段代码在 count 单独变化时,只会打印 count,压根不会打印 count2 数据。
控制台打印如下所示:
- count is 0
- count2 is 666
- count is 1
- count is 2
- ...
从打印结果来看,Solid 只会在最开始执行一次渲染函数,后续仅仅只会渲染更改过的 DOM 节点。这在 React 中是不可能做到的,React 是基于视图驱动的,状态改变会重新执行整个渲染函数,并且 React 完全无法识别状态是如何被使用的,开发者甚至可以通过下面的代码来实现 React 的重新渲染。
const [, forceRender] = useReducer((s) => s + 1, 0);
除了更新粒度细之外,使用 Signals 的框架心智模型也更加简单。其中最大的特点是:开发者完全不必在意状态在哪定义,也不在意对应状态在哪渲染。如下所示:
import { createSignal } from "solid-js"; // 把状态从过组件中提取出来 const [count, setCount] = createSignal(0); const [count2] = createSignal(666); setInterval(() => { setCount((c) => c + 1); }, 1000); // 子组件依然可以使用 count 函数 const SubCountingComponent = () => { return <div>{count()}</div>; }; const CountingComponent = () => { return ( <div> <div> count: {count()} {console.log("count is", count())} </div> <div> count2: {count2()} {console.log("count2 is", count2())} </div> <SubCountingComponent /> </div> ); };
上述代码依然可以正常运行。因为它是基于状态驱动的。开发者在组件内使用 Signal 是本地状态,在组件外定义 Signal 就是全局状态。
Signals 本身不是那么有价值,但结合派生状态以及副作用就不一样了。代码如下所示:
import { createSignal, onCleanup, createMemo, createEffect, onMount, } from "solid-js"; const [count, setCount] = createSignal(0); setInterval(() => { setCount((c) => c + 1); }, 1000); // 计算缓存 const doubleCount = createMemo(() => count() * 2); // 基于当前缓存 const quadrupleCount = createMemo(() => doubleCount() * 2); // 副作用 createEffect(() => { // 在 count 变化时重新执行 fetch fetch(`/api/${count()}`); }); const CountingComponent = () => { // 挂载组件时执行 onMount(() => { console.log("start"); }); // 销毁组件时执行 onCleanup(() => { console.log("end"); }); return ( <div> <div>Count value is {count()}</div> <div>doubleCount value is {doubleCount()}</div> <div>quadrupleCount value is {quadrupleCount()}</div> </div> ); };
从上述代码可以看到,派生状态和副作用都不需要像 React 一样填写依赖项,同时也将副作用与生命周期分开(代码更好阅读)。
实现机制
细粒度,高性能,同时还没有什么限制。不愧被誉为前端框架的未来。那么它究竟是如何实现的呢?
本质上,Signals 是一个在访问时跟踪依赖、在变更时触发副作用的值容器。
这种基于响应性基础类型的范式在前端领域并不是一个特别新的概念:它可以追溯到十多年前的 Knockout observables 和 Meteor Tracker 等实现。Vue 的选项式 API 也是同样的原则,只不过将基础类型这部分隐藏在了对象属性背后。依靠这种范式,Vue2 基本不需要优化就有非常不错的性能。
依赖收集
React useState 返回当前状态和设置值函数,而 Solid 的 createSignal 返回两个函数。即:
type useState = (initial: any) => [state, setter]; type createSignal = (initial: any) => [getter, setter];
为什么 createSignal 要传递 getter 方法而不是直接传递对应的 state 值呢?这是因为框架为了具备响应能力,Signal 必须要收集谁对它的值感兴趣。仅仅传递状态是无法提供 Signal 任何信息的。而 getter 方法不但返回对应的数值,同时执行时创建一个订阅,以便收集所有依赖信息。
模版编译
要保证 Signals 框架的高性能,就不得不结合模版编译实现该功能,框架开发者通过模版编译实现动静分离,配合依赖收集,就可以做到状态变量变化时点对点的 DOM 更新。所以目前主流的 Signals 框架没有使用虚拟 DOM。而基于虚拟 DOM 的 Vue 目前依靠编译器来实现类似的优化。
下面我们先看看 Solid 的模版编译:
const CountingComponent = () => { const [count, setCount] = createSignal(0); const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); onCleanup(() => clearInterval(interval)); return <div>Count value is {count()}</div>; };
对应编译后的的组件代码。
const _tmpl$ = /*#__PURE__*/ _$template(`<div>Count value is `); const CountingComponent = () => { const [count, setCount] = createSignal(0); const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); onCleanup(() => clearInterval(interval)); return (() => { const _el$ = _tmpl$(), _el$2 = _el$.firstChild; _$insert(_el$, count, null); return _el$; })(); };
- 执行 _tmpl$ 函数,获取对应组件的静态模版
- 提取组件中的 count 函数,通过 _$insert 将状态函数和对应模版位置进行绑定
- 调用 setCount 函数更新时,比对一下对应的 count,然后修改对应的 _el$ 对应数据
其他
大家可以看一看使用 Signals 的主流框架:
不过目前来看 React 团队可能不会使用 Signals。
- Signals 性能很好,但不是编写 UI 代码的好方式
- 计划通过编译器来提升性能
- 可能会添加类似 Signals 的原语
PREACT 作者编写了 @preact/signals-react 为 React 提供了 Signals。不过个人不建议在生产环境使用。
篇幅有限,后续个人会解读 @preact/signals-core 的源码。
参考资料
鼓励一下
如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
两行代码解决大语言模型对话局限!港中文贾佳亚团队联合 MIT 发布超长文本扩展技术
中途迷失、模型偷懒、上下文越长大模型越笨......如果体验过大语言模型产品,用户多少会对文本输入长度带来的限制有所感触,比如当想和大模型讨论一些稍长的内容,需要拆分输入,而前面输入的要点,很快就会被大模型忘记。 这是典型的大语言模型对话缺陷!就像先天有注意力缺陷的儿童,难以专注看完一本新书。而缺陷的关键,在于模型缺乏长文本处理能力。这个局面如今被打破。 近日,贾佳亚团队联合MIT发布的新技术和新模型悄然登上各大开源网站的热榜:hugging face热榜第一、paperwithcode热度第一,Github全部python项目热度第五、github stars一周内破千,Twitter上的相关技术帖子浏览量近18万...... github stars已达1.3k Twitter上的相关技术帖子浏览量近18万 这项名为LongLoRA的技术实用但却简单得令人惊讶:只需两行代码、一台8卡A100机器,便可将7B模型的文本长度拓展到100k tokens,70B模型的文本长度拓展到32k tokens;同时,该研究团队还发布了首个拥有70B参数量的长文本对话大语言模型LongAlpaca...
- 下一篇
这道面试题工作中经常碰到,但 99% 的程序员都答不上来
小时候都被问过一个脑筋急转弯,把大象放进冰箱有几个步骤?我们一开始都会抓耳挠腮,去想着该如何把大象塞进冰箱。最终揭晓的答案却根本不关心具体的操作方法,只是提供了 3 个步骤组成的流程,「把冰箱打开,把大象放进去,再把冰箱关上」。而对于每一位开发者来说,变更数据库字段是绕不过去的操作。而当被问及需要几步时,不少人都会脱口而出 1 步, 不就是执行一条 ALTER TABLE 语句嘛。 这当然不是一道脑筋急转弯题,但确实是一道经典的技术面试题,而答 1 步的同学,基本就挂掉了。实际上,一个标准的数据库字段变更操作需要分成很多步,比如给字段重命名,会分成 6 步: 创建一个使用新名字的字段 更新应用,同时双写 (dual-write)旧字段和新字段 把启动双写前,旧字段的数据回填 (backfill) 到新字段 当回填结束后,添加诸如 NOT NULL 之类的约束到新字段 更新应用,移除所有对于旧字段的依赖,只使用新字段 删除旧名字的字段 以上只是一个大致的执行步骤,而具体的执行细节多到值得许多公司都会单独撰文 有关如何做数据库变更的讨论一直也是 HN 上的热点话题 两年多前,Bytebas...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8编译安装MySQL8.0.19
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7设置SWAP分区,小内存服务器的救世主
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库