大家好,我卡颂。
最近,Angular
、Qwik
的作者MIŠKO HEVERY发文表示Signal是前端框架的未来,并考虑在Angular
中实现它。
在此之前,Vue
、Solid.js
、Preact
、Svelte
都已实现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:
同样的例子用React
实现,触发点击后总用时98.5ms,其中React
的逻辑执行了89.38ms:
在这个例子中,React
性能比Svelte
差了一个数量级。之所以会有这样的差异,很大一部分原因在于Svlete在更新前就知道状态变化时需要更新哪个组件。
而这一切的源头就在于Signal
。
更好的DX
更好的开发者体验主要体现在两方面:
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>
- 减少开发者性能优化的心智负担
使用Signal
的框架通常能获得不错的运行时性能,所以不需要额外的性能优化API
。反观React
,开发者如果遇到性能问题,需要手动调用性能优化API
(比如React.memo
、useMemo
、PureComponent
)。
总结
有以上这么多优点,难怪很多框架都使用了Signal
。那么React
对Signal
是什么态度呢?
React
团队成员对此的观点是:
- 有可能引入类似
Signal
的原语 Signal
性能确实好,但他不太符合React
的理念
React
的理念可以用一句话概括:UI反映状态在某一刻的快照。
既然是快照,那就不是局部的,而是个整体概念。在React
中,状态更新会引起整个应用重新render
,就是对React
快照理念的最好诠释。
React
现阶段的所有实现都是基于快照理念。所以,即使引入类似Signal
的原语,可能也是类似Mobx
这样的上层实现,而不是从底层重构。
我个人比较倾向于认为:React
团队承认Signal
的优点,但由于积重难返,而且现代设备的性能通常是过剩的,所以性能问题并不是首要问题。
如果这个观点是正确的,那么React
可能会在开发者体验(Signal
的另一个优点)方面努努力。比如去年提出的RFC: useEvent可能就是这方面的一次尝试。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。