-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React Hooks 常见问题及解决方案 #6
Comments
很好的文章 可以补充一下 |
@panw3i 好的👌🏻 |
好全面啊,star |
为什么 count:5 的时候就就打印了 5次呢? |
useEffect 没有加 deps 参数,组件每次渲染都会绑定一次点击事件,你加到5的试试,就会绑定5次,然后就会输出 5次 |
@hwx98 如楼上所说,useEffect 没有 deps 参数,那么在第一次渲染及后面的每次更新时,都会执行。所有每次走 useEffect 的时候,按照这里的逻辑,都会绑定一个 click 事件,也就意味着 count 到 5 的时候,绑定了5 次,所以再次点击,就会打印五次了。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
本文已收录在
Github
: https://github.com/beichensky/Blog 中,欢迎 Star!常见问题
🐤 useState 和 setState 有什么明显的区别?
🐤 useState 和 useReducer 的初始值如果是个执行函数返回值,执行函数是否会多次执行?
🐤 还原 useReducer 的初始值,为什么还原不回去了?
🐤 useEffect 如何模拟 componentDidMount、componentUpdate、componentWillUnmount 生命周期?
🐤 如何在 useEffect 中正确的为 DOM 设置事件监听?
🐤 useEffect、useCallback、useMemo 中取到的 state、props 中为什么会是旧值?
🐤 useEffect 为什么会出现无限执行的问题?
🐤 useEffect 中出现竞态如何解决?
🐤 如何在函数组件中保存一些属性,跟随组件进行创建和销毁?
🐤 当 useCallback 会频繁触发时,应该如何进行优化?
🐤 useCallback 和 useMemo 的使用场景有何区别?
🐤 useCallback 和 useMemo 是否应该频繁使用?
🐤 如何在父组件中调用子组件的状态或者方法?
相信看完本文,你可以得到需要的答案。
一、函数组件渲染过程
先来看一下函数组件的运作方式:
Counter.js
每次点击
p
标签,count
都会 + 1,setCount
会触发函数组件的渲染。函数组件的重新渲染其实是当前函数的重新执行。在函数组件的每一次渲染中,内部的
state
、函数以及传入的props
都是独立的。比如:
二、useState / useReducer
useState
VSsetState
useState
只能作用在函数组件,setState
只能作用在类组件useState
可以在函数组件中声明多个,而类组件中的状态值都必须声明在this
的state
对象中一般的情况下,
state
改变时:useState
修改state
时,同一个useState
声明的值会被 覆盖处理,多个useState
声明的值会触发 多次渲染setState
修改state
时,多次setState
的对象会被 合并处理useState
修改state
时,设置相同的值,函数组件不会重新渲染,而继承Component
的类组件,即便setState
相同的值,也会触发渲染useState
VSuseReducer
初始值
useState
设置初始值时,如果初始值是个值,可以直接设置,如果是个函数返回值,建议使用回调函数的方式设置会发现即便
Counter
组件重新渲染时没有再给count
重新赋初始值,但是initCount
函数却会重复执行修改成回调函数的方式:
这个时候,
initCount
函数只会在Counter
组件初始化的时候执行,之后无论组件如何渲染,initCount
函数都不会再执行useReducer
设置初始值时,初始值只能是个值,不能使用回调函数的方式修改状态
useState
修改状态时,同一个useState
声明的状态会被覆盖处理useReducer
修改状态时,多次dispatch
会按顺序执行,依次对组件进行渲染还原
useReducer
的初始值,为什么还原不了比如下面这个例子:
点击修改按钮,将对象的
name
改为 小红,点击重置按钮,还原为原始对象。但是我们看看效果:可以看到
name
修改小红后,无论如何点击重置按钮,都无法还原。这是因为在
initPerson
的时候,我们改变了state
的属性,导致初始值initPerson
发生了变化,所以之后RESET
,即使返回了initPerson``,但是name
值依然是小红。所以我们在修改数据时,要注意,不要在原有数据上进行属性操作,重新创建新的对象进行操作即可。比如进行如下的修改:
看看修改后的效果,可以正常的进行重置了:
三、useEffect
useEffect
基本用法:每次点击
p
标签,Counter
组件都会重新渲染,都可以在控制台看到有log
打印。使用
useEffect
模拟componentDidMount
将
useEffect
的依赖设置为空数组,可以看到,只有在组件初次渲染时,控制台会打印输出。之后无论count
如何更新,都不会再打印。使用
useEffect
模拟componentDidUpdate
useRef
设置一个初始值,进行比较使用
useEffect
模拟componentWillUnmount
useEffect
中包裹函数中返回的函数,会在函数组件重新渲染时,清理上一帧数据时触发执行。因此这个函数可以做一些清理的工作。如果
useEffect
给定的依赖项是一个空数组,那么返回函数被执行时,代表着组件真正被卸载了。在
useEffect
正确的为DOM
设置事件监听在
useEffect
中设置事件监听,在return
的函数中对副作用进行清理,取消监听事件在
useEffect、useCallback、useMemo
中获取到的state、props
为什么是旧值正如我们刚才所说,函数组件的每一帧会有自己独立的
state、function、props
。而useEffect、useCallback、useMemo
具有缓存功能。因此,我们取的是当前对应函数作用域下的变量。如果没有正确的设置依赖项,那么
useEffect、useCallback、useMemo
就不会重新执行,其中使用的变量还是之前的值。useEffect
中为什么会出现无限执行的情况useEffect
设置依赖项,并且在useEffect
中更新state
,会导致界面无限重复渲染这种情况会导致界面无限重复渲染,因为没有设置依赖项,如果我们想在界面初次渲染时,给
count
设置新值,给依赖项设置空数组即可。修改后:只会在初始化时设置
count
值count
增加的时候,我们需要进行翻页(page
+ 1),看看如何写:由于此时我们依赖
count
,依赖项中要包含count
,而修改page
时又需要依赖page
,所以依赖项中也要包含page
此时也会导致界面无限重复渲染的情况,那么此时修改
page
时改成函数的方式,并从依赖性中移除page
即可修改后:既能实现效果,又避免了重复渲染
四、竞态
在
useEffect
中,可能会有进行网络请求的场景,我们会根据父组件传入的id
,去发起网络请求,id
变化时,会重新进行请求。展示结果:
上面的实例,多次刷新页面,可以看到最终结果有时展示的是
id 为 0 的请求结果
,有时是id 为 10 的结果
。正确的结果应该是 ‘id 为 10 的请求结果’。这个就是竞态带来的问题。
解决办法:
展示结果:
此时无论如何刷新页面,都只展示
id 为 10 的请求结果
。可以发现,此时无论如何刷新页面,也都只展示
id 为 10 的请求结果
。五、如何在函数组件中保存住非
state
、props
的值函数组件是没有
this
指向的,所以为了可以保存住组件实例的属性,可以使用useRef
来进行操作函数组件的
ref
具有可以 穿透闭包 的能力。通过将普通类型的值转换为一个带有current
属性的对象引用,来保证每次访问到的属性值是最新的。保证在函数组件的每一帧里访问到的
state
值是相同的useRef
的情况下,每一帧里的state
值是如何打印的先点击
p
标签 5 次,之后点击window
对象,可以看到打印结果:useRef
之后,每一帧里的ref
值是如何打印的和之前一样的操作,先点击
p
标签 5 次,之后点击window
界面,可以看到打印结果如何保存住函数组件实例的属性
函数组件是没有实例的,因此属性也无法挂载到
this
上。那如果我们想创建一个非state
、props
变量,能够跟随函数组件进行创建销毁,该如何操作呢?同样的,还是可以通过
useRef
,useRef
不仅可以作用在DOM
上,还可以将普通变量转化成带有current
属性的对象比如,我们希望设置一个
Model
的实例,在组件创建时,生成model
实例,组件销毁后,重新创建,会自动生成新的model
实例按照这种写法,可以实现在函数组件创建时,生成
Model
的实例,挂载到countRef
的current
属性上。重新渲染时,不会再给countRef
重新赋值。也就意味着在组件卸载之前使用的都是同一个
Model
实例,在卸载之后,当前model
实例也会随之销毁。所以此时我们可以借用
useState
的特性,改写一下。这样使用,可以在不修改
state
的情况下,使用model
实例中的一些属性,可以使flag
,可以是数据源,甚至可以作为Mobx
的store
进行使用。六、useCallback
如题,当依赖频繁变更时,如何避免
useCallback
频繁执行呢?这里,我们把
click
事件提取出来,使用useCallback
包裹,但其实并没有起到很好的效果。因为
Counter
组件重新渲染目前只依赖count
的变化,所以这里的useCallback
用与不用没什么区别。使用
useReducer
替代useState
可以使用
useReducer
进行替代。useReducer
返回的dispatch
函数是自带了memoize
的,不会在多次渲染时改变。因此在useCallback
中不需要将dispatch
作为依赖项。向
setState
中传递函数在
setCount
中使用函数作为参数时,接收到的值是最新的state
值,因此可以通过这个值执行操作。通过
useRef
进行闭包穿透七、useMemo
上面讲述了
useCallback
的一些问题和解决办法。下面看一看useMemo
。useMemo
和React.memo
不同:useMemo
是对组件内部的一些数据进行优化和缓存,惰性处理。React.memo
是对函数组件进行包裹,对组件内部的state
、props
进行浅比较,判断是否需要进行渲染。useMemo
和useCallback
的区别useMemo
的返回值是一个值,可以是属性,可以是函数(包括组件)useCallback
的返回值只能是函数因此,
useMemo
一定程度上可以替代useCallback
,等价条件:useCallback(fn, deps) => useMemo(() => fn, deps)
所以,上述关于
useCallback
一些优化点同样适用于useMemo
。八、useCallback 和 useMemo 是否应该频繁使用
这里先说一下我的浅见:不建议频繁使用
各位大佬先别开喷,容我说一说自己的观点
原因:
原因解释了一波,那 useCallback 和 useMemo 是不是就没有意义呢,当然不是,一点作用没有的话,React 何必提供出来呢。
用还是要用的,不过我们需要根据情况进行判断,什么时候去使用。
下面介绍一些 useCallback 和 useMemo 适用的场景
useCallback 的使用场景
场景一:需要对子组件进行性能优化
这个例子中,App 会向子组件 Foo 传递一个函数属性 onClick
使用 useCallback 进行优化前的代码
App.js
Foo.js
点击 App 中的 count increment 按钮,可以看到子组件 Foo 每次都会重新 render,但其实在 count 变化时,父组件重新 render,而子组件却不需要重新 render,当前情况自然没有什么问题。
但是如果 Foo 组件是一个非常复杂庞大的组件,那么此时就有必要对 Foo 组件进行优化,useCallback 就能派上用场了。
使用 useCallback 进行优化后的代码
App.js
中将传递给子组件的函数属性用 useCallback 包裹起来Foo.js
中使用 React.memo 对组件进行包裹(类组件的话继承 PureComponent 是同样的效果)此时再点击
count increment
按钮,可以看到,父组件更新,但是子组件不会重新render
场景二:需要作为其他
hooks
的依赖,这里仅使用useEffect
进行演示这个例子中,会根据状态
page
的变化去重新请求网络数据,当page
发生变化,我们希望能触发useEffect
调用网络请求,而useEffect
中调用了getDetail
函数,为了用到最新的page
,所以在useEffect
中需要依赖getDetail
函数,用以调用最新的getDetail
使用
useCallback
处理前的代码App.js
但是按照上面的写法,会导致
App
组件无限循环进行render
,此时就需要用到useCallback
进行处理使用
useCallback
处理后的代码App.js
此时可以看到,
App
组件可以正常的进行render
了。这里仅使用useEffect
进行演示,作为其他hooks
的依赖项时,也需要照此进行优化useCallback
使用场景总结:向子组件传递函数属性,并且子组件需要进行优化时,需要对函数属性进行
useCallback
包裹函数作为其他
hooks
的依赖项时,需要对函数进行useCallback
包裹useMemo 的使用场景
同
useCallback
场景一:需要对子组件进行性能优化时,用法也基本一致同
useCallback
场景二:需要作为其他hooks
的依赖时,用法也基本一致需要进行大量或者复杂运算时,为了提高性能,可以使用
useMemo
进行数据缓存看下面这个例子,
App
组件中两个状态:count
和Number
数组dataSource
,点击increment
按钮,count
会增加,点击fresh
按钮,会重新获取dataSource
,但是界面上并不需要展示dataSource
,而是需要展示dataSource
中所有元素的和,所以我们需要一个新的变量sum
来承载,展示到页面上。下面看代码
使用
useMemo
优化前的代码App.js
打开控制台,可以看到,此时无论点击
increment
或者Refresh
按钮,reduceDataSource
函数都会执行一次,但是dataSource
中有 100 个元素,所以我们肯定是希望在dataSource
变化时才重新计算sum
值,这时候useMemo
就排上用场了。使用
useMemo
优化后的代码App.js
此时可以看到,只有点击
Refresh
按钮 时,useMemo
中的函数才会重新执行。点击increment
按钮时,sum 还是之前的缓存结果,不会重新计算。useMemo
使用场景总结:向子组件传递 引用类型 属性,并且子组件需要进行优化时,需要对属性进行
useMemo
包裹引用类型值,作为其他
hooks
的依赖项时,需要使用useMemo
包裹,返回属性值需要进行大量或者复杂运算时,为了提高性能,可以使用
useMemo
进行数据缓存,节约计算成本九、如何在父组件中调用子组件的状态或者方法
在函数组件中,没有组件实例,所以无法像类组件中,通过绑定子组件的实例调用子组件中的状态或者方法。
那么在函数组件中,如何在父组件调用子组件的状态或者方法呢?答案就是使用
useImperativeHandle
语法
第一个参数是 ref 值,可以通过属性传入,也可以配合 forwardRef 使用
第二个参数是一个函数,返回一个对象,对象中的属性都会被挂载到第一个参数 ref 的 current 属性上
第三个参数是依赖的元素集合,同 useEffect、useCallback、useMemo,当依赖发生变化时,第二个参数会重新执行,重新挂载到第一个参数的 current 属性上
用法
注意:
createHandle
重复执行hook
中,对于同一个ref
,只能使用一次useImperativeHandle
,多次的话,后面执行的useImperativeHandle
的createHandle
返回值会替换掉前面执行的useImperativeHandle
的createHandle
返回值Foo.js
App.js
十、参考文档
写在后面
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
The text was updated successfully, but these errors were encountered: