浅谈hooks

83 阅读6分钟

“忘记你已经学到的。” — 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:

image.png

在这里,count 会“监听”状态的变化并自动更新吗? 在这里count仅仅是一个数字,他没有data binding/watcher/proxy.
实际上:当我们更新状态的时候,React会重新渲染组件。每一次渲染都能拿到独立的count 状态,这个状态值是函数中的一个常量。

它仅仅只是在渲染输出中插入了count这个数字。这个数字由React提供。当setCount的时候,React会带着一个不同的count值再次调用组件。然后,React会更新DOM以保持和渲染输出一致。

像这样:

image.png

关键: 任意一次渲染中的count常量都不会随着时间改变。渲染输出会变是因为我们的组件被一次次调用,而每一次调用引起的渲染中,它包含的count值独立于其他渲染。

每一次渲染都有它自己的事件处理函数

image.png

image.png

Gif:  overreacted.io/46c55d5f1f7…

按照如下步骤:

  • 点击增加counter到3
  • 点击一下 “Show alert”
  • 点击增加 counter到5并且在定时器回调触发前完成

Question: 此时alert会弹出什么???

解析:我们的组件函数每次渲染都会被调用,但是每一次调用中count值都是常量,并且它被赋予了当前渲染中的状态值。

其实就和普通函数多次调用函数类似:只不过多次调用函数在count改变时React已经帮你完成了

image.png

结论:在任意一次渲染中,每一次渲染都有它自己的 Props and State 且始终保持不变的 如果props和state在不同的渲染中是相互独立的,那么使用到它们的任何值也是独立的(包括事件处理函数)。

          它们都“属于”一次特定的渲染。即便是事件处理中的异步函数调用“看到”的也是这次渲染中的count值。

每次渲染都有它自己的Effects

useEffect 和事件函数很相似, 并不是count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同 。React会记住你提供的effect函数,并且会在每次更改作用于DOM并让浏览器绘制屏幕后去调用它。

所以,你可以想象effects是渲染结果的一部分

image.png       

类似于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次,此时会打印出什么???

image.png

答案: overreacted.io/a5727d333c2…

思考在class中会产生什么结果?原因?

image.png

答案: overreacted.io/264b329edc1…

利用闭包修复:codesandbox.io/s/w7vjo0705…

问题: 在effect的回调函数里读取最新的值而不是捕获的值,同class一样打印出最新值,怎么解决?

image.png

答案: overreacted.io/264b329edc1…

结论: 采用useRef, latestCount.current是可变值,类似class中修改this一样获取最新值

告诉React去比对你的Effects(添加依赖项)

背景:当我们不想每次渲染结束后都去调用Effect

在React更新Dom对象时,会生成虚拟树和真实树进行对应比较,仅修改不同的节点,如:

image.png 但在effect中并不能这样对比,在没被调用之前不能猜测函数行为

image.png

故若添加依赖项给告知React, 只有依赖项name 改变之后才调用effects,而react也并不需要知道effects中的行为,React只会比较依赖项是否变化。

image.png

如何正确设置依赖

首先来看错误设置依赖的情况:依赖项为[], effect只执行一次,导致effects中的自增函数每隔1秒执行一次setCount(0 + 1), count始终为第一次拿到的count = 0,且不执行清除操作

这就叫依赖项对react撒谎

image.png

两种诚实告知依赖的方法

  1. 第一种策略是在依赖中包含所有effect中用到的组件内的值

image.png           

         如此可在不断调用的setInterval中获取到最新的count值;

         弊端:定时器会在每一次count改变后清除和重新设定。容易死循环

      2. 修改effect内部的代码以确保它包含的值只会在需要的时候发生变更。(减少依赖项)

image.png           

         setCount(c => c + 1) 可以绕过由组件获取当前的count, React其实已经知道当前的count。回到目的上来,我们需要告知React的仅仅是去递增状态 ,不管它现在具体是什么值。

        这种情况下useEffect还是只执行一次,其中定时器函数每次通过setState获取新值

        弊端: 受用模式十分有限,比如两个相互依赖的状态,或则说基于当前props来计算下一次的state值,则无法使用。

我们做到了移除依赖,并且没有撒谎。我们的effect不再读取渲染中的count值。

解耦来自Actions的更新

背景: 当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时

image.png

如果我们既不想要多次设定定时器函数且想移除/减少effects依赖,我们可以考虑 useReducer, 它可以将组件内行为和状态更新分开描述,如下:

image.png     

image.png 在dispatch 中保存action状态,在reducer中执行状态更新,因为dispatch在react的生命周期中保持不变,所以仅执行一次effects且可在reducer中获取最新的state

结论: 当你写类似setSomething(something => ...)这种代码的时候,也许就是考虑使用reducer的契机

利用useCallback 来监听函数变化以实现复用该函数

image.png

我们知道,函数在每次渲染过程中都会改变,所以不能以函数作为useEffect的依赖项,这时useCallback可解决这个问题。useCallback监听函数中,只要依赖项保持不变,在react中函数就不改变

如此便实现了对函数监听,同时复用函数。