“忘记你已经学到的。” — Yoda,
当我们不再透过熟悉的class生命周期方法去窥视
useEffect这个Hook的时候,我们才得以融会贯通。
Question: 如何用useEffect模拟componentDidMount生命周期?
虽然可以使用useEffect(fn, []),但它们并不完全相等。和componentDidMount不一样,useEffect会捕获 props和state。所以即便在回调函数里,你拿到的还是初始的props和state。
如果你想得到“最新”的值,你可以使用ref。记住,effects的心智模型和componentDidMount以及其他生命周期是不同的,试图找到它们之间完全一致的表达反而更容易使你混淆。想要更有效,
你需要“think in effects”,它的心智模型更接近于实现状态同步,而不是响应生命周期事件
每一次渲染都有它自己的 Props and State
在我们讨论effects之前,我们需要先讨论一下渲染(rendering)。我们来看一个计数器组件Counter:
在这里,count 会“监听”状态的变化并自动更新吗? 在这里count仅仅是一个数字,他没有data binding/watcher/proxy.
实际上:当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。
它仅仅只是在渲染输出中插入了count这个数字。这个数字由React提供。当setCount的时候,React会带着一个不同的count值再次调用组件。然后,React会更新DOM以保持和渲染输出一致。
像这样:
关键: 任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。
每一次渲染都有它自己的事件处理函数
Gif: overreacted.io/46c55d5f1f7…
按照如下步骤:
- 点击增加counter到3
- 点击一下 “Show alert”
- 点击增加 counter到5并且在定时器回调触发前完成
Question: 此时alert会弹出什么???
解析:我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。
其实就和普通函数多次调用函数类似:只不过多次调用函数在count改变时React已经帮你完成了
结论:在任意一次渲染中,每一次渲染都有它自己的 Props and State 且始终保持不变的 。 如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。
它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。
每次渲染都有它自己的Effects
useEffect 和事件函数很相似, 并不是count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同 。React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。
所以,你可以想象effects是渲染结果的一部分
类似于React,组件,浏览器三者通信的模式:
-
React: 给我状态为
0时候的UI。 -
你的组件:
- 给你需要渲染的内容:
<p>You clicked 0 times</p>。 - 记得在渲染完了之后调用这个effect:
() => { document.title = 'You clicked 0 times' }。
- 给你需要渲染的内容:
-
React: 没问题。开始更新UI,喂浏览器,我要给DOM添加一些东西。
-
浏览器: 酷,我已经把它绘制到屏幕上了。
-
React: 好的, 我现在开始运行给我的effect
-
运行
() => { document.title = 'You clicked 0 times' }。
-
思考:如果我连续点击5次,此时会打印出什么???
答案: overreacted.io/a5727d333c2…
思考在class中会产生什么结果?原因?
答案: overreacted.io/264b329edc1…
利用闭包修复:codesandbox.io/s/w7vjo0705…
问题: 想在effect的回调函数里读取最新的值而不是捕获的值,同class一样打印出最新值,怎么解决?
答案: overreacted.io/264b329edc1…
结论: 采用useRef, latestCount.current是可变值,类似class中修改this一样获取最新值
告诉React去比对你的Effects(添加依赖项)
背景:当我们不想每次渲染结束后都去调用Effect
在React更新Dom对象时,会生成虚拟树和真实树进行对应比较,仅修改不同的节点,如:
但在effect中并不能这样对比,在没被调用之前不能猜测函数行为
故若添加依赖项给告知React, 只有依赖项name 改变之后才调用effects,而react也并不需要知道effects中的行为,React只会比较依赖项是否变化。
如何正确设置依赖
首先来看错误设置依赖的情况:依赖项为[], effect只执行一次,导致effects中的自增函数每隔1秒执行一次setCount(0 + 1), count始终为第一次拿到的count = 0,且不执行清除操作
这就叫依赖项对react撒谎
两种诚实告知依赖的方法
- 第一种策略是在依赖中包含所有effect中用到的组件内的值
如此可在不断调用的setInterval中获取到最新的count值;
弊端:定时器会在每一次count改变后清除和重新设定。容易死循环
2. 修改effect内部的代码以确保它包含的值只会在需要的时候发生变更。(减少依赖项)
setCount(c => c + 1) 可以绕过由组件获取当前的count, React其实已经知道当前的count。回到目的上来,我们需要告知React的仅仅是去递增状态 ,不管它现在具体是什么值。
这种情况下useEffect还是只执行一次,其中定时器函数每次通过setState获取新值
弊端: 受用模式十分有限,比如两个相互依赖的状态,或则说基于当前props来计算下一次的state值,则无法使用。
我们做到了移除依赖,并且没有撒谎。我们的effect不再读取渲染中的count值。
解耦来自Actions的更新
背景: 当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时
如果我们既不想要多次设定定时器函数且想移除/减少effects依赖,我们可以考虑 useReducer, 它可以将组件内行为和状态更新分开描述,如下:
在dispatch 中保存action状态,在reducer中执行状态更新,因为dispatch在react的生命周期中保持不变,所以仅执行一次effects且可在reducer中获取最新的state
结论: 当你写类似setSomething(something => ...)这种代码的时候,也许就是考虑使用reducer的契机
利用useCallback 来监听函数变化以实现复用该函数
我们知道,函数在每次渲染过程中都会改变,所以不能以函数作为useEffect的依赖项,这时useCallback可解决这个问题。useCallback监听函数中,只要依赖项保持不变,在react中函数就不改变
如此便实现了对函数监听,同时复用函数。