前端框架:性能与灵活性的取舍

开发 前端
本文将介绍一款名为legendapp的状态管理库,他与其他状态管理库设计理念上有很大不同。

大家好,我卡颂。

针对「前端框架」,长期存在着各种纷争。其中争论比较大的是下面两项:

  • 性能之争
  • API设计之争

比如,各大新兴框架都会掏出benchmark​证明自己优秀的运行时性能,在这些benchmark中React通常是垫底的存在。

在API​设计上,Vue爱好者认为:“更多的API约束了开发者,不会因为团队成员水平的差异造成代码质量较大的差异”。

而React​爱好者则认为:“Vue​大量的API​限制了灵活性,JSX yyds”。

上述讨论归根结底是框架「性能」与「灵活性」的取舍。

本文将介绍一款名为[1]的状态管理库,他与其他状态管理库设计理念上有很大不同。

图片

在React​中合理使用legendapp,可以极大提升应用的运行时性能。

但本文的目的并不仅仅是「介绍一个状态管理库」,而是与你一起感受「随着性能提高,框架灵活性发生的变化」。

React的性能优化

React​性能确实不算太好,这是不争的事实。原因在于React自顶向下的更新机制。

每次状态更新,React都会从根组件开始深度优先遍历整棵组件树。

既然遍历方式是固定的,那么如何优化性能呢?答案是「寻找遍历时可以跳过的子树」。

什么样的子树可以跳过遍历呢?显然是「没有发生变化的子树」。

在React中,「变化」主要由下面3个要素造成:

  • state
  • props
  • context

他们都可能改变UI​,或者触发useEffect。

所以,一棵子树中如果存在上述3个要素的改变,可能会发生变化,也就不能跳过遍历。

从「变化」的角度,我们再来看看React中的性能优化API,对于下面2个:

  • useMemo
  • useCallback

他们的本质是 —— 减少props的变化。

对于下面2个:

  • PureComponent
  • React.memo

他们的本质是 —— 直接告诉React这个组件没有变化,你不用再去检查上述3个要素了。

状态管理库能做的优化

了解了React的性能优化,我们再来看看状态管理库能为「性能优化」做些什么呢。

性能瓶颈主要发生在更新时,所以性能优化的方向主要有两个:

  • 减少不必要的更新
  • 减少每次更新时要遍历的子树

像Redux​语境下的useSelector走的就是第一条路。

对于后一条路,「减少更新时遍历的子树」通常意味着「减少上文介绍的3要素的变化」。

PS:黄玄开发的React Forget​,是一个「可以产生等效于useMemo、useCallback代码的编译器」,目的就是减少三要素中props的变化。

状态管理库在这方面能发挥的地方很有限,因为不管状态管理库如何巧妙的封装,也无法掩盖「他操作的其实是一个React状态」这一事实。

比如,虽然Mobx为React​带来了「细粒度更新」,但并不能带来与Vue​中「细粒度更新」相匹配的性能,因为Mobx最终触发的是自顶向下的更新。

legendapp的思路

本文要介绍的legendapp也走的是第二条路,但他的理念蛮特别的 —— 如果减少3要素的数量,那不就能减少3要素的变化么?

举个极端的例子,如果一个庞大的应用中一个状态都没有,那更新时整棵组件树都能被跳过。

下面是个Hook实现的计数器例子,useInterval每秒触发一次回调,回调中会触发更新:

function Counter() {
const [count, setCount] = useState(1)

useInterval(() => {
setCount(v => v + 1)
}, 1000)

return <div>Count: {count}</div>
}

根据3要素法则,Counter中包含名为count的state,且每秒发生变化,则更新时Counter不会被跳过(表现为Counter每秒都会render)。

下面是使用legendapp改造的例子:

function Counter() {
const count = useObservable(1)

useInterval(() => {
count.set(v => v + 1)
}, 1000)

return <div>Count: {count}</div>
}

在这个例子中,使用legendapp​提供的useObservable​方法定义状态count。

Counter​只会render​一次,后续即使count​变化,Counter​也不会render。

在线Demo[2]。

这是如何办到的呢?

在legendapp​源码中,useObservable方法代码如下:

function useObservable(initialValue) {
return React.useMemo(() => {

}, []);
}

通过包裹依赖项为空的React.useMemo,useObservable返回的实际是个「永远不会变的值」。

既然返回的不是state​,那Counter​组件中就不包含3要素(state​、props​、context​)中的任何一个,当然不会render了。

我们将这个思路推广开,如果整个应用中所有状态都通过useObservable​定义,那不就意味着整个应用都不存在state,那么更新时整棵组件树不都能跳过了么?

也就是说,legendapp在React​原有更新机制基础上,实现了一套基于「细粒度更新」的完整更新流程,最大限度摆脱React的影响。

legendapp的原理

接下来我们再聊聊legendapp状态更新的实现。

在传统的React例子中:

function Counter() {
const [count, setCount] = useState(1)

useInterval(() => {
setCount(v => v + 1)
}, 1000)

return <div>Count: {count}</div>
}

count变化,造成Counter组件render,render时count是新的值,所以返回的div中count是新的值。

而在legendapp例子中,Counter只会render一次,count如何更新呢?

function Counter() {
const count = useObservable(1)

useInterval(() => {
count.set(v => v + 1)
}, 1000)

return <div>Count: {count}</div>
}

实际上,useObservable返回的count并不是一个数字,而是一个叫做Text的组件:

const Text = React.memo(function ({ data }) {

});

在Text组件中,会监听count的变化。

当count变化后,会通过内部定义的useReducer触发一次React更新。

虽然React的更新是自顶向下遍历整棵组件树,但是整个应用中只有Text组件中存在状态且发生变化,所以除Text组件外其他子树都会被跳过。

性能与易用性的取舍

现在我们知道在legendapp中文本节点如何更新。

但JSX非常灵活,除了文本节点,还有比如:

  • 条件语句

如:

isShow ? <A/> : <B/>
  • 自定义属性

如:

<div className={isFocus ? 'text-blue' : ''}></div>

这些形式的变化该如何监听,并触发更新呢?

为此,legendapp提供了自定义组件Computed:

<Computed>
<span
className={showChild.get() ? 'text-blue' : ''}
>
{showChild.get() ? 'true' : 'false'}
</span>
</Computed>

对应的React语句:

<span className={showChild ? 'text-blue' : ''}>
{showChild ? 'true' : 'false'}
</span>

Computed​相当于一个容器,会监听children​中的状态变化,并触发React更新。

文本节点对应的Text组件可以类比为「被Computed包裹的文本内容」:

<Computed>{文本内容}</Computed>

除此之外,还有些更具语意化的标签(本质都是Computed的封装),比如用于条件语句的Show:

<Show if={showChild}>
<div>Child element</div>
</Show>

对应的React语句:

{showChild && (
<div>Child element</div>
)}

还有用于数组遍历的<For/>组件等。

到这一步你应该发现了,虽然我们利用legendapp​提高了运行时性能,但也引入了如Computed​、Show​等新的API。

你是愿意框架更灵活、有更多想象力,还是愿意牺牲灵活性,获得更高的性能?

这就是本文想表达的「性能与易用性的取舍」。

总结

用过Solid.js​的同学会发现,引入legendapp的React在API​上已经无限接近Solid.js了。

事实上,当Solid.js​选择结合React与「细粒度更新」,并在性能上作出优化的那一刻起,就决定了他的最终形态就是如此。

legendapp​ + React已经在运行时做到了很高的性能,如果想进一步优化,一个可行的方向是「编译时优化」。

如果朝着这个路子继续前进,在不舍弃「虚拟DOM」的情况下,就会与Vue3无限接近。

如果更极端点,舍弃了「虚拟DOM」,那么就会与Svelte无限接近。

每个框架都在性能与灵活性上作出了取舍,以讨好他们的目标受众。

参考资料

[1]legendapp:https://www.legendapp.com/open-source/state/hooks/。

[2]在线Demo:https://codesandbox.io/s/legend-state-primitives-140tmg。

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2010-02-01 18:23:54

Python

2009-06-08 09:57:45

编程语言编程语言性能高级编程语言

2013-02-26 10:44:26

2012-03-07 15:22:02

2009-01-22 19:03:32

服务器虚拟化VMware

2024-05-15 08:54:04

C++类型限定符代码

2010-02-23 17:04:32

Python编程语言

2009-01-01 22:08:15

企业网络提高

2015-12-31 09:44:56

公有云谷歌云评测

2016-11-08 13:50:57

2012-02-24 09:03:11

云计算虚拟化

2009-12-09 10:31:14

SOA质量SOA灵活性

2010-07-20 09:18:48

云计算灵活性

2009-08-25 16:11:12

Repeater控件

2012-09-25 13:45:36

Marvell

2010-01-06 12:16:46

Ubuntu软件包

2014-05-08 13:31:43

移动安全协作系统

2014-04-30 15:52:59

红帽

2010-07-22 10:08:39

JavaFXJava

2021-02-09 10:55:10

物联网人工智能边缘计算
点赞
收藏

51CTO技术栈公众号