首页>国内 > 正文

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

2022-10-10 18:58:51来源:魔术师卡颂

大家好,我卡颂。

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


(相关资料图)

性能之争API设计之争

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

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

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

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

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

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

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

React的性能优化

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

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

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

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

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

statepropscontext

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

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

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

useMemouseCallback

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

对于下面2个:

PureComponentReact.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 
Count: {count}
}

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

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

function Counter() {  const count = useObservable(1)  useInterval(() => {    count.set(v => v + 1)  }, 1000)  return 
Count: {count}
}

在这个例子中,使用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 
Count: {count}
}

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 
Count: {count}
}

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

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

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

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

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

性能与易用性的取舍

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

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

条件语句

如:

isShow ?  : 
自定义属性

如:

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

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

      {showChild.get() ? "true" : "false"}  

对应的React语句:

  {showChild ? "true" : "false"}

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

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

{文本内容}

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

  
Child element

对应的React语句:

{showChild && (  
Child element
)}

还有用于数组遍历的组件等。

到这一步你应该发现了,虽然我们利用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。

关键词: 发生变化 自顶向下 触发一次 我们再来

相关新闻

Copyright 2015-2020   三好网  版权所有