在函数组件里天天 use 来 use 去,我也想自己整点
什么是 React Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
其实就是写类组件太麻烦了,函数组件有的功能又没有,所以就用 Hook 来实现所需的效果
平常见到的那些 useXXX 就是 Hook
为什么需要 Hook
可以参看官方给出的 Hook 简介 - 动机
简单说就是,写函数组件就是要用 Hook
常用 Hooks
一般有以下 Hook 是常用的
- useState
- useEffect / useLayoutEffect
- useContext
- useReducer
- useMemo / useCallback
- useRef
注意事项
在开始介绍 Hook 之前,需要先介绍一下注意事项
只在最顶层使用 Hook
react 要求在每次执行组件渲染的时候,都要执行相同的 Hook 序列,否则会认为代码出错
1 | const [n, setN] = useState(0) |
上述代码会触发 react 报错,因为每次执行时,有可能出现不一样的 Hook 序列
同样的,循环、嵌套函数也可能出现上述问题,请根据具体报错进行处理
只在 React 函数中调用 Hook
Hook 在别的函数中,是不起作用的,因为它只为函数组件服务
所以,Hook 的使用场景必须是以下两个条件之一
- 在 react 函数组件中调用 Hook
- 在自定义 Hook 中调用其它 Hook
关于自定义 Hook,下文中会有描述,可以通过目录快进
useState
类组件可以通过 this.state = {}
来声明私有变量,但是函数组件不行,所以就需要 useState
,其作用实际上就是创建一个变量
通常使用如下形式创建
1 | const [n, setN] = useState(0) |
上述例子创建了一个变量,赋初始值为 0,并取得其 getter/setter
API
useState
接受一个传入参数,表示要创建的变量的初始值,之后返回一个具有两个值的数组
第一个值是该变量的 getter
,第二个则是对应的 setter
对于 getter
,像普通变量一样使用即可,比如此时直接对 n
取值,就可以取到值 0
对于 setter
,可以有两种调用方式
传入一个新值,此时会完全覆盖旧值
当且仅当新值与旧值地址不同时,会触发 render
传入一个函数,取函数的返回值作为新值
setter
会向这个函数传入一个参数,该参数的值是目标变量当前的最新值
当使用方式 1,直接传入一个新值时,需要注意以下两点
如果此时目标变量是一个对象且具有多个字段,则传入新对象时,不会同步旧有字段的值
例如,此时对象是
{n: 0, m:1}
,通过setter
设置新值为{n: 1}
则之后
getter
只能取到{n: 1}
,字段 m 会丢失不要修改旧有的值再传入
例如,此时对象是
{n: 0}
,先直接通过getter
执行obj.n ++
,再通过setter
设置新值为obj
则因为新值和旧值的地址相等,即使内部值变化了,react 也还是认为这个变量并没有发生变化,所以不会重新触发 render
useEffect
useEffect
最大的作用就是监听
useEffect
要求传入两个参数,第一个参数是回调函数,第二个参数是一个数组,表示当数组中列出的对象变化后,执行回调函数
1 | // 当 n 变化后,输出 'n changed' |
注意,此处所有的回调函数,都会在页面重绘后才执行
useEffect
还有一个功能类似的函数,名为 useLayoutEffect
useLayoutEffect
会在 DOM diff 之后,页面重绘之前执行
但由于上述特点会浪费时间,阻碍用户看到新页面,所以一般不使用 useLayoutEffect
,除非使用 useEffect
无法解决问题
useContext
提供组件上下文,让变量可以穿透组件,从父组件到达子组件
需要配合 React.createContext
使用
一个使用例如下
1 | import React, { useState, useContext } from 'react'; |
先声明 themes
作为父子组件通用的内容
然后通过 React.createContext
创建上下文,此处返回的变量可以任意命名
之后,在父组件中,通过 useState
创建一个响应式变量,用来存放当前提供给子组件的主题,并在模板中,使用一对 ThemeContext.Provider
标签,包裹住可以使用上下文的其它内容
此处允许子组件 ThemedButton
以及父组件中的一个 button
元素使用上下文
注意,不论你的上下文变量叫什么名字,此处的包裹标签都必须是 .Provider
形式
此时就可以在子组件中,通过 useContext
获取上下文,该 Hook 要求传入上下文变量作为参数,并返回此时父组件提供的具体内容,之后就可以在子组件的模板中使用了
useContext / createContext
组合通常用来提供局部的全局变量
之所以是全局变量,是因为其可以在父子组件之间通用
之所以又称为局部,是因为我们通常不希望有太多的变量污染全局空间,容易产生冲突,难以维护,所以最好不要放在主入口中
注意,上下文的修改不是响应式的,例如直接通过赋值语句修改上例中的 value
的话,并不会触发视图更新,所以上例采用了 useState
提供的响应式更新的方法
useReducer
是一种 useState
的替代方案,使用方法类似于 VUE 的 VUEX,都是一个存放数据的地方,加上预先声明的若干操作,之后通过 dispatch
提交操作
通常使用如下模样使用
1 | const [state, dispatch] = useReducer(reducer, initialArg, init); |
要求传入三个参数,按顺序分别是对数据的一组操作、数据初始值、初始化方法,其中第三个参数是可选的
该 Hook 的两个返回值,按顺序分别是数据的 getter
,以及向对应数据提交操作的 dispatch
函数
基本用法
一个使用例如下
1 | import React, { useReducer } from 'react'; |
先声明要引入的初始值,一般命名为某种 state
然后以函数形式声明一组操作,该函数接受两个参数,按顺序分别是数据对象和 dispatch
提交上来的对象
按照约定,一般将操作类型放在 type
字段,将额外的操作数放在 payload
字段
在这组操作中,通过判断 action.type
,来确定要执行的是哪种操作,然后返回一个新的对象,作为新的 state
需要注意的是,此处的返回值不会与旧的 state
自动合并,在使用中需要手动使用 spread 语法进行展开赋值
从上例的模板可以看出,对 state
的读操作,其写法与直接读源数据相同;对于 dispatch
函数,要求传入一个对象,该对象在操作函数中作为 action
出现
惰性初始化
也可以采用函数式声明,进行惰性初始化,这么做可以将用于计算 state 的逻辑提取到 reducer 外部,也为将来对重置 state 的 action 做处理提供了便利
一个惰性初始化的例子如下
1 | import React, { useReducer } from 'react'; |
显然,通过函数式初始化,可以对数据进行复杂的预处理,并且不会与 reducer
过耦合
但写法较为麻烦,实践中一般没什么必要,有需要的时候再重构吧
useMemo
常见的 useMemo
格式如下
1 | const memoizedValue = useMemo(() => fn(a, b), [a, b]); |
接受两个参数,返回一个缓存值
第一个参数是一个函数,该函数的传入参数为空,取该函数的返回值作为 useMemo
的返回值
第二个参数是一个监听数组,效果同 useState
可以通过该 Hook 回避多余的渲染
useCallback
一般我们执行一个函数的时候,都需要传入参数,但是用 useMemo
第一个参数必须为空,所以就会变成这样
1 | const memoizedValue = useMemo(() => (a,b) => fn(a, b), [a, b]); |
这是一个返回函数的函数,一看就很丑
所以有一个语法糖 useCallback
,自带可选参数
格式如下
1 | const memoizedValue = useCallback((a,b) => fn(a, b), [a, b]); |
就是这样,与上面提到的 useMemo
的例子完全等效
React.memo
也可以通过 React.memo
函数,来缓存整个组件
其格式如下
1 | function MyComponent(props) { |
要求传入两个参数,返回缓存的组件
第一个参数是函数组件
第二个参数可选,是一个判断函数,通过该函数进行对于新旧 props
的判断,返回 true / false
分别表示认为 props
变化或没有变化
当传递给 MyComponent
的 props
没有发生变化时,组件不会重新渲染
useRef
之前说过,react 的推荐思想之一就是对象不变性
但是这样的话,每次都要产生一个新对象,各种开销不得不考虑
于是就有了 useRef
这个 Hook,用来在组件中产生一个唯一的引用,使其在每次重新渲染的时候都保持同一个引用
一般格式如下
1 | const refContainer = useRef(initialValue); |
传入一个初始值,该初始值可以是对象
之后返回一个唯一的引用,通过该对象的 current
字段,取得我们传入的数据
一个用例如下
1 | function TextInputWithFocusButton() { |
通过创建一个初始值为空的引用,然后通过 ref
属性绑定到 DOM 元素上,之后该引用的 current
字段值就一直是该 DOM 元素
乍一看是不错,解决了重复产生多个对象的问题,但是又产生了另一个问题——当 ref 对象内容发生变化时,useRef
并不会通知你。变更 current
属性不会引发组件重新渲染
此时需要用户手动调用渲染函数,或采取如下的补救方法
补救方法
我们知道 useState
返回的 setter
可以刷新页面,那么我们就可以利用这个 Hook
1 | function App() { |
通过 useState
取得一个可以刷新页面的函数,之后每次在 count.current
更新时,传入保证不相同的随机数,就可以做到更新页面的效果了
子组件传递 ref
但是 ref 不能通过 props 传递,怎么办呢
可以通过 React.forwardRef
来包装一个子组件,并向下传递 ref
React.forwardRef
函数接受一个函数组件作为内部组件,并向该内部组件提供两个参数
第一个参数是正常的 props,第二个参数则是绑定在当前包装器组件上的 ref
一个用例如下
1 | function App() { |
不过类组件就没有这个问题,毕竟有 this 指针
useImperativeHandle
格式如下
1 | useImperativeHandle(ref, createHandle, [deps]) |
将 ref 设置为 createHandle 的值,在依赖项变更的时候重新计算
其实就是起到一个设置 ref 的效果
但是修改 ref 有什么用呢?答案就是没什么用(
自定义 Hook
这个就简单了,先给个例子吧
1 | // useUpdate.js |
通过 react 原生的 useState
和 useEffect
,来做到监听、回调与刷新
自定义 Hook 要求是必须命名为 useXxx
格式
除了本例的 useUpdate
,也可以自定义一些别的对某些数据的操作,只要直接向外暴露读写接口即可
如果想要加强功能的话,甚至其余的 CURD 也可以一起写好暴露出去,2333
过时的闭包
英文说法为 stale closure
我们可以发现 react 的 useState
等功能,其实都是隐藏了真实目标,而向外暴露接口的形式,这就是一种闭包
但是大量使用闭包,也有显而易见的缺点,就是不利于内存的管理
所以 VUE 的作者尤雨溪认为这是一种”过时的闭包”
当然大家见仁见智吧,react 毕竟这么自由
但是!我去用 VUE 3.0 了,888888888888888
感谢阅读