大家好,我卡颂。
针对前端框架,长期存在着各种纷争。其中争执比拟大的是上面两项:
- 性能之争
- API设计之争
比方,各大新兴框架都会掏出benchmark
证实本人优良的运行时性能,在这些benchmark
中React
通常是垫底的存在。
在API
设计上,Vue
爱好者认为:“更多的API束缚了开发者,不会因为团队成员程度的差别造成代码品质较大的差别”。
而React
爱好者则认为:“Vue
大量的API
限度了灵活性,JSX
yyds”。
上述探讨归根结底是框架性能与灵活性的取舍。
本文将介绍一款名为legendapp的状态治理库,他与其余状态治理库设计理念上有很大不同。
在React
中正当应用legendapp
,能够极大晋升利用的运行时性能。
但本文的目标并不仅仅是介绍一个状态治理库,而是与你一起感触随着性能进步,框架灵活性产生的变动。
欢送退出人类高质量前端框架群,带飞
React的性能优化
React
性能的确不算太好,这是不争的事实。起因在于React
自顶向下的更新机制。
每次状态更新,React
都会从根组件开始深度优先遍历整棵组件树。
既然遍历形式是固定的,那么如何优化性能呢?答案是寻找遍历时能够跳过的子树。
什么样的子树能够跳过遍历呢?显然是没有发生变化的子树。
在React
中,变动次要由上面3个因素造成:
state
props
context
他们都可能扭转UI
,或者触发useEffect
。
所以,一棵子树中如果存在上述3个因素的扭转,可能会发生变化,也就不能跳过遍历。
从变动的角度,咱们再来看看React
中的性能优化API,对于上面2个:
useMemo
useCallback
他们的实质是 —— 缩小props
的变动。
对于上面2个:
- PureComponent
- React.memo
他们的实质是 —— 让比拟props
的形式从全等比拟变为浅比拟。
状态治理库能做的优化
理解了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
这是如何办到的呢?
在legendapp
源码中,useObservable
办法代码如下:
function useObservable(initialValue) {
return React.useMemo(() => {
// ...一套相似Vue的细粒度更新机制
}, []);
}
通过包裹依赖项为空的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
有限靠近。
每个框架都在性能与灵活性上作出了取舍,以讨好他们的指标受众。
发表回复