1.什么是hooks
- Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。实际就是函数组件解决没有
state,生命周期,逻辑不能复用的一种技术方案。 - "hooks" 直译是 “钩子”,它并不仅是
react,甚至不仅是前端界的专用术语,而是整个行业所熟知的用语。通常指:系统运行到某一时期时,会调用被注册到该时机的回调函数。 - 以
react为例,hooks是:一系列以“use”作为开头的方法,它们提供了让你可以完全避开class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
2.命名规范 和 指导思想
- 通常来说,
hooks的命名都是以use作为开头,这个规范也包括了那么我们自定义的hooks。为什么?因为这是一种约定 - 在
react官方文档里,对hooks的定义和使用提出了 “一个假设、两个只在” 核心指导思想。- 一个假设: 假设任何以 use 开头并紧跟着一个大写字母的函数就是一个
Hook。 - 第一个只在: 只在
React函数组件中调用Hook,而不在普通函数中调用Hook。(Eslint通过判断一个方法是不是大坨峰命名来判断它是否是React函数) - 第二个只在: 只在最顶层使用
Hook,而不要在循环,条件或嵌套函数中调用 Hook。
- 一个假设: 假设任何以 use 开头并紧跟着一个大写字母的函数就是一个
- 因为是约定,因而
useXxx的命名并非强制,你依然可以将你自定义的hook命名为byXxx或其他方式,但不推荐。
3.为什么需要hooks
-
1.更好的状态复用,解决mixins的各种弊端
-
2.更好的代码组织
-
一个页面中,N件事情的代码在一个组件内互相纠缠,代码量庞大的时候影响阅读 维护困难。
-
那么
Hooks写法在代码组织上究竟能带来怎样的提升呢?参考下图。 -
无论是
vue还是react, 通过Hooks写法都能做到,将“分散在各种声明周期里的代码块”,通过Hooks的方式将相关的内容聚合到一起 -
这样带来的好处是显而易见的:“高度聚合,可阅读性提升”。伴随而来的便是 “效率提升,bug变少”。
-
-
3.比
class组件更容易理解,尤其是this。- 在
react的class写法中,随处可见各种各样的.bind(this)。(甚至官方文档里也有专门的章节描述了“为什么绑定是必要的?”这一问题)。很显然,绑定虽然“必要”,但并不是“优点”,反而是“故障高发”地段。 - 但在
Hooks写法中,你就完全不必担心this的问题了。因为:本来无一物,何处惹尘埃。 Hooks写法直接告别了this,从“函数”来,到“函数”去。
- 在
-
4.友好的渐进式
- 渐进式的含义是:你可以一点点深入使用。
- 无论是
vue还是react,都只是提供了HooksAPI,并将它们的优劣利弊摆在了那里。并没有通过无法接受的break change来强迫你必须使用Hooks去改写之前的class组件。 - 你依然可以在项目里一边写
class组件,一边写Hooks组件,在项目的演进和开发过程中,这是一件没有痛感,却悄无声息改变着一切的事情。
4.useState
-
react的所有hooks都是从react中引入后再使用,或者用React.useXxx使用
-
useState传入一个参数作为默认值,返回一个数组,数组中第一个参数为状态,第二个参数为修改状态的方法。
简单体验
import React, { useState } from 'react';
const Index = () => {
const [count, setCount] = useState(0)
return (
<React.Fragment>
UseState: {count}  
<button onClick={() => setCount(count + 1)}>点我+1</button>
</React.Fragment>
);
}
export default Index;
详细一点
import React, { useState } from 'react';
const Index = () => {
// 创建一个或多个状态
const [count, setCount] = useState(0)
const [name, setName] = useState('小明')
const [age, setAge] = useState(18)
// 创建对象状态
const [list, addList] = useState([])
return (
<React.Fragment>
UseState: {count}   姓名:{name}   年龄:{age} <br />
<button onClick={() => setCount(count + 1)}> 点我+1 </button>
<button onClick={() => setName(name === '小明' ? '小张' : '小明')}> 点我切换姓名 </button>
<button onClick={() => setAge(age + 1)}> 点我修改年龄 </button><hr />
<button onClick={() => addList([{ name, age, id: Math.random() * 10000 }, ...list])}>
点我列表+1
</button>
{list.map(i => (<div key={i.id}>姓名:{i.name},年龄:{i.age}</div>))}
</React.Fragment>
);
}
export default Index;
5.useEffect
-
useEffect以函数形式直接调用,没有返回值,接收两个参数
-
第一个是回调函数(必传),可以在里面模拟生命周期
- 第一个参数必传,同时必须是一个函数
- 该函数中可返回一个函数,返回的函数实现了componentWillUnmount
-
第二个是一个数组(选传),可以指定需要
监听的对象-
如果不传,默认所有状态变更(包括父组件和子组件)都会触发useEffect的第一个回调函数
-
如果传一个空数组,那么只在第一次加载和组件卸载时才会触发useEffect的第一个回调函数
-
如果在useEffect的第一个回调函数中使用了某个状态,那么第二个参数就不能是空数组,否则会警告
-
如果数组中传递了参数,那么就只会在该参数发生改变时,才会触发useEffect的第一个回调函数
-
import React, { useState, useEffect } from 'react';
const Index = () => {
const [count, setCount] = useState(0)
const [show, setShow] = useState(true)
return (<div>
父组件: {count}
{show ?<Children count={count} setCount={setCount} /> : ''}
<button onClick={() => setShow(!show)}>点我</button> </div>
);
}
const Children = (props) => {
useEffect(() => {
console.log('加载子组件,开启计时器')
const timer = setInterval(() => {
props.setCount(props.count + 1)
}, 1000)
return () => {
console.log('下载子组件,关闭定时器')
clearInterval(timer)
}
}, [props])
return (<div>子组件:{props.count}</div> )
}
export default Index;
6.useContext
1.首先回忆一下Context
一种组件间通信方式, 常用于【祖组件】与【后代组件】间通信
使用方式
- 创建Context容器对象:
const XxxContext = React.createContext()
- 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<xxxContext.Provider value={value}>
子组件
</xxxContext.Provider>
- 后代组件读取数据:
//第一种方式:仅适用于类组件
static contextType = xxxContext // 声明接收context
this.context // 读取context中的value数据
//第二种方式: 函数组件与类组件都可以
<xxxContext.Consumer>
{
// value就是context中的value数据
value => ( 要显示的内容 )
}
</xxxContext.Consumer>
2.useContext的使用
- contexts.js
export const CountContext = React.createContext()
- Parent.js
import React, { useState } from 'react';
import { CountContext } from './contexts'
import Children from './Children'
const Parent = () => {
const [count, setCount] = useState(0)
return (
<CountContext.Provider value={{ count, setCount }}> <Children />
</CountContext.Provider>
);
}
export default Parent;
- Children.js
import React, { useContext } from 'react';
import { CountContext } from './contexts'
const Parent = () => {
let { count, setCount } = useContext(CountContext)
return (
<div onClick={() => setCount(count + 1)}>
{count}
</div>
);
}
export default Parent;
7.useReducer
1.useReducer 基本使用
-
传入两个参数
- 第一个是业务函数,该函数接收两个参数
- 第一个是状态state(初始状态/新的状态)
- 第二个是自定义配置参数action,用于逻辑处理
- 第二个是初始化值
- 第一个是业务函数,该函数接收两个参数
-
返回一个数组,该数组有两个值
- 第一个值是状态值
- 第二个值是dispatch函数,调用dispatch函数 传入上面业务函数的action相匹配的规则,即可对state进行修改
import React, { useReducer } from 'react';
const Index = () => {
const [count, dispatch] = useReducer((state, action) => {
switch (action) {
case 'add':
return state + 1
case 'sub':
return state - 1
default:
return state
}
}, 0)
return (
<div style={{ padding: '20px', border: '10x solid #ccc' }}>
{count}
<button onClick={() => dispatch('add')}>点我+1</button>
<button onClick={() => dispatch('sub')}>点我-1</button> </div>
);
}
export default Index;
2.useReducer + useContext + createContext 实现 redux
以下为项目中该如何使用,(分为3个模块写 个人理解)
- constant.js > 定义常量,提供给reducer和dispatch调用
- reducers.js > reducer的集合,存放各种reducer
- contexts.js > 使用react的createContext创建的context合集,存放各种context
1.创建onstant.js
export const INCREMENT = 'increment' // 加
export const DECREMENT = 'decrement' // 减
export const UPDATE_COLOR = 'update_color' // 改变颜色
2.创建reducers.js
// 引入constant定义的常量
import { INCREMENT, DECREMENT } from './constant'
// 加减法reducer
export const CountReducer = (state, action) => {
const { type, data } = action
switch (type) {
case INCREMENT:
return state += data
case DECREMENT:
return state -= data
default:
return state
}
}
// 改变颜色reducer
export const ColorReducer = (state, action) => {
const { type, data } = action
switch (type) {
case UPDATE_COLOR:
return data
default:
return '#000'
}
}
3.创建contexts.js
// 引入核心hook -- createContext
import { createContext } from 'react'
// 创建context
export const CountContext = createContext()
4.上层组件中注册
顶层组件/容器组件/父组件... 都是上层组件
5.子组件中调用dispatch
import React, { useReducer } from 'react';
// 引入reducer
import { CountReducer, ColorReducer } from '../redux/reducers'
// 引入context对象
import { CountContext } from '../redux/context'
const Container = (props) => {
// 创建并解构 state 及 dispatch --- dispatch可修改任意别名,个人喜欢用setXxx
const [count, setCount] = useReducer(CountReducer, 0)
const [color, setColor] = useReducer(ColorReducer, 10)
// 统一管理,传递给所有子孙组件
const reduxData = { count, color, setCount, setColor }
const { children, render } = props
return (
<CountContext.Provider value={reduxData}>
{/* 这里可直接使用state */}
<div style={{ fontSize: '50px', color }}>{count}</div>
{/* 作为容器组件时的内容判断 */}
{children ? children : render ? render : ''}
</CountContext.Provider>
);
}
export default Container;
ChangeCount组件
import React, { useContext } from 'react';
import { CountContext } from '../redux/context'
import { DECREMENT, INCREMENT } from '../redux/constant'
const ChangeCount = () => {
const { setCount } = useContext(CountContext)
return (
<div>
<button onClick={() => setCount({ type: DECREMENT, data: 10 })}>点我+10</button>
<button onClick={() => setCount({ type: INCREMENT, data: 10 })}>点我-10</button>
</div>
);
}
export default ChangeCount;
8.useRef
使用超级简单
import React, { useRef } from 'react';
const Index = () => {
const inputRef = useRef()
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => alert(inputRef.current.value)}>点我</button>
</div>
);
}
export default Index;
9.useMemo
1.调用方式
- 调用方式和useEffect一样
const a = useMemo(()=>{return 1},[])
- 回调函数中可返回新的数据,该数据可作为计算属性使用(类似vue的computed)
const newName = useMemo(()=>(name + '123'),[name])
<div> { newName } <div/>
2.使用场景
假如组件中有两个状态A和B,同时有两个依赖A和B的计算函数(类似vue计算属性computed),当A发生改变时,依赖B的计算函数会被重新调用。
使用useMemo可解决这一问题,useMemo一般用于性能优化,但一定不局限于性能优化。肯定有很多我还没发现的特点。
3.代码示例
import { useState, useMemo } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [total, setTotal] = useState(0);
// 没有使用 useMemo,即使是更新 total, countToString 也会重新计算
const countToString = (() => {
console.log("countToString 被调用");
return count.toString();
})();
// 使用了 useMemo, 只有 total 改变,才会重新计算
const totalToStringByMemo = useMemo(() => {
console.log("totalToStringByMemo 被调用");
return total + "";
}, [total]);
return (
<div className="App">
<h3>countToString: {countToString}</h3>
<h3>countToString: {totalToStringByMemo}</h3>
<button onClick={() => setCount((count) => count + 1)} >Count+1</button>  
<button onClick={() => setTotal((total) => total + 1)}>Total+1</button>
</div >
);
}
10.useCallback
1.调用方式
- 调用方式和useMemo一样
useCallback(()=>{},[])
- 返回一个回调函数,该函数可用于在其他地方调用,同时会有缓存
const handleCountAddByCallBack = useCallback(() => {
setCount((count) => count + 1);
}, []);
// 子组件中
this.props.handleCountAddByCallBack()
2.使用场景
子组件调用父组件传递来的函数,去改变父组件的状态时,子组件即使并没有使用父组件的状态,依旧会被重新渲染
使用useCallback包括的函数,传递给子组件使用,则能解决上述问题。一般用于新能优化。
3.代码示例
import React, { useCallback, useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
// 使用 useCallBack 缓存
const handleCountAddByCallBack = useCallback(() => {
setCount((count) => count + 1);
}, []);
// 不缓存,每次 count 更新时都会重新创建
const handleCountAdd = () => {
setCount((count) => count + 1);
};
return (
<div className="App">
<h3>CountAddByChild1: {count}</h3>
<Child1 addByCallBack={handleCountAddByCallBack} add={handleCountAdd} />
</div>
);
}
const Child1 = React.memo(function (props) {
const { add, addByCallBack } = props;
// 没有缓存,由于每次都创建,memo 认为两次地址都不同,属于不同的函数,所以会触发 useEffect
useEffect(() => {
console.log("Child1----addFcUpdate");
}, [add]);
// 有缓存,memo 判定两次地址都相同,所以不触发 useEffect
useEffect(() => {
console.log("Child1----addByCallBackFcUpdate");
}, [addByCallBack]);
return (
<div>
<button onClick={props.add}>+1</button>
<br />
<button onClick={props.addByCallBack}>+1(addByCallBack)</button>
</div>
);
});
11.自定义hook
自定义 Hooks 是一个函数,约定函数名称必须以 use 开头,React 就是通过函数名称是否以 use 开头来判断是不是 Hooks
Hooks 只能在函数组件中或其他自定义 Hooks 中使用,否则,会报错!
自定义 Hooks 用来提取组件的状态逻辑,根据不同功能可以有不同的参数和返回值(就像使用普通函数一样)
简单封装一个获取鼠标坐标的hooks
import { useState, useEffect } from 'react'
export const useMousePosition = () => {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const updateMouse = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
document.addEventListener('mousemove', updateMouse)
return () => {
document.removeEventListener('mousemove', updateMouse)
}
}, [])
return position
}