State Hook作用域和类组件的State有很大不同,State Hook 每一次都会直接替换掉旧的state,每一次render的时候都会通过闭包获取一个全新的state引用(上一次render时替换的新值),并且在本次render过程中保持不变(被改变之后要在下一次render中才能获得新的值)。而类组件其实多次render我们都是读写的同一份State。画一个图表示他们的区别:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
有一个小技巧,如果有一个Effect 只想运行一次,那么直接传一个空数组即可。
Hook 背后的实现原理
假设我们在一个组件中写了多个Hook,比如:
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// ...
}
什么是Hooks
从19年初 React V16.8 开始,正式支持Hooks特性。React Hooks 是一种能让你在函数组件中使用state和组件生命周期的一种方式,在Hooks出来之前,你必须把函数组件改成class组件才能用到这些特性。 而且,Hooks特性是完全兼容老版本代码的,所以不会对已有代码造成任何影响。并且官网也不推荐为了用Hooks而重构老代码。
Hooks分为几种:
useState
来在函数组件中使用state
,而不必声明一个类useContext
和useReducer
等为什么要弄出一个Hooks特性?最重要的原因是为了解决逻辑复用的问题,相比对 HoC 或者 render props,他能用更少更简洁的代码实现逻辑复用。在后续的例子中我们可以看到如何用Hooks实现逻辑代码复用。
官方给出的Hooks使用规范:
下面我们看看最重要的两个Hook:
useState
和useEffect
useState
State Hook 可以让我们在函数式组件中就能直接使用 state,而在这之前只能声明一个类并且初始化state才行。以下是官方给的简单使用的例子:
逻辑上等价于如下代码:
可以明显看到
useState
代码更加简洁,并且不会把数据和UI的声明周期混在一起。其中最神奇的一行代码就是:这行代码第一眼会非常难以理解。其基本工作原理是,useState 会返回一个数组,这个数组的结构是
[{变量值}, {设置变量的方法}]
,所以我们通过解构语法就能把count
和setCount
取出来,当然,这里你随便取什么名字都是没关系的。第一次运行的时候,会返回一个初始值,就是你传入的参数,之后每一次调用都会返回当前值。和
setState
不同的地方是,setCount
并不会执行merge操作,而是每一次都是直接替换(划重点了)。这就是
State Hook
的基本用法,其实很简单好理解。State Hook的作用域
State Hook作用域和类组件的State有很大不同,State Hook 每一次都会直接替换掉旧的state,每一次render的时候都会通过闭包获取一个全新的state引用(上一次render时替换的新值),并且在本次render过程中保持不变(被改变之后要在下一次render中才能获得新的值)。而类组件其实多次render我们都是读写的同一份State。画一个图表示他们的区别:
Effect Hook
相比于State Hook,Effect Hook会复杂一些,他的作用是:在更新DOM之后执行一些有副作用的方法,比如加载数据、修改DOM等。一般我们会在
componentDidMount
和componentDidUpdate
这两个生命周期中做,并且可能需要在componentWillUnmount
中做一些清理工作。而现在我们可以把这三个生命周期统一到一个Effect Hook 中。放在生命周期中做一些逻辑操作会有什么缺点呢?其实主要是两个方面:
借用官网的一个例子来说明,假设我们有一个组件需要在
title
上显示数据,传统的写法要这样:在上述代码中,其实我们就是需要在render执行完了改变title,然而我们把同一段逻辑重复了两次。如果有其他逻辑,可能我们都需要这种重复代码。如果用Effect Hook重写,不单解决了重复代码的问题,也解决了代码逻辑散落在各个生命周期中的问题:
Effect Hook的执行特点:
useEffect
会记住你传入的方法,然后每一次render执行完之后都会调用。useEffect
,所以其实每一次更新其实我们都是创建了一个新的方法。所以如果有清理操作,每一次更新之前都会进行清理,而不是只在 unmount 的时候进行清理。useEffect
其实是异步执行的,并不会阻塞DOM更新!而传统的通过componentDidMount
或者componentDidUpdate
进行副作用的操作其实会阻塞DOM的更新清理 Effect Hook
那么如果我们的操作还需要进行“清理”应该怎么办呢?比如订阅了一个事件,当结束的时候需要取消订阅。在传统的做法中,我们一般通过 “umount” 生命周期来做。在Hooks的实现中,我们只需要返回一个函数即可。React会在合适的时机调用这个函数进行清理。
官方示例:
具体何时进行清理就比较特殊了,因为我们前面说过,
useEffect
是每一次都会创建一个新的方法,每次render都会调用一遍,所以清理操作也是每次render都需要做的,而不是仅仅在unmount
才做,具体的时机是“每一次render
前都会清理上一次的effects”。也就是当前执行render结束后不会清理这一次的,而是清理上一次render调用的。通过返回函数进行清理的方式还有一个好处,就是我们一般清理操作都会用到原来的一些变量,放在同一个函数中,就不会出现作用域隔离而不得不绑定到
this
上来共享变量的问题。有些同学会有疑问,如果有些操作消耗比较大,不想每次
render
都做怎么办呢?React 官方提供了一种方式,可以指定只有某些变量发生变化了才调用,具体的用法如下:有一个小技巧,如果有一个Effect 只想运行一次,那么直接传一个空数组即可。
Hook 背后的实现原理
假设我们在一个组件中写了多个Hook,比如:
那么在多次渲染的过程中,React是怎么知道不同的
useState
和useEffect
对应哪一个呢? 其实React内部是通过一个数组进行记忆的,也就是React并没有记住谁是谁,仅仅按照顺序来分配。所以,多次render的过程中顺序一定不能乱,举个例子:向这样加了条件判断,那么就有可能导致顺序的不同而出现错误。所以官方文档强调“一定要把Hook的代码放在顶级,不能被任何其他代码包裹,也不能通过外部函数调用”。如果确实需要加一些条件,那么就放在Hook里面去做。
通过 Custom Hook 封装复用业务逻辑
上一篇主要讲了内置的 State Hook 和 Effect Hook,这一篇我们讲一下 Custom Hook。自定义Hook的主要目的是为了封装代码逻辑。
还是直接用官网的例子,假设我们有一个组件,会显示好友的在线状态:
其中最重要的一段逻辑就是通过API获取好友的状态,如果有其他组件也需要这样的逻辑,就可以把这段逻辑封装成一个自定义Hook。当然,如果你用 HoC 或者 render props 也完全可以实现逻辑封装,只是实现方式有一些差异。通过 Custom Hook 我们可以这样来实现:
这里我们定义了一个全新的 Custom Hook,注意,这不是一个“函数组件”,因为很明显我们接收的参数不是 props,并且返回的也不是VDOM,这就是一种全新的类型。这样在我们显示好友状态的组件中,直接调用这个 Custom Hook就行了:
Custom Hook 背后的原理
可能大家第一反应是 Custom Hook 是不是就是一个简单的函数? 从语法上看,确实就是一个简单的函数,但是从功能上看,细想一下,答案显然不是。因为我们在
useFriendStatus
中调用了useEffect
,前面讲过Hooks的规范,不能在自定义的函数中调用。为什么呢?React 实现Hook的基本原理,是通过一个数组记录,必须严格保证调用顺序始终不变。在组件中用Hook,每个组件其实都有隔离的state。那么Custom Hook 的状态是不是隔离的呢?下面要划重点了:
正因为React会给Custom Hook创建隔离的环境,所以他在运行时,显然和我们调用其他的自定义函数是有区别的,这也是为什么我们的Hook必须以 “use” 开头,不能随便取名字,不然React就不知道函数到底是不是Hook了,毕竟他们在JS语法上没区别。 而且React官方强调的一个概念就是:所有的Hooks其实执行原理都一样,Custom Hook 和 内置的
useState/useEffect
等没有本质区别。常见问题
Hooks能用在Class Component里面吗? 答案是不能。 Hooks 会完全代替 HoC 和 Render Props吗? 答案是不能。不能完全代替的原因是:Hooks仅仅适用于逻辑上的复用,如果想有渲染上的一些复用,还是后面两个比较合适。比如有一个 List 组件,可以通过
renderItem
来自定渲染,这种情况用 Hooks 就不好处理。参考资料