Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
这是react官网对react hook 的一句话简介。简而言之,在我看来就是让函数式组件(无状态组件)可以拥有自己的状态(state)以及改变状态的方法(setState)
React Hook 为何而存在
- 在组件之间复用状态逻辑很难
虽然我们可以用render props 和高阶组件来解决这一问题,但是!但是! 这不仅会容易形成“嵌套地狱”,还会面临一个很尴尬的问题(相对于我这种新手菜鸟来说) 那就是复杂逻辑难写!简单逻辑我宁可重写一遍都觉得比用高阶组件要快要方便(毕竟可以复制粘贴嘛)。
- 复杂组件难以理解
各种生命周期函数内充斥着各种状态逻辑处理和副作用,且难以复用、零散,比如一个调用列表数据的接口方法getList分别要写到componentDidMount 和componentDidUpdate中等等。
- 难以理解的class
this的指向问题(经常在某一处忘记bind(this)然后bug找半天)、组件预编译技术(组件折叠)会在class中遇到优化失效的case(还并不了解,先写上~)、class不能很好的压缩、class在热重载时会出现不稳定的情况。
所以!有了 react hook 就不用写class了,组件都可以用function来写,不用再写生命周期钩子,最关键的,不用再面对this了!
State Hook
首先我们先看下官方给出的简单例子
这是一个简单的累加计数器,我们先看class声明的组件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>你点击了{this.state.count}次</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
点我
</button>
</div>
);
}
}
这是一个非常非常简单的组件了,相信了解过react的人都能够看懂
那么我们再看一下使用hook的版本
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>你点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>
点我
</button>
</div>
);
}
是不是hook要简单了一点?可以看到,这是一个函数,与以往不同的是它拥有了自己的状态(count),就像class中的this.state.count;同时它还可以通过setCount()来更新自己的状态,就像class中的this.setState()。
为什么会这样呢? 仔细看第一行我们引入了useState这个hook,就是这个hook让我们的无状态组件成为了一个有状态的组件。
状态值的声明、读取、更新
那么我们分解一下看看到底这个hook都为我们做了什么
const [count, setCount] = useState(0);
首先useState的作用就是来声明状态变量,这个函数接收的参数是我们要为变量赋予的初始值,它返回了一个数组,索引[0]是当前的状态值,[1]是用来更新这个状态值的方法
所以这一句其实就是声明了一个count变量,就好比
this.state = {
count: 0
};
并且通过useState(0)传入参数0来为count赋了一个初始值0,同时提供了一个像this.setState一样可以更新它的方法setCount
然后我们引用读取这个变量的时候直接 {count}就好了,不用再this.state.count 这么长了!
<p>你点击了{count}次</p>
当我们想更新这个值的时候,直接调用setCount(新的值)就可以了。
怎么样,是不是很简单很容易理解?
但是,但是! 诶? 不对啊,这个函数是怎么记住之前的状态的? 通常来说我们在函数中声明一个变量,函数运行完也就跟着销毁了,重复调用的时候会重新声明,那这个Example函数是怎么做到记住之前声明的状态变量的?
State Hook 解决存储持久化的方案
我所知道的可以通过js存储持久化状态的方法有: class类、全局变量、DOM、闭包
通过学习react hook源码解析了解到是用闭包实现的,深入的我也看不懂!简单来说首先useState就是这样实现的
function useState(initialState){
let state = initialState;
function dispatch = (newState, action)=>{
state = newState;
}
return [state, dispatch]
}
像不像redux? 给定一个初始state,然后通过dispatch一个action,经由reducer改变state,再返回新的state,触发组件的重新渲染。
但是仅仅这样还满足不了要求,我们需要一个新的数据结构来保存上一次的state和这一次的state,以便可以在初始化流程调用useState和更新流程调用useState时可以取到对应的正确值。假定这个数据结构叫Hook:
type Hook = {
memoizedState: any, // 上一次完整更新之后的最终状态值
queue: UpdateQueue<any, any> | null, // 更新队列
};
考虑到第一次组件mounting和后续的updating逻辑差异,定义两个不同的useState函数来实现,分别叫做mountState和updateState
function useState(initialState){
if(isMounting){
return mountState(initialState);
}
if(isUpdateing){
return updateState(initialState);
}
}
// 第一次调用组件的 useState 时实际调用的方法
function mountState(initialState){
let hook = createNewHook();
hook.memoizedState = initalState;
return [hook.memoizedState, dispatchAction]
}
function dispatchAction(action){
// 使用数据结构存储所有的更新行为,以便在 rerender 流程中计算最新的状态值
storeUpdateActions(action);
// 执行 fiber 的渲染
scheduleWork();
}
// 第一次之后每一次执行 useState 时实际调用的方法
function updateState(initialState){
// 根据 dispatchAction 中存储的更新行为计算出新的状态值,并返回给组件
doReducerWork();
return [hook.memoizedState, dispatchAction];
}
function createNewHook(){
return {
memoizedState: null,
baseUpdate: null
}
}
以上就是基本的实现思路,内容参考自源码解析React Hook构建过程
想深入了解源码原理的请自行跳转~
声明多个state变量
useState是可以多次调用的
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
// ...
}
useState也支持接收对象或者数组作为参数。与this.setState不同的是,this.setState是合并状态后返回一个新的状态,而useState只是直接替换老状态后返回新状态。
Hook 规则
hook本质也是javaScript函数,但是他在使用的时候需要遵循两条规则,并且react要求强制执行这两条规则,不然就会出现奇怪的bug。
1.只能在最顶层使用hook
不要在循环,条件或者嵌套函数中调用hook,确保总是在react函数的最顶层调用,目的是为了确保hook在每一次渲染中都按照同样的顺序被调用,这让react能够在多次的useState和useEffect(另一种hook,下面会说)调用之间保持状态的正确,来保证多个state的相互独立和一一对应的关系。
2.只在react函数中调用hook
不要在普通js函数中调用hook
这里针对第一条举个栗子:
function Form() {
const [name1, setName1] = useState('zhangsan');
const [name2, setName2] = useState('lisi');
const [name3, setName3] = useState('wangwu');
// ...
}
这里连续使用了三次useState来声明了name1,name2,name3三个状态并且都赋予了初始值,那么在渲染时是这样的
//首次渲染(赋初始值)
useState('zhangsan') // 1. 使用 'zhangsan' 初始化变量名为 name1 的 state
useState('lisi') // 2. 使用 'lisi' 初始化变量名为 name2 的 state
useState('wangwu') // 3. 使用 'wangwu' 初始化变量名为 name3 的 state
//第二次渲染
useState('zhangsan') // 1. 读取name1的值
useState('lisi') // 2. 读取name2的值
useState('wangwu') // 3. 读取name3的值
如果我们这么写!
let tag = true;
function Form() {
const [name1, setName1] = useState('zhangsan');
if (tag) {
const [name2, setName2] = useState('lisi');
tag = false;
}
const [name3, setName3] = useState('wangwu');
}
这个过程就会变为
//首次渲染(赋初始值,和上面一样)
useState('zhangsan') // 1. 使用 'zhangsan' 初始化变量名为 name1 的 state
useState('lisi') // 2. 使用 'lisi' 初始化变量名为 name2 的 state
useState('wangwu') // 3. 使用 'wangwu' 初始化变量名为 name3 的 state
//第二次渲染
useState('zhangsan') // 1. 读取name1的值
//useState('lisi') // 2. 通过条件判断并没有走读取name2这一步
useState('wangwu') // 3. 读取name3的值时通过顺序判断读取到的却是name2,导致报错
所以hook规则其实就是确保hook的执行顺序,因为它是通过顺序来完成一一对应关系以及互相独立的!
Effect Hook
官方文档中一句话说明 Effect Hook 可以让你在函数组件中执行副作用操作,可能一句话并不能让人很好的理解它是干嘛用的,我们可以接着看上面那个最简单的计数器的栗子,在它的基础上加一个小功能
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
//设置浏览器标题内容
document.title = `你点击了${count} 次`;
});
return (
<div>
<p>你点击了{count}次</p>
<button onClick={() => setCount(count + 1)}>
点我
</button>
</div>
);
}
这段代码增加了一个将document的title设置为包含了点击次数的消息,如果通过class组件,应该怎么写呢?我们对比一下:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `你点击了 ${this.state.count} 次`;
}
componentDidUpdate() {
document.title = `你点击了 ${this.state.count} 次`;
}
render() {
return (
<div>
<p>你点击了{this.state.count}次</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
点我
</button>
</div>
);
}
}
通过对比,可以看出好像effect hook就相当于生命周期的componentDidMount,componentDidUpdate; 其实,我们写的有状态组件,通常都会产生副作用,比如ajax请求,浏览器事件的绑定和解绑,手动修改dom,记录日志等等;副作用还分为需要清除的和不需要清除的,所以effect hook 还相当于一个componentWillUnmount(比如我们有个需求是需要论询向服务器请求最新数据,那么我们就需要在组件卸载的时候来清理掉这个轮询操作)
清除副作用
componentDidMount(){
//轮询获取数据
this.getNewData()
}
componentWillUnmount(){
//组件卸载前清除轮询操作
this.unGetNewData()
}
我们完全可以在函数式组件中使用effect hook 来清除这个副作用,用法是在effect函数中return一个函数(清除操作)
useEffect(()=>{
getNewData()
return function cleanup() {
unGetNewData()
}
})
effect中返回一个函数,这是effect可选的清除机制。每个effect都可以返回一个清除函数,看你的需要。
react会在组件卸载的时候执行清除操作。effect在每次渲染的时候都会执行,并且是每次渲染之前都会去执行cleanup来清除上一个effect副作用。
需要注意的是!!!这种解绑模式同componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次,而effect hook里的函数,每次组件渲染都会执行一遍,包括副作用函数和它return的清理操作
effect的性能优化
大家一看到“每一次”都会执行,首先就会想到跟性能有关的问题,那么其实effect是可以跳过的,同样通过对比class组件来理解effect hook,class组件中使用componentDidUpdate来进行前后逻辑的比较
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
//判断状态值count改变了,再触发副作用操作
document.title = `你点击了${this.state.count}次`;
}
}
同样的在effect中,有第二个参数,起同样的作用
useEffect(() => {
document.title = `你点击了${count}次`;
}, [count]); // 仅在 count 更改时更新
第二个参数为一个数组,如果数组中有多个元素,即使只有一个元素发生了改变,react也会执行effect(即只有数组中所有元素都未变化,react才会跳过这次effect);若为空数组[],则表示该effect只会执行一次(包括副作用和return的清除副作用操作)
自定义Hook
也就是我们最初的目的:逻辑复用! 试想一下,加入我们现在要实现这样一个功能组件,点击button随机切换背景颜色,如图
用我们所熟悉的class组件是这样实现的
import React, { Component } from "react";
export default class ChangeColor extends Component {
constructor() {
super();
this.state = {
color: "red"
};
this.colors = ["red", "bule", "green", "yellow", "black"];
}
changeColor() {
const index = Math.floor(Math.random() * this.colors.length);
this.setState({ color: this.colors[index] });
}
render() {
return (
<div>
<div
style={{
width: 400,
height: 100,
border: "1px solid #ccc",
background: this.state.color
}}
></div>
<button onClick={() => this.changeColor()}>随机切换</button>
</div>
);
}
}
那么用hook是怎么实现的呢
import React, { useState } from "react";
export default function ChangeColor() {
const colors = ["red", "bule", "green", "yellow", "black"];
const [color, setColor] = useState("red");
function changeColor() {
const index = Math.floor(Math.random() * colors.length);
setColor(colors[index]);
}
return (
<div>
<div
style={{
width: 400,
height: 100,
border: "1px solid #ccc",
background: color
}}
></div>
<button onClick={changeColor}>随机切换</button>
</div>
);
}
这样看起来两种方法并没有什么大的差别,只是写法上的不同而已。那么问题来了,现在我们同时又有了一个需求,和它很像,但是不是点击切换了,而是定时器自动切换(滑动切换,反正各种切换方法~)。这下class组件直接就傻掉了,没办法复用啊! 里面有点击事件的逻辑和dom啊,我们能够复用的只是切换颜色这个逻辑而已!没办法那就复制粘贴再写一个新的吧,反正也很短。那那那要是很复杂的逻辑呢?怎么办?这时候我们看一哈自定义hook,它可以帮我们解决这个问题,抽离出我们需要复用的逻辑,实现优雅的复用。
首先我们考虑,能够复用的只是切换颜色的逻辑而已,所以我们抽离出的也一定是纯逻辑,也就是说这个自定义hook中不应包含dom
import { useState } from "react";
export default function useRandomColor() {
const colors = ["red", "bule", "green", "yellow", "black"];
const [color, setColor] = useState("red");
function changeColor() {
const index = Math.floor(Math.random() * colors.length);
setColor(colors[index]);
}
return [color, changeColor];
}
这里我们自定义了一个叫做useRandomColor的hook,返回值为[颜色,改变颜色的方法] 是不是很像useState?这就是一个简单的由我们自定义的一个hook。我们在父组件中调用的时候就像这样
import React from "react";
import useRandomColor from "./changeColor";
export default function ChangeColor() {
const [color, setColor] = useRandomColor();
return (
<div>
<div
style={{
width: 400,
height: 100,
border: "1px solid #ccc",
background: color
}}
></div>
<button onClick={setColor}>随机切换</button>
</div>
);
}
简直可以说是和useState的使用方法一模一样是不是!
需要注意的一点是,自定义hook是一个函数,其名称以 “use” 开头,它的内部可以调用其他的hook(无论是api提供的还是我们自定义的)
自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
还有哪些react提供的hook
这里我只讲述了useState 和 useEffect两个最重要也是最最常用的hook(对于目前阶段的我来说) 其实react还提供了很多hook:
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeMethods
- useMutationEffect
- useLayoutEffect
深入了解使用方法可跳转:react-1251415695.cos-website.ap-chengdu.myqcloud.com/docs/hooks-… 鉴于我个人的能力有限和不足,这些hook会在日后陆续学习和研究使用方法和场景等等。
Js中文网 – 前端进阶资源教程 www.javascriptC.com,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到
参考
作者:张不喝.
链接:https://juejin.im/post/5d9168aa6fb9a04e0855a8a5
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com