Open forthealllight opened 5 years ago
“在react hooks组件中声明的任何方法,或者任何对象都必须要包裹在useCallback或者useMemo中。”
不敢苟同
“在react hooks组件中声明的任何方法,或者任何对象都必须要包裹在useCallback或者useMemo中。”
不敢苟同
怎么说呢,使用useCallback和useMemo本身确实会带来一定的内存等消耗。你这篇文字我看了,没有给出实际的性能分析对比,我目前在项目中,对props的依赖大部分都用useCallback和useMemo包裹了,当然最好的还是选择性使用,比如按你的意思,非复杂计算,完全没有必要缓存等。
React Hooks工程实践总结
最近在项目中基本上全部使用了React Hooks,历史项目也用React Hooks重写了一遍,相比于Class组件,React Hooks的优点可以一句话来概括:就是简单,在React hooks中没有复杂的生命周期,没有类组件中复杂的this指向,没有类似于HOC,render props等复杂的组件复用模式等。本篇文章主要总结一下在React hooks工程实践中的经验。
一、React hooks中的渲染行为
1.React hooks组件是如何渲染的
理解React hooks的关键,就是要明白,hooks组件的每一次渲染都是独立,每一次的render都是一个独立的作用域,拥有自己的props和states、事件处理函数等。概括来讲:
每一次的render都是一个互不相关的函数,拥有完全独立的函数作用域,执行该渲染函数,返回相应的渲染结果
而类组件则不同,类组件中的props和states在整个生命周期中都是指向最新的那次渲染.
React hooks组件和类组件的在渲染行为中的区别,看起来很绕,我们可以用图来区别,
上图表示在React hooks组件的渲染过程,从图中可以看出,react hooks组件的每一次渲染都是一个独立的函数,会生成渲染区专属的props和state. 接着来看类组件中的渲染行为:
类组件中在渲染开始的时候会在类组件的构造函数中生成一个props和state,所有的渲染过程都是在一个渲染函数中进行的并且,每一次的渲染中都不会去生成新的state和props,而是将值赋值给最开始被初始化的this.props和this.state。
2.工程中注意React hooks的渲染行为
理解了React hooks的渲染行为,就指示了我们如何在工程中使用。首先因为React hooks组件在每一次渲染的过程中都会生成独立的所用域,因此,在组件内部的子函数和变量等在每次生命的时候都会重新生成,因此我们应该减少在React hooks组件内部声明函数。
写法一:
写法二:
App组件是一个hooks组件,我们知道了React hooks的渲染行为,那么写法1在每次render的时候都会去重新声明函数formatCounter,因此是不可取的。我们推荐写法二,如果函数与组件内的state和props无相关性,那么可以声明在组件的外部。如果函数与组件内的state和props强相关性,那么我们下节会介绍useCallback和useMemo的方法。
React hooks中的state和props,在每次渲染的过程中都是重新生成和独立的,那么我们如果需要一个对象,从开始到一次次的render1 , render2, ...中都是不变的应该怎么做呢。(这里的不变是不会重新生成,是引用的地址不变的意思,其值可以改变)
我们可以使用useRef,创建一个“常量”,该常量在组件的渲染期内始终指向同一个引用地址。
通过useRef,可以实现很多功能,比如在某次渲染的时候,拿到前一次渲染中的state。
上述的例子中,我们通过useRef()创建的ref对象,在整个usePrevious组件的周期内都是同一个对象,我们可以通过更新ref.current的值,来在App组件的渲染过程中,记录App组件渲染中前一次渲染的state.
这里其实还有一个不容易理解的地方,我们来看usePrevious:
这里的疑问是:为什么当value改变的时候,返回的ref.current指向的是value改变之前的值?
也就是说:
为什么useEffect在return ref.current之后才执行?
为了解释这个问题,我们来聊聊神奇的useEffect.
3.神奇的useEffect
hooks组件的每一次渲染都可以看成一个个独立的函数 render1,render2 ... rendern,那么这些render函数之间是怎么关联的呢,还有上小节的问题,为什么在usePrevious中,useEffect在return ref.current之后才执行。带着这两个疑问我们来看看在hooks组件中,最为神奇的useEffect。
用一句话概括就是:
每一渲染都会生成不同的render函数,并且每一次渲染通过useEffect会生成一个不同的Effects,Effects在每次渲染后声效。
每次渲染除了生成不同的作用域外,如果该hooks组件中使用了useEffect,通过useEffect还会生成一个独有的effects,该effects在渲染完成后生效。
举例来说:
上述的例子中,完成的逻辑是:
<p>You clicked 0 times</p>
<p>You clicked 1 times</p>
也就是说每次渲染render中,effect位于同步执行队列的最后面,在dom更新或者函数返回后在执行。
我们在来看usePrevious的例子:
因为useEffect的机制,在新的渲染过程中,先返回ref.current再执行deps依赖更新ref.current,因此usePrevios总是返回上一次的值。
现在我们知道,在一次渲染render中,有自己独立的state,props,还有独立的函数作用域,函数定义,effects等,实际上,在每次render渲染中,几乎所有都是独立的。我们最后来看两个例子:
(1)
(2)
这两个例子中,我们在3内点击5次Click me按钮,那么输出的结果都是一样的。
You clicked 0 times You clicked 1 times You clicked 2 times You clicked 3 times You clicked 4 times You clicked 5 times
总而言之,每一次渲染的render,几乎都是独立和独有的,除了useRef创建的对象外,其他对象和函数都没有相关性.
二、React hooks中的性能优化
前面我们讲了React hooks中的渲染行为,也初步 提到了说将与state和props无关的函数,声明在hooks组件外面可以提高组件的性能,减少每次在渲染中重新声明该无关函数. 除此之外,React hooks还提供了useMemo和useCallback来优化组件的性能.
(1).useCallback
有些时候我们必须要在hooks组件内定义函数或者方法,那么推荐用useCallback缓存这个方法,当useCallback的依赖项不发生变化的时候,该函数在每次渲染的过程中不需要重新声明
useCallback接受两个参数,第一个参数是要缓存的函数,第二个参数是一个数组,表示依赖项,当依赖项改变的时候会去重新声明一个新的函数,否则就返回这个被缓存的函数.
上述例子我们在第一章的例子基础上增加了onClick方法,并缓存了这个方法,只有props中的count改变的时候才需要重新生成这个方法。
(2).useMemo
useMemo与useCallback大同小异,区别就是useMemo缓存的不是函数,缓存的是对象(可以是jsx虚拟dom对象),同样的当依赖项不变的时候就返回这个被缓存的对象,否则就重新生成一个新的对象。
为了实现组件的性能优化,我们推荐:
在react hooks组件中声明的任何方法,或者任何对象都必须要包裹在useCallback或者useMemo中。
(3)useCallback,useMemo依赖项的比较方法
我们来看看useCallback,useMemo的依赖项,在更新前后是怎么比较的
其中is方法的定义为:
这个is方法就是es6的Object.is的兼容性写法,也就是说在useCallback和useMemo中的依赖项前后是通过Object.is来比较是否相同的,因此是浅比较。
三、React hooks中的状态管理和通信
react hooks中的局部状态管理相比于类组件而言更加简介,那么如果我们组件采用react hooks,那么如何解决组件间的通信问题。
(1) UseContext
最基础的想法可能就是通过useContext来解决组件间的通信问题。
比如:
在这个例子中通过createContext和useContext,可以在App的子组件CounterDisplay中使用context,从而实现一定意义上的组件通信。
此外,在useContext的基础上,为了其整体性,业界也有几个比较简单的封装:
https://github.com/jamiebuilds/unstated-next https://github.com/diegohaz/constate
但是其本质都没有解决一个问题:
如果context太多,那么如何维护这些context
也就是说在大量组件通信的场景下,用context进行组件通信代码的可读性很差。这个类组件的场景一致,context不是一个新的东西,虽然用了useContext减少了context的使用复杂度。
(2) Redux结合hooks来实现组件间的通信
hooks组件间的通信,同样可以使用redux来实现。也就是说:
在React hooks中,redux也有其存在的意义
在hooks中存在一个问题,因为不存在类似于react-redux中connect这个高阶组件,来传递mapState和mapDispatch, 解决的方式是通过redux-react-hook或者react-redux的7.1 hooks版本来使用。
在redux-react-hook中提供了StoreContext、useDispatch和useMappedState来操作redux中的store,比如定义mapState和mapDispatch的方式为:
这也是官方较为推荐的,react-redux 的hooks版本提供了useSelector()、useDispatch()、useStore()这3个主要方法,分别对应与mapState、mapDispatch以及直接拿到redux中store的实例.
简单介绍一下useSelector,在useSelector中除了能从store中拿到state以外,还支持深度比较的功能,如果相应的state前后没有改变,就不会去重新的计算.
举例来说,最基础的用法:
实现缓存功能的用法:
在上述的缓存用法中,只要todos.filter(todo => todo.isDone).length不改变,就不会去重新计算.