Dan在OverReact上发表的文章深入浅出,本文只针对个人之前不理解的点进行思考,采用了他的案例,参考了他的文章——useEffect的完整指南.
在我看来,Effect hook是React Hooks中最强大最核心的一个hook,是驱动整个程序的纽带我也将采用Dan的案例进行思考
- 根据文章问题,进行思考后,抛出了以下几个问题,将在下文对这几个问题做详细的思考
- Effect是如何进行渲染的?
- 如何用Effect模拟React的生命周期?
useEffect(fn,[])
和componentDidMount
一样吗? - 如何正确的使用Effect请求数据?
- Effect的依赖到底用什么,可以用函数嘛,什么时候用函数作为依赖?
- Effect Hook怎么会导致死循环?
- Effect Hook怎么会拿到旧state和props,如果我真的想用旧的state和props,我应该怎么去获取?
Effect到底是如何渲染的?
渲染中state的渲染
- 以下是最简单的点击次数加一的事件, 分析一下点击后数字的改变
1 | function Counter() { |
以下展示了count改变的来源,并非是通过事件监听或是事件绑定或是代理等对count本身做出的改变,而是重新创建了一个count, 新创建的count值是最后一次改变的state中的count.
点击次数 | 值 | count来源 |
---|---|---|
0 | 0 | useState默认值 |
1 | 1 | 上一个useState的返回值 |
2 | 2 | 上一个useState的返回值 |
代码体现则是如下
1 | // During first render |
- 因此发现,其实count只是一个常量,React在使用
setCount
后,带着一个新count再次调用组件!至于更深入的研究,还未研究过,准备参考Dan的另一篇文章将 React 作为 UI 运行时
渲染中事件处理函数的渲染
1 | function Counter() { |
- 我们按照以下步骤做
- 我先点击到按钮,使count到达1
- 点击show alert,再3秒内迅速点击按钮使count到3
- 观察alert的值, 值为1 or 3 ??
- 根据上文,以下展示了这个调用的情况
是否点击alert | 此时点击次数 | 值 | count来源 | handleAlertClick |
---|---|---|---|---|
否 | 0 | 0 | useState默认值 | handleAlertClick中的count取0 |
否 | 1 | 1 | 上一个useState的返回值 | handleAlertClick中的count取1 |
是 | 1 | 1 | 上一个useState的返回值 | handleAlertClick中的count取1 |
否 | 2 | 2 | 上一个useState的返回值 | handleAlertClick中的count取2 |
否 | 3 | 3 | 上一个useState的返回值 | handleAlertClick中的count取3 |
alert弹出 | … | … | … | 弹出表格对应的第三行的handleAlertClick |
- 我们发现,每次调用的count和handleAlertClick,都是重新创建的
count
和handleAlertClick
,每次重新渲染组件,上一次的栈内存都将被释放。由于闭包,第一次的count并未被释放,而handleAlertClick被存放在了任务队列,记录的是没有被释放的count,哪怕点到了3,与之前的也没有任何关系,每次渲染都是独立的,因此值是1。
每次渲染的state和props在渲染中是不会被改变的,因此每次渲染都是独立的,每次渲染的state和props都是不同的state和props。这种独立关系,再修改引用类型时,希望我们setObject(newObject)
,这样可以保证上一个state不被污染
问题1:Effect清理与浏览器渲染屏幕的执行顺序是什么样的呢?
1 | // First render, props are {id: 10} |
按照常理的逻辑,是这个顺序吗???????
次数 | 操作 |
---|---|
1 | 渲染props.id为10的UI |
2 | 执行Effect,订阅数据 |
3 | 清除id为10的Effect |
4 | 渲染props.id为20的UI |
5 | 执行Effect,订阅数据 |
yes???? | |
No |
结果应该是如下的
次数 | 操作 |
---|---|
1 | 渲染props.id为10的UI |
2 | 执行Effect,订阅数据 |
3 | 渲染props.id为20的UI |
4 | 清除id为10的Effect |
5 | 执行Effect,订阅数据 |
因为Effect的执行一定是放在浏览器渲染屏幕之后的!因为每次渲染都是独立的,上一个Effect只能记住id为10的状态,因此,effect的清除并不会读取最新的props
。它只能读取到定义它的那次渲染中的props
值。
问题2:每次渲染的Effect都是不同的Effect嘛?那么Effect中的state和外部state是什么关系?
每次渲染的Effect都是不同的Effect
Effect中的state和props,都是特定的那次渲染的state和props
React执行Effect的时机是什么?
1 | function Counter() { |
- 执行顺序如下
- 执行
const [count, setCount] = useState(0);
- React记住Effect
- 渲染dom
- 调用
document.title = count
- 执行
- 因此我们要记住,Effect是在每次更改作用于DOM并让浏览器绘制屏幕后去调用它
Effect中的异步
针对同步的情况,已经了解的差不多了,那么如果Effect中有延迟呢?
1 | function Counter() { |
假如我点击三次,将会打印什么,结论如下,原因很简单,因为每一个Effect中保存的是当前的count
1 | You clicked 0 times |
但是!上述工作机制与类并不相同,hooks写法中,每一个count是独立的,类写法中,将会输出一次you clicked 0 times
和3次you clicked 3 times
,原因是因为类写法中的count是同一个count
这原来就是困扰我好久好久的Effect中的闭包啊
问题来了,如果我就想打印3次you clicked 3 times
怎么办
ref登场
- 不同于class中的ref,hooks中的ref不仅可以保存dom元素,他可以作为任何值的容器
1
2
3
4
5
6
7
8
9
10
11
12function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// Set the mutable latest value
latestCount.current = count;
setTimeout(() => {
// Read the mutable latest value
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
}); - ok,这样就搞定啦,官网说了,
useRef
返回的对象,在整个生命周期内保持不变,因此不用担心每次创建的都是新的ref,这样我Effect函数中改变的ref内的容器值,都是同一个。 - 文章上半部分已经可以解决我们开篇提到的1、2、5、6三个问题
Effect到底是怎么更新的?
告诉React你要做什么样的比对
- 我想关于这个依赖项,React文档做了更详尽的阐述。这里只拎几个点出来
类似于setState的函数式更新
1 | function Counter() { |
- 由于每次渲染都是独立的,我们知道,这里count永远都是1
- 解决方案如下:
- 1、设置count作为依赖项
- 虽然解决了问题,但是非常不好,代码如下,原因是因为每次修改count,都将重新生成一个定时器,useEffect都会被重新执行,这显然不是我们想要的结果
- 2、函数式更新
- 比较理想的解决方案
- 1、设置count作为依赖项
1 | // 设置count为依赖项 |
1 | // 函数式更新 |
- 这种函数式更新,无需知道count的值,React已经知道,并将最新的count传递进去。成功将依赖项count移除
但是
我不仅想要知道最新的count,我还想要知道最新的props
或是其他的state
。。(函数式更新凉凉)
1 | function Counter() { |
炸了,不是我们想要的
useReducer
- 首先我们要知道,什么时候用这个useReducer?这里引用了React文档的内容。
在某些场景下,
useReducer
会比useState
更适用,例如state
逻辑较复杂且包含多个子值,或者下一个state
依赖于之前的state
等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数。
这里默认已经会Redux了。
- 关键点:
- 逻辑复杂
- 状态依赖
- 嵌套深的组件性能优化
其中第二点就是我们说的那一点,因此上面的案例可以改写为:
- 嵌套深的组件性能优化
1 | function Counter() { |
- 关键点: 用
dispatch
替代依赖(dispatch
会保证生命周期内保持不变) - 这种模式的好处:通过
reducer
让我们不必再关心state
和props
的状态,成功达到了解耦的目的。
但是问题来了,如果我每次都想获取最新的props
,还有戏嘛?
有的,把reducer扔组件里
每次dispatch
,都会调用reducer
,这时候reducer
获取的就是最新的props
了。
1 | function Counter({ step }) { |
难怪Dan说是Hooks的作弊模式
接下来,我们着重解决第三和第四个问题
Effect中的数据请求
直接上案例
这个模式曾经也是我在不是很懂Effect的情况下经常使用的模式,我曾经对eslint-plugin-react-hooks
这个插件提供的警告存有很大的疑问,现在终于明白了。
乍一看?没问题!
1 | function SearchResults() { |
- 但是上述模式存在一个弊端,如果我们忘记写入依赖,那么我们的effects就不会同步props和state带来的变更。这当然不是我们想要的。
我们把他放进去,前提是某些函数仅在effect中调用。
1 | function SearchResults() { |
还是没问题吗?是的,没问题了。
稍微修改一下,使请求url中需要我们的状态,对,就是这么请求。
1 | function SearchResults() { |
但我曾经有一次,在封装自定义Hook的时候,我将request提供给自定义hook,但是我却没法将我的state或者是props放进自定义hook,因为他们属于不同的js文件,这可怎么办?
说白了,就是逻辑复用咋搞。
useCallback
这个Hook很简单,缓存一个函数,只在函数本身需要改变的时候调用副作用
怎么知道这个函数是否需要改变,通过第二个参数
1 | function SearchResults() { |
问题解决!
Dan对class
模式和hooks
模式的这种网络请求没法放入依赖的情况分别作了比较,当request作为请求向下传递的情况。这里不细说了,class模式本身不是数据流的一部分,因此他必须将不必要的query
传下去才能再componentDidUpdate
中发生响应,而Hooks就完美的解决了这个问题
在我看来,useCallback就是一个工具人,我是老板,你没法直接跟我说话,就跟我秘书说。我秘书会传达给我的。
OK!1、2、3、4、5、6问题全都解决了!
总结&致谢
前前后后通读了Dan的文章好多遍,并看了好几遍Effect的文档。初识Effect,似乎很简单,随着项目的锻炼,发现越来越难以管理自己的状态。花心思重新学了一下Effect。让我更清晰的明白了设计Hooks的初衷和目的,找到正确使用的姿势。
从阅读到完成,花了大概有半个月,主要参考资料来自Dan的Overreacted上的一篇文章useEffect完整指南。