React Hooks 是 React V16.8 加入的新特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
它主要为了解决 React 长期以来的以下几个问题
- 在组件之间复用状态逻辑很难,例如分页,上传文件等附带状态的逻辑需要每个页面都写一份
- 复杂组件变得难以理解,生命周期函数如
componentWillReciceProps
需要大量的条件对比代码 - 难以理解的 class
缓存函数(Memoization)
缓存函数是计算机提高计算性能一个手段,它将之前的计算结果存储起来,如果函数传入了相同的参数,就会取之前计算过的值,而不是从新执行一遍函数。
1 | // 斐波纳契数列,模拟耗时函数 |
在上面的例子中执行二次fib
函数,每一次几乎执行相同时间,而我们可以看出第二次的计算完全是没有必要的,因为一般来说,函数在相同输入的情况下,输出值也是相同的。这种情况下可以使用高阶函数增强原有函数,使其拥有缓存计算结果的能力。
1 | // 缓存单个计算结果 |
由于两次都是相同的输入,所以第二次计算时直接返回了上一次的结果,节约了大量的执行时间。
缓存函数也可以设计成缓存多个值
由于存储了额外的变量和数据,可以看出缓存函数是用空间换时间的一种优化方式。为了要先说这个呢,因为在 React 中,我们最重要的优化点在于减少组件虚拟 dom 的执行次数,而在减少渲染虚拟 dom 的核心点是减少组件state
和props
的变化(假设我们已经开启了shouldComponentUpdate
优化),缓存函数可以减少state
和props
的变化,同时别忘了函数组件本身就是函数。下面我们将会看到缓存函数在 React 中的实际运用。
Hooks
React 内置了很多基础的 hooks, 通过组合可以生成逻辑更复杂的 hooks。
useState
useState
赋予了函数组件拥有状态的能力
1 | import React, { useState } from "react"; |
文档:调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
也就是说,使用 hooks 组件可以理解成类组件的PureComponent
。当我们点击 按钮不变 按钮式,组件并不会渲染(未输出 “Render” log)。值得一提的是,文档上只是承诺了跳过了子组件的渲染,如果我们先点击+1
再点击不变
,当前组件还是会打印Render
,但是子组件确实是没有渲染的。这个问题我在 github 提了issue,希望能找到原因。文档的下一段就说明了这个原因(多么粗心的人!!!)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
不过官方的回复还是有一些额外的信息值得参考
The reasons for why this is can’t be explained without going fairly deep into the current implementation details- which probably wouldn’t be that useful and they’re likely to change in an upcoming release as we continue working on new APIs like concurrent mode and suspense.
简单的说就是:你没必要了解那么多细节,知道怎么用就行了。。 ̄ □  ̄||
使用 useState 还有一些技巧。
函数初始化
State 的初始值需要一些复杂的计算,由于使用 hooks 只能在函数组件中使用,每一次渲染会时整个函数内部的代码都会执行一次。
1 | function Comp() { |
因此 useState 可以传入一个函数,React 会保证这个函数只会在初始化执行,也就是相当于类组件的contructor
。
1 | function Comp() { |
类似于 setState,setCount 也可以传入一个回调函数
1 | useEffect(() => { |
forceUpdate
上面提到了使用 useState 的状态是纯的,而 useState 并没有提供forceUpdate
之类的方法,所以如果想要强制重新渲染组件。我们只能通过添加额外的状态来实现
1 | function Comp() { |
或者自己写一个 hooks 来实现
1 | function useForceUpdate() { |
useEffect
useEffect 替换了componentDidMount
,componentWillUpdate
,componentDidUpdate
,componentWillUnmount
等生命周期函数,它使对状态改变的监听变得简单。
如我们想在组件挂载后从远端加载数据
1 | import React, { useState, useEffect } from "react"; |
在组件挂载后,useEffect
的回调函数就会执行,而每次 render 回调函数都会执行。这当然不是我们想要的,所以可以传入第二参数 - 依赖数据列表。当传入的数据项没有变化时,回调函数就不会重新执行。如果不传入空数组,就相当于componentDidMount
1 | useEffect(() => { |
useEffect 的回调函数支持返回一个函数,便于在 effect 重新执行前进行上一次操作需要清理的操作。比如
1 | useEffect(() => { |
这时我们会发现一个问题,我可能只是想输出 count,并不想将我的定时器中断,于是我们把 count 从依赖中移除。然后点击按钮后我们会发现,无论当前的 count 是多少,我们的定时器永远只会输出 0 了。
想想我们的 count 来自于哪里?- 函数外部的组件。
外部组件是什么?- 函数。
函数内引用外部作用域变量在 javascript 中叫做什么?- 闭包!
没错,每次 render 时我们的函数组件都会执行一遍,然后内部会生成新的闭包。记得我们最开始提到的缓存函数
吗?如果把第二个参数数组看做函数的arguments
,那么我们可以认为当其没有变化时,effect 的回调函数并没有刷新(也没有清理),所以第一次渲染时生成的回调函数仍然在运行着,而内部的 count 仍然是上一次渲染的组件函数内的 count。所以 count 始终为 0.
那怎样才能不清理状态也能输出 count 呢,我们可以移花接木,通过组件变量来同步当前的状态值。
1 | const countRef = useRef(0); // hooks函数组件通过ref保存变量并能保证在组件声明周期内不变 |
在 ui 频繁变动的情况下,使用 Ref 同步 state 可以有效的减少依赖变化引起了状态变更。
UI 更新
useEffect 的调用并非总在 ui 更新完成之后,也可能在状态更变和浏览器绘制之前,React 会更新当前浏览器的渲染状态动态选择。所以我们无法在 useEffect 获取 UI 更新。
useMemo
1 | useMemo(() => { |
useMemo 就是 Hooks 版本的 Memoization。
useCallback
useCallback = (fn, deps)=> useMemo(() => fn, deps)
每一次 render 时,函数组件内部都会执行,内部的回调函数也会重新创建传给子组件。
1 | const ItemComp = React.memo((props: ItemProps) => { |
在这个长列表的 demo 中,我们可以在控制台中发现,每一次定时器的更新,都会触发子组件的重新渲染。