大家好,我卡颂。
针对 前端框架,长期存在着各种纷争。其中争执比拟大的是上面两项:
- 性能之争
- 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
有限靠近。
每个框架都在性能与灵活性上作出了取舍,以讨好他们的指标受众。