Signal:更多前端框架的选择

量子计
• 阅读 1592

大家好,我卡颂。

最近,AngularQwik的作者MIŠKO HEVERY发文表示Signal是前端框架的未来,并考虑在Angular中实现它。

在此之前,VueSolid.jsPreactSvelte都已实现Signal。实际上,signal并不是一个新概念,他还有很多别名,比如:

  • 响应式更新
  • 细粒度更新

如果你了解过Vue响应式更新的实现原理,对Signal就不会陌生。

实际上,Signal的技术在10年前Knockout框架中就有应用。为什么这项技术正受到越来越多前端框架的青睐?

本文,让我们一起探讨下这个话题。

欢迎加入人类高质量前端交流群,带飞

signal的本质

signal的本质,是将对状态的引用以及对状态值的获取分离开。这么说可能有点抽象,让我们先看一个非signal的例子。

以下是React中定义状态的方式:

function App() {
  const [state, dispatch] = useState(0);
  return <p onClick={
    () => dispatch(state + 1)
  }>{state}</p>
}

useState的返回值包括两部分:

  • state:状态的值
  • dispatch:状态的setter

可以发现,state耦合了对状态的引用以及对状态值的获取这两个含义。

再来看一个signal的例子。以下是同一个例子用Solid.js书写的样子:

function App() {
  const [getState, dispatch] = createSignal(0);
  return <p onClick={
    () => dispatch(getState() + 1)
  }>{getState()}</p>
}

createSignal的返回值包括两部分:

  • getState:对状态的引用
  • dispatch:状态的setter

区别就体现在getState上,其中:

  • getState是对状态的引用
  • getState()是对状态值的获取

也就是说,我们可以不必立刻获取状态的值,而是在需要的时候再获取(即在需要时再执行getState())。

这么做有什么好处呢?如果我们在需要的时候再获取状态的值,就能感知当前的上下文环境。

举个很粗糙的例子,在下面的代码中,组件实例(Component实例)在render时会将全局变量cpnContext指向自己:

let cpnContext = null;

class Component {
  render() {
    cpnContext = this;
    // ...省略逻辑
  }
}

那么在createSignal返回的getState方法内部,可以获取全局变量cpnContext来感知当前处于哪个组件的渲染流程:

function createSignal() {
  // ...省略逻辑
  function getState() {
    const curContext = cpnContext;
    // ...
  }
  function dispatch {}
  return [getState, dispatch]
}

这么做的目的是建立状态变化需要更新哪个组件之间的联系。

相比于React,基于Signal实现的框架会有两个优势:

  • 更好的细粒度更新性能
  • 更好的DX(开发者体验)

更好的细粒度更新性能

由于Signal建立了状态与组件之间的联系,所以相比于React更有性能优势。

比如,在我的电脑上,用Svelte渲染1w个li,点击某个li后改变他的内容:

<ul>
  {#each items as item (item.id)}
      <li on:click={() => items[item.id].name = 'change!'}>{item.name}</li>
  {/each}
</ul>

从点击事件触发,到Svelte逻辑运行完,再到浏览器重排重绘,总用时18.88ms,其中Svelte的逻辑执行只花了9.5ms:

Signal:更多前端框架的选择

同样的例子用React实现,触发点击后总用时98.5ms,其中React的逻辑执行了89.38ms:

Signal:更多前端框架的选择

在这个例子中,React性能比Svelte差了一个数量级。之所以会有这样的差异,很大一部分原因在于Svlete在更新前就知道状态变化时需要更新哪个组件

而这一切的源头就在于Signal

更好的DX

更好的开发者体验主要体现在两方面:

  1. Signal感知上下文环境的能力减少了代码心智负担

比如在React中,useEffect在使用时需要指明依赖的状态:

useEffect(() => {
  // ...state1, state2变化后的逻辑
}, [state1, state2])

如果采用Signal的实现,状态能感知到自己在useEffect上下文环境,可以自动建立两者之间的联系,不用再担心少写依赖状态、闭包陷阱等问题,减少心智负担。

比如在Vue中,类似useEffect(仅仅是功能类似,两者的用途其实是不同的)的watch,就不需要显式指明依赖:

<script setup>
import { ref, watch } from 'vue'

const name = ref('')

watch(name, (newName, oldName) => {
  // ...省略逻辑
})
</script>
  1. 减少开发者性能优化的心智负担

使用Signal的框架通常能获得不错的运行时性能,所以不需要额外的性能优化API。反观React,开发者如果遇到性能问题,需要手动调用性能优化API(比如React.memouseMemoPureComponent)。

总结

有以上这么多优点,难怪很多框架都使用了Signal。那么ReactSignal是什么态度呢?

React团队成员对此的观点是:

  1. 有可能引入类似Signal的原语
  2. Signal性能确实好,但他不太符合React的理念

Signal:更多前端框架的选择

React的理念可以用一句话概括:UI反映状态在某一刻的快照

既然是快照,那就不是局部的,而是个整体概念。在React中,状态更新会引起整个应用重新render,就是对React快照理念的最好诠释。

React现阶段的所有实现都是基于快照理念。所以,即使引入类似Signal的原语,可能也是类似Mobx这样的上层实现,而不是从底层重构。

我个人比较倾向于认为:React团队承认Signal的优点,但由于积重难返,而且现代设备的性能通常是过剩的,所以性能问题并不是首要问题。

如果这个观点是正确的,那么React可能会在开发者体验(Signal的另一个优点)方面努努力。比如去年提出的RFC: useEvent可能就是这方面的一次尝试。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Stella981 Stella981
3年前
AssemblyScript 入门指南[每日前端夜话0xEB]
每日前端夜话0xEB每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2459 字预计阅读时间:10分钟作者:DannyGuo翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/b880277c594152a503
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
Node.js 12中的ES模块[每日前端夜话0x9E]
每日前端夜话0x9E每日前端夜话,陪你聊前端。每天晚上18:00准时推送。正文共:2552字预计阅读时间:10 分钟作者:BrianDeSousa翻译:疯狂的技术宅来源:logrocket!(https://oscimg.oschina.net/oscnet/2ccaf94cecd3
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
MBR笔记
<bochs:100000000000e\WGUI\Simclientsize(0,0)!stretchedsize(640,480)!<bochs:2b0x7c00<bochs:3c00000003740i\BIOS\$Revision:1.166$$Date:2006/08/1117
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(