,useState 可以说是我们日常最常用的 hook 之一了,在实际使用过程中,有一些简单的小技巧能帮助你提升性能 & 减少出 bug 的概率。,通常我们会使用以下的方式初始化 state。,对于简单的初始值,这样做完全没有任何性能问题,但如果初始值是根据复杂计算得出来的,我们这么写就会产生性能问题。,相信你已经发现这里的问题了,对于 useState 的初始值,我们只需要计算一次,但是根据 React Function Component 的渲染逻辑,在每一次 render 的时候,都会重新调用该函数,因此 initalState 在每次 render 时都会被重新计算,哪怕它只在第一次渲染的时候被用到,这无疑会造成严重过的性能问题,我们可以通过 useState 的惰性初始值来解决这个问题。,当我们想更新 state 的时候,我们通常会这样调用 setState。,看上去没有任何问题,我们来看看另外一个 case。,点击 div,我们可以看到计数器增加,那么 3 秒过后,计数器的值会是几呢?,答案是 1,这是一个非常反直觉的结果,原因在于第一次运行函数时,state 的值为 0,而 setTimeout 中的回调函数捕获了第一次运行 Demo 函数时 state 的值,也就是 0,所以 setState(state + 1)执行后 state 的值变成了 1,哪怕当前 state 值已经不是 0 了。,让我们通过函数式更新修复这个问题。,直接在 setState 中依赖 state 计算新的 state 在异步执行的函数中会由于 js 闭包捕获得到预期外的结果,此时可以使用 setState(prev => getNewState(prev)) 函数式更新来解决。,相信很多同学听过 immer.js 这个库,简单来说就是基于 proxy 拦截 getter 和 setter 的能力,让我们可以很方便的通过修改对象本身,创建新的对象,那么这有什么用呢?我们知道,React 通过 Object.is 函数比较 props,也就是说对于引用一致的对象,react是不会刷新视图的,这也是为什么我们不能直接修改调用 useState 得到的 state 来更新视图,而是要通过 setState 刷新视图,通常,为了方便,我们会使用 es6 的 spread 运算符构造新的对象(浅拷贝)。,我相信你已经发现问题了,对于嵌套层级多的对象,使用 spread 构造新的对象写起来心智负担很大,也不易于维护,这时候聪明的你肯定想到了,我直接 deepClone,修改后再 setState 不就完事了。,这样就完全没有心智负担的问题了,程序也运作良好,然而,这不是没有代价的,且不说 deepClone 本身对于嵌套层级复杂的对象就非常耗时,同时因为整个对象都是 deepClone 过来的,而不是浅拷贝,react 认为整个大对象都变了,这时候使用到对象里的引用值的组件也都会刷新,哪怕这两个引用前后值根本没有变化。,有没有两全其美的方法呢?,当然是用的,这里就要用到我们提到的 immer.js 了。,这里我们可以看到,即使用了 deepClone 没有心智负担的写法,同时 immer 只会改变写部分的引用 (也就是所谓的“Copy On Write”),其余没用变动的部分引用保持不变,react 会跳过这部分的更新,这样我们就同时获得了简易的写法和良好的性能。,事实上,我们还可以使用 useImmer 这个语法糖来进一步简化调用方式。,可以看到,使用 useImmer 之后,setState 几乎跟原生的 useState提供的函数式更新 api 一模一样,只不过,你可以在 setState 内直接修改对象生成新的 state ,同时 useImmer 还对普通的 setState 用法做了兼容,你也可以直接在 setState 内返回新的 state,完全没有心智负担。,前面讲了useState,那么还有一个开发过程中最常用的就是 useEffect 了,接下来来聊聊我的日常使用过程中 useEffect 的坑吧。,在很多情况下,我们可能会写出这样的代码,咋一眼看上去没有任何问题,dep1 这个 state 变动的时候我更新一下 dep2 的值,然而这其实是一种反模式,我们可能会习惯性的把 useEffect 当成一个 watch 来用,但每次我们 setState 过后,函数组件又会重新执行一遍,useEffect 也会重新跑一遍,这里你肯定会想,那不是成死循环了,但其实不然,useEffect 提供的第二个参数允许我们传递依赖项,在依赖项不变的情况下会跳过 effect 执行,这才让我们的代码可以正常运行。,所以到这里,聪明的你肯定已经发现问题了么?,要是我 dep 数组写的不对,那不是有可能出现无限循环?,在实际开发过程中,如此高的心智负担必然不利于代码的维护,因此我们来聊一聊什么是 effect,setState 又该在哪里调用,我们来看一个图:,,这里的 input / output 部分即 IO,也就是副作用,Input 产生一些值,而中间的纯函数对 Input 做一些转换,最终生成一堆数据,通过 output driver 执行副作用,也就是渲染,修改标题等操作,对应到 React 中,我们可以这样理解。,所有使用 hook 得到的状态即为 input 副作用产生的值。,function 组件函数本身是中间转换的纯函数。,React.render 函数作为 driver 负责读取转换好之后的值,并且执行渲染这个副作用 (其它的副作用在 useEffect 和 useLayoutEffect中执行 )。,基于以上的心智模型,我们可以得出这么几个结论:,(提醒一下,直接在函数顶层 block 中调用 setState,if 条件一下没写好,组件就挂了),我们可以使用这种方式计算新的 state。,(注意这里并没有使用 useMemo ),如果你写过以下的代码:,这样的代码非常容易造成循环依赖的问题,而且一旦出了问题,非常难排查很解决,整个 state 的更新很难预测,相关的 state 更新如果建议一次更新 (可以考虑使用 useReducer 并且在可能的情况下,尽量将状态更新放到事件而不是 useEffect 里)。,在多个组件共享状态以及要向深层组件传递状态时,我们通常会使用 useContext 这个 hook 和 createContext 搭配,也就是下面这样:,这也是 React 官方推荐的共享状态的方式,然而在需要共享状态的组件非常多的情况下,这有着严重的性能问题,在上述例子里,哪怕 A 组件只更新 state.a,并没有用到 state.b,B 组件更新 state.b 的时候 A 组件也会刷新,在组件非常多的情况下,就卡死了,用户体验非常不好。好在这个地方有很多种方法可以解决这个问题,这里我要推荐最简单的一种,也就是 react-tracked (https://react-tracked.js.org/) 这个库,它拥有和 useContext 差不多的 api,但基于 proxy 和组件内部的 useForceUpdate 做到了自动化的追踪,可以精准更新每个组件,不会出现修改大的 state,所有组件都刷新的情况。,一个很常见的误区是为了心理上的性能提升把函数通通使用 useCallback 包裹,在大多数情况下,javascript 创建一个函数的开销是很小的,哪怕每次渲染都重新创建,也不会有太大的性能损耗,真正的性能损耗在于,很多时候 callback 函数是组件 props 的一部分,因为每次渲染的时候都会重新创建 callback 导致函数引用不同,所以触发了组件的重渲染。然而一旦函数使用 useCallback 包裹,则要面对声明依赖项的问题,对于一个内部捕获了很多 state 的函数,写依赖项非常容易写错,因此引发 bug。所以,在大多数场景下,我们应该只在需要维持函数引用的情况下使用 useCallback,例如下面这个例子:,这里我们需要在组件卸载的时候移除 event listener callback,因此需要保持 event handler 的引用,所以这里需要使用 useCallback 来保持引用不变。,然而一旦我们使用 useCallback,我们又会面临声明依赖项的问题,这里我们可以使用 ahook 中的 useMemoizedFn (https://ahooks.js.org/zh-CN/hooks/use-memoized-fn) 的方式,既能保持引用,又不用声明依赖项。,是不是觉得很神奇,为什么不用声明依赖项也能保持函数引用不变,而内部的变量又可以捕获最新的 state,实际上,这个 hook 的实现异常的简单,我们只需要用到 useRef 和 useMemo。,对于需要优化渲染性能的场景,我们可以使用 memo 和 useMemo,通常用法如下:,考虑到 useMemo 需要声明依赖项,而 memo 不需要,会自动对所有 props 进行浅比较 (Object.is),因此大多数场景下,我们可以结合上面提到的 useImmer 以及 useMemoizedFn 保持对象和函数的引用不变,以此减少不必要的渲染,对于 Context 共享的数据,我们可以使用 react-tracked 进行精准渲染,这些库的好处是不需要声明依赖项,能减小维护成本和心智负担,对于剩下的没法 cover 的场景,我们再使用 useMemo 进行更细粒度的渲染控制。,相对于上文中提到的这些 hook,useReducer 是我们日常开发过程中很少会用到的一个 hook (因为大部分需要 flux 这样架构的软件一般都直接上状态管理库了)。,但是,我们可以思考一下,在很多场景下,我们真的需要额外的状态管理库么?,我们来看一下下面的这个例子:,这是一个非常简单的计数器例子,虽说运作良好,但是却反映了一个问题,当我们需要同时操作一系列相关的 state 时,在不借助外部状态管理库的情况下,随着程序的规模变大,函数组件内部可能会充斥着非常多的 setState 一系列 state 的操作,这样视图就和实际逻辑耦合起来了,代码变得难以维护,但其实我们不一定需要使用外部的状态管理库解决这个问题,很多时候 useReducer 就能帮我们搞定这个问题,我们尝试用 useReducer 重写一下这个逻辑。,我们先写一个 reducer 的纯函数:,我们再来定义一下初始状态以及 action 类型:,接下来只要用 useReducer 把他们组合起来就行了:,这样我们就把 reducer 这个状态的变更逻辑从组件中抽离出去了,代码看起来清晰易懂,维护起来也方便多了。,React Hook 心智负担真的很重,希望 react-forget (https://zhuanlan.zhihu.com/p/443807113) 能早日 production ready。
© 版权声明
文章版权归作者所有,未经允许请勿转载。