最近在用React写知识图谱的可视化,用了react-force-graph 这个库,这个库基于three.js进行渲染。

Web本来效率就不高,还要渲染3D,而且知识图谱的规模还可能很大,而我还用了React,所以优化很重要,。

不彻底懂React的写出来自然奇卡无比,这让我意识到,之前用React写的东西就像乱写的一样 `(*>﹏<*)′

状态的改变

众所周知,React中的一些钩子,比如useEffectuseMemo 等等,都要求传入一个数组,如果这个数组里面的东西变了那么就会更新这个钩子···

我在此之前一直不知道原理,不知道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的哲学:

  1. 不管什么东西,只要在组件间传递了,只要作为状态了,统统当作值类型,每次修改去构造一个新的去替换旧的。

  2. ref不要拿来存会变的数据

  3. 组件接收ref作参数的话,一定用forwardRef包一下组件。

  4. 组件尽量全用memo包起来。

  5. 会被React比较的对象(直接放在依赖关系里的和被传递出去的)全用useMemo包起来。

  6. 会被React比较的函数(直接放在依赖关系里的和被传递出去的)全用useCallback包起来。

4、5、6只要遵循了第1、2条就一定可以包。(但是第1条未必能完全遵守,因为可能从其他地方获得不能随便动的对象)

5、6可能没这个问题,因为不能随便动的对象一般不会影响到其他对象和函数,但是4的话要是硬要包memo的话可能要去写比较方法,后边可能也不好维护,带来额外的心智负担,干脆不包。

v2-8c93162207c613a926848bbdcbe34d00_720w.gif

我能想到的,最大的成功就是无愧于自己的心。