重学 React 之基础认知
重学 React 之三大属性
重学 React 之生命周期
重学 React 之路由
Hook 简介
什么是 React Hook?
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
我们知道在 React 中,函数组件中是不能使用 state
的,不了解的可以翻我前几篇文章,都有说到哦。而 Hook
的出现,就可以让你在函数组件中使用 state
以及其他的 React 特性。
关于 Hook
出现的具体原因,可以参考官方说明:动机
再说 Hook
的本质,其实就是函数,它可以让你在函数组件里“钩入” React state 及生命周期等特性。Hook
不能在 class 组件中使用,如果你完全熟悉了 Hook
的使用,你可以仅用函数组件来编写 React 应用。
除了 React 内置的 Hook
,我们也可以使用一些第三方封装的 Hook
插件,甚至自己封装 Hook
。
需要注意 React 16.8.0
是第一个支持 Hook
的版本,本章 React 用例版本是 v17.0.2
。如果你想测试效果,React 版本需要大于等于 v16.8.0
。
useState
useState
是 React 内置的一个 Hook
,它允许你在 React 函数组件中添加 state 的 Hook
,我们还是以传统的计数器的案例来介绍它,在这之前,我们先看一个对比的示例,就是用 class 方式来写一个计数器组件:
class Counter extends React.Component {
state = {
count: 0,
};
add = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.add}>+1</button>
</div>
);
}
}
复制代码
这段简单的代码就完成了一个计数器逻辑,但是如果使用函数组件来写就不行了,因为函数组件不能使用 state
。不过现在可以借助 useState
钩子函数来帮我们在函数组件中完成同样的功能,使用示例:
// 需要引入 useState 钩子
import React, { useState } from "react";
function Counter() {
// 此处声明 count,是 state 变量
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return (
<div>
<p>count: {count}</p>
<button onClick={add}>+1</button>
</div>
);
}
复制代码
上面代码使用了 useState
来开发一个计数器功能,相比于 class 方式,我觉得更简洁优雅了。
useState
使用语法是:
const [xxx,setXxx] = useState(initValue)
复制代码
useState
方法传入 state
变量的初始值作为参数,同时返回包含两个元素的数组,数组第一项是内部当前状态值,第二项是更新状态值的函数。等号左边我们使用数组解构的方式,自定义两项值的名称,这两个名称都是任意的,没有特殊要求,不过一般都是取语义的名称。
以上例说明,setCount
是更新 count
状态的方法,它有两种使用方式,一种就是上面使用的那样直接传入新的状态值作为参数,另一种是传入一个函数作为参数,函数的参数是旧的状态值,返回值是新的状态值,如下:
// setCount(count + 1);
setCount((count) => count + 1);
复制代码
以上就是 useState
的使用,是不是很简单,如果定义多个 state
变量,那就多写一行而已:
const [count, setCount] = useState(0);
const [name, setName] = useState('Tom');
复制代码
useEffect
useEffect
也是 React 内置的 Hook
,根据官方说明,useEffect
可以让你在函数组件中执行副作用操作,所谓“副作用”操作,其实就是在 React 渲染或更新 Dom 时执行一些额外的操作,比如 ajax 请求,设置订阅或者启动定时器之类的。一般这些副作用操作都是放在生命周期钩子中执行,但是函数组件中是没有生命周期的,而 useEffect
的作用就是模拟 class 组件中的生命周期。
useEffect
钩子传入一个函数作为参数,函数中就可以执行副作用操作,还是在上面计数器的代码例子上,我们引入 useEffect
,执行打印 count
的副作用操作。代码如下:
// 引入 hook
import React, { useEffect, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
// 使用 useEffect,执行打印 count 操作
useEffect(() => {
console.log(count);
})
function add() {
setCount(count + 1);
}
return (
<div>
<p>count: {count}</p>
<button onClick={add}>+1</button>
</div>
);
}
复制代码
浏览器执行上述代码时,在页面初始化时会打印 0
,同时在进行计数操作时,会实时打印 count
的状态值。回想一下如果我们要在 class 组件中实现这样的效果要怎么做,其实就是分别在生命周期 componentDidMount
和 componentDidUpdate
中执行打印 count
的操作就行。
由此可见,这里 useEffect
已经模拟出了两个生命周期的作用了。但是也有一个问题,在 class 组件中,我们可以自由控制生命周期的钩子数量,假如我不希望在页面更新时执行副作用操作,那去掉 componentDidUpdate
这部分代码即可,需要就再加上。而 useEffect
是默认两个周期阶段都执行了,有什么办法不让更新阶段执行操作吗?
答案是肯定的,useEffect
可以控制执行周期,而且我觉得这部分设计的很巧妙。如果你不希望在页面更新的时候执行副作用操作,那么就给 useEffect
传入第二个参数,传一个空数组 []
即可,如下:
useEffect(() => {
console.log(count);
}, []); // 这里传入第二个参数,为一个空数组
复制代码
此时刷新浏览器,会先打印一次 0
,当你再进行计数操作时,不会再打印更新的值了,也就是不再执行更新阶段的作用。是不是很神奇,同时又觉得有点奇怪,为什么要传一个空数组呢?聪明的童鞋应该能想到一些了,如果数组里传入 count
变量会如何?当你把 count
放到数组中去时,页面更新时又会执行打印操作了。
useEffect(() => {
console.log(count);
}, [count]); // 传入 count
复制代码
准确的说其实是当 count
改变时,更新操作又执行了。这个数组有点像监听器,当你传一个空数组时,就不执行更新阶段副作用操作,而当你传入具体的变量时,只有当传入的变量更新时,才执行更新阶段的操作。默认不传数组时,只要有任何的状态更新,都执行更新阶段的副作用操作。
还有一个生命周期阶段也很重要,那就是 componentWillUnmount
,我们有时需要在组件卸载时清除掉一些东西,最常见例如销毁定时器。
useEffect
可以返回一个函数,这个函数的执行时机就是组件销毁前。为了演示,我改写上面定时器的例子,应用定时器将计数器改成每秒自动加 1,同时加入组件销毁按钮:
import React, { useEffect, useState } from "react";
import ReactDom from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 使用定时器更新 count,每秒 +1
setInterval(() => setCount((count) => count + 1), 1000);
}, []);
// 定义组件销毁方法
function onUnmount() {
ReactDom.unmountComponentAtNode(document.getElementById("root"));
}
return (
<div>
<p>count: {count}</p>
<button onClick={onUnmount}>销毁</button>
</div>
);
}
复制代码
例子中的代码已加入定时器,浏览器执行时能够每秒对 count
进行加一操作,如果此时点击销毁按钮,会发现浏览器有一个警告
这其实就是因为组件销毁了,但是定时器程序还在执行导致的,所以我们需要在组件卸载的时候也一并销毁定时器。我们可以在 useEffect
中返回一个函数,在这个函数中销毁定时器,改写如下:
useEffect(() => {
// 将定时器存到一个变量
const timer = setInterval(() => setCount((count) => count + 1), 1000);
// 返回一个函数,此函数会在组件卸载之前调用
return () => {
// 清理定时器
clearInterval(timer)
}
}, []);
复制代码
这样我们就模拟出了 componentWillUnmount
生命周期了,是不是很巧妙。总结就是你可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
三个函数的组合。
React 中不仅仅只有这两个 Hook
,更多的可以参考:Hook API 索引。这里篇幅有限就不一一介绍了,举一反三,基本上了解了上面两个 Hook
,就能看懂别的 Hook
。
自定义 Hook
Hook
本质上就是 js 函数,我们完全可以自定义特定功能的 Hook
。自定义 Hook
的一个必要条件就是名称必须 use
开头。我们可以使用已有的 Hook
,再度封装一个新的 Hook
,比如我们要定义一个 useMount
的 Hook
,表示只在组件初始化时执行的 Hook
,可以这样的封装:
import { useEffect } from "react";
const useMount = (fn) => {
useEffect(() => {
fn && fn()
}, [])
}
复制代码
哈哈,有点过于简单了,不过这样一封装更语义话了一些啊,说的是一个思路。由于我本人也没有大量实践,就不再多赘述了。
总结
关于 Hook
就讲这么多了吧,毕竟工作中没用过 React,也讲不出太多花。也有一些第三方的 hooks 库可以使用,这里推荐一个阿里出品的 ahooks,我看了一下功能很齐全啊,感兴趣的可以看看。
本篇是重学 React 系列最后一篇文章了,基本上讲完了 React 入门的所有东西,除了这篇竟然只有四篇,不得不说 React 学起来可比 Vue 快多了,不过想要用的好,细节上东西的肯定还有很多需要自己实践出来,不是几篇文章能讲清的。我没有再写关于 Redux
的了,因为我懒,我的掘金第一篇文章就是写的 Redux
,不想再写了,还有就是 Redux
本来就不是 React
内的东西,是一个非强制性的工具,真要用到时再看也无妨。
就这样吧,今年的最后一篇记录。忘记了今天竟然是圣诞节,哈哈。
如有错误,欢迎指出。