最近在用React写知识图谱的可视化,用了react-force-graph 这个库,这个库基于three.js进行渲染。
Web本来效率就不高,还要渲染3D,而且知识图谱的规模还可能很大,而我还用了React,所以优化很重要,。
不彻底懂React的写出来自然奇卡无比,这让我意识到,之前用React写的东西就像乱写的一样 `(*>﹏<*)′
状态的改变
众所周知,React中的一些钩子,比如useEffect、useMemo 等等,都要求传入一个数组,如果这个数组里面的东西变了那么就会更新这个钩子···
我在此之前一直不知道原理,不知道React如何判断数组里面的东西有没有变的。
之前以为是,传入的数据肯定是从某处得来的,比如组件参数,React通过一些手段,获取钩子数据传入的数据来源,当来源改变时就改变。
自从仔细看了memo的文档,它接受一个参数叫arePropsEqual,里面有这么一句话:
可选参数 arePropsEqual:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop。
React 将使用 Object.is 比较每个 prop 所以说,React默认使用浅比较去比较状态是否发生变化。
此时此刻,我理解了很多地方的用意,只能说React妙啊! ( •̀ ω •́ )✧
对钩子的使用
useState
之前的我会去用这个钩子,因为不用它我存不下状态,他返回的set函数有两种用法:
传入值,值会被设置到状态
传入函数 执行函数,函数的返回值会被设置到状态
第二种用法我从来没用过,没Get到第二种用法设计的用意。
不正确使用useState会产生问题:
Q: 要是状态是一个数组,不用第二种方法怎么保证同一次刷新插多个值不会相互覆盖?
A: 我用原数组push然后原数组作为值调用set函数。
Q: React的比较是浅比较,你这个数组改完,内容变了,但它还是原数组不会更新状态啊,这个数组传递给其他组件时不会产生刷新的问题吗?
A: 会造成问题,所以我定义另外一个flag变量,每次需要刷新flag自增一,解决刷新问题。
Q: ...
Q: 你定义了flag,但是flag是个值类型,如果你要在useEffect中多次自增flag岂不是每次自增出来都是一样的,怎么解决?
A: 我再用useRef定义一个flag的引用,每次刷新更新ref里面的值,这样就可以拿到最新的值啦。
Q: 这样不会很麻烦吗,有没有更简单的方法?
A: 每次flag更新就刷新useEffect,啊,这样useEffect不就没意义了嘛。React真垃圾
Q: ...
哲学:
实际上,第二种方法在修改数组和自增变量时很有用。
在React中修改一个状态的数组时,要将它复制一份修改,以产生状态更新。
useMemo
官方是这样说的:在组件的顶层调用 useMemo 来缓存每次重新渲染都需要计算的结果。
我一直对这句话深信不疑,遇到计算量大的东西就要考虑使用useMemo。
但是我好像之前还从来没遇到过需要用它缓存的东西,所以从来也没用过它。
哲学:
用它很多时候不是为了减小计算量,而是为了在多次渲染刷新中能复用同一个对象,避免一个对象改变造成链式刷新,影响性能。
useRef
他有两个用途:
拿到Dom对象(或者一些其他组件想要逆向传递出来的东西)
引用记录一个值,在useEffect之类的代码里面方便使用
之前我是完全滥用了,
第一个用途滥用,需要修改显示的东西时直接拿Dom对象改。
第二个用途滥用,该刷新的时候不刷新,代码难以维护。
哲学:
除非没有其他办法解决,否则永远不要用ref拿Dom对象(感觉用了一些React的库,根本没有拿Dom的需求)
永远不要用第二个用途。
useCallback
useCallback会记忆自己的第一个参数,第一个参数必须是一个函数,它会将这个函数原封不动的返回。
乍一看,它是缓存函数的,因为在JavaScript这个动态语言中,定义函数也是有开销的。
但仔细一想,使用的时候它要写成这个样子:
useCallback((XXX)=>{
XXXXXX;
},[])代码执行到这里,给它传入匿名函数,这个匿名函数不还是定义了吗?
所以不仅定义函数的开销没减小,还多了useCallback的开销。
得出结论:useCallback完全没用。
哲学:
useCallback是为了多次渲染刷新返回同一个函数对象(众所周知,同一个函数在动态语言每次定义都会返回不同的函数对象(因为要保存闭包,我的猜测))
useMemo可以取代useCallback,但是写起来不好写,官方文档说:这看起来很笨拙!记忆函数很常见,React 有一个专门用于此的内置 Hook。将你的函数包装到 useCallback 而不是 useMemo 中,以避免编写额外的嵌套函数。
复杂的React
最开始,我不会React,后来,我学了React,我自信地跟别人讲我会React,现在,我发现我并不会React···
React的官方文档很简短,但每次来看都有看不懂的地方,每次都有新的收获。
最开始读官方文档还觉得API也就大同小异,用到了再回来查,现在发现,真得看完,要不然写出来的代码真就一次性的,还都是BUG。
以前觉得React也就也就提供了一种编程范式,React帮忙存一下状态,需要的时候帮忙调一下函数,但是我看到了这些UI,不再感觉仅此而已了。
Suspense组件
如果这个组件的子节点抛出了Promise,那么它会先显示其他节点,等Promise完成再显示该显示的东西。
在此之前我刷到过一个视频,他说异步函数具有传染性,如何把异步函数改写成纯函数,说是在React中会很有用,我百思不得其解,为啥在React中好好的异步函数不执行,非要让它抛个错误出来呢?
现在知道了,原来React里面有个这东西,不用再去设置状态记录loading了,需要加载就直接抛Promise。( ̄︶ ̄)↗
startTransition
之前看,没看懂,现在看,还没看懂。
文档上写:startTransition 可以让你在不阻塞 UI 的情况下更新 state。
不理解,在我印象中,阻塞UI一般是执行什么计算密集的任务吧,计算密集任务解决不了吧?难道React的异步任务会阻塞UI?不可能吧?
惯性思维错误(#°Д°)
查资料···
确实计算密集任务没什么特别的解决方法,要不就在代码中手动加await多帧计算,要不就放Worker里面。
而startTransition也确实是解决计算密集任务的,它是解决渲染的组件太耗时的,虽然渲染组件部分的代码是自己写的,不可能中断,但是切换准备渲染下一个组件时会去执行React的代码,React可以此时中断渲染,放到下一帧。
所以startTransition是解决计算密集任务问题的,但它只解决了React本身的,自己写的计算密集任务还得自己去解决。
React会在后台渲染新的状态。
React要求每一个组件都要是一个纯函数,所以什么时间执行,怎么执行,在哪执行,理论上不会出现问题。我暂时没有自信用这个函数
同时还有一个useTransition,原理相同。
useDeferredValue
这个也是后台渲染优化的。
useDeferredValue以一个值作为参数,它的返回值可能是输入的值或者上一次输入的值,是什么取决于这个组件正在前台渲染还是后台渲染。
当一个组件里面存在这个Hook时,React在渲染这个组件时会分支,先渲染前台,前台使用上一次的值,然后在后台渲染新值,后台的渲染可被状态打断,渲染完自动拿后台的渲染内容去替换前台的内容。
尊嘟好高级好复杂( •̀ ω •́ )✧
官方文档里有这么一句话:如果你还不熟悉 switch 语句,使用 if/else 也是可以的。
文档我都看到这儿了,我可能不会用switch?
总结
React的哲学:
不管什么东西,只要在组件间传递了,只要作为状态了,统统当作值类型,每次修改去构造一个新的去替换旧的。
ref不要拿来存会变的数据
组件接收ref作参数的话,一定用forwardRef包一下组件。
组件尽量全用memo包起来。
会被React比较的对象(直接放在依赖关系里的和被传递出去的)全用useMemo包起来。
会被React比较的函数(直接放在依赖关系里的和被传递出去的)全用useCallback包起来。
4、5、6只要遵循了第1、2条就一定可以包。(但是第1条未必能完全遵守,因为可能从其他地方获得不能随便动的对象)
5、6可能没这个问题,因为不能随便动的对象一般不会影响到其他对象和函数,但是4的话要是硬要包memo的话可能要去写比较方法,后边可能也不好维护,带来额外的心智负担,干脆不包。
