React Hooks 介绍、原理和API使用场景
概念
- React Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性;
- 以前在编写函数式组件,组件需要自己的 state 的时候,通常我们会转化成 class 组件来做。现在可以在函数组件中使用 Hook 来实现。
Hooks的原理
– 单向链表通过next把hooks串联起来;
– memoizedState存在fiber node上,组件之间不会相互影响;
– useState和useReducer中通过dispatchAction调度更新任务。
React Hooks,它带来了那些便利
- 代码逻辑聚合,逻辑复用
- HOC嵌套地狱
- 代替class
React 中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
好处:
- 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
- 类定义更为复杂
- 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
- 时刻需要关注this的指向问题;
- 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
- 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
遵循的规则
- 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
- 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
- 不能在useEffect中使用useState,React 会报错提示;
- 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存。
类被会替代吗?
Hooks
不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。
我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks
可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。
Hooks API
一:useState
在组件中,我们难免使用state来进行数据的实时响应,这是react框架的一大特性,只需更改state,组件就会重新渲染,试图也会响应更新。userState是一个方法,方法返回值为当前state以及更新state的函数。
不同于react在class可以直接定义state,或者是在constructor中使用this.state来直接定义state值,在hooks中使用state需要useState函数,如下:
import React, { useState, useEffect } from 'react';
function Hooks() {
const [count, setCount] = useState(0);
const [age] = useState(16);
useEffect(() => {
console.log(count);
});
return (
<div>
<p>小女子芳年{age}</p>
<p>计数器目前值为{count}</p>
<button type="button" onClick={() => { setCount(count + 1); }}>点击+1</button>
<button type="button" onClick={() => { setCount(count - 1); }}>点击-1</button>
</div>
);
}
export default Hooks;
在上面的例子中,我们使用了useState定义了两个state变量,count和age,其中定义count的时候还定义了setCount,就是用来改变count值的函数。在class类中,改变state是使用setState函数,而在hooks中是定义变量的同时定义一个改变变量的函数。
userState是一个方法,方法返回值为当前state以及更新state的函数,所以,在上面的例子中,我们用const [count, setCount] = useState(0);将count和setCount解构出来,而userState方法的参数就是state的初始值。当然count和与之对应的改变函数名称并不一定非得是setCount,名称可以随便起,只要是一块解构出来的即可。
在class组件中,我们可以用setState一次更改多个state值而只渲染一次,同样的,在hooks中,我们调用多个改变state的方法,也只是渲染一次。
二:userEffect
在class组件中,有生命周期的概念,最常用的,我们通常会在componentDidMount这个生命周期中做数据请求,偶尔,我们也会用一些其它的生命周期,像是componentDidUpdata,componentWillReceiveProps等。在hooks中,没有生命周期的概念,但是,有副作用函数useEffect。
默认情况下,useEffect会在第一次和每次更新之后都会执行,useEffect函数接受两个参数,第一个参数是一个函数,每次执行的就是函数中的内容,第二个函数是个数组,数组中可选择性写state中的数据,代表只有当数组中的state发生变化是才执行函数内的语句。如果是个空数组,代表只执行一次,类似于componentDidUpdata。所以,向后端请求可以写成下面这种方式:
// 页面进来只调用一次
useEffect(()=>{
axios.get('/getYearMonth').then(res=> {
console.log('getYearMonth',res);
setValues(oldValues => ({
...oldValues,
fileList:res.data.msg
}));
})
},[]);
useEffect函数会在浏览器完成画面渲染之后延迟调用。在一个hooks函数中,可以同时存在多个effect函数,所以,当有需求每次更新都执行useEffect中的代码时,可以用一个useEffect请求数据,用其他的useEffect做另外的事情。只需根据第二个参数即可区别不同作用。
//官方示例性能优化
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
三:userContext
使用userContext,不仅可以实现父子组件传值,还可以跨越多个层级进行传值,例如父组件可以给孙子组件甚至重孙子组件进行直接传值等,redux全局状态管理本质上也是对content的一种应用。
在hooks中使用content,需要使用createContext,useContext,废话不多说,直接示例展示用法
// context.js 新建一个context
import { createContext } from 'react';
const ShowContext = createContext('aaa');
export default ShowContext;
// HooksContext.jsx 父组件,提供context
import React, { useState } from 'react';
import Show from './Show.jsx';
import ShowContext from './context';
function HooksContext() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setCnt(count + 1); setAge(age + 1); }}
>
点击+1
</button>
<ShowContext.Provider value={{ count, age, clear }}>
<Show />
</ShowContext.Provider>
</div>
);
}
export default HooksContext;
// Show.jsx 子组件,使用context
import React, { useContext } from 'react';
import ShowContext from './context';
function Show() {
const { count, age, clear } = useContext(ShowContext);
return (
<div>
数量:{count}
年龄:{age}
<button
type="button"
onClick={() => { clear(); }}
>
复原
</button>
</div>
);
}
export default Show;
上面是一个完整的使用content实现父子组件传值的过程,如果Show组件下还有子组件,无论多少层,都可以用useContext直接取到HooksContext父组件提供的值,而context.js文件是新建一个context,新建必须要单独列出来,否则子组件无法使用useContext。
content提供了一种树状结构,被Context.Provider所包裹的所有组件,都可以直接取数据。redux就是利用了context的这种特性实现全局状态管理。在下面的几小节中,我们会讲hooks中context搭配useReducer来实现redux的功能。
四:userReducer
userReducer是useState的替代方案,它接收一个形如(state,action) => newState 的reducer,并返回当前的state以及其配套的dispatch方法。
总的来说呢,userReducer可以接受两个参数,第一个参数就是和redux中的reducer一样的纯函数,第二个参数是state的初始值,并返回当前state以及dispatch。
还是以官方示例的计数器为例:
import React, { useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
function HooksEffect() {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<div>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { dispatch({ type: 'add' }); }}
>
点击+1
</button>
<button
type="button"
onClick={() => { dispatch({ type: 'minus' }); }}
>
点击-1
</button>
</div>
);
}
export default HooksEffect;
相比起redux还需要connect高阶函数包裹一下才能将dispatch和state注入到props中,hooks中使用reducer更加简洁。
useReducer替代Redux案例
我们会用context和useReducer来实现redux的效果。依然是使用计数器这个功能,先贴代码,后面会详细讲解:
// count.js 定义context和reducer,导出context和包含reducer的context包裹组件。
import React, { createContext, useReducer } from 'react';
function countReducer(state, action) {
switch (action.type) {
case 'add':
return state + 1;
case 'minus':
return state - 1;
default:
return state;
}
}
const ADDCOUNT = 'add';
const MINUSCOUNT = 'minus';
export const CountContext = createContext();
export const CountWrap = (props) => {
const [count, dispatch] = useReducer(countReducer, 0);
return (
<CountContext.Provider
value={{ count, dispatch, ADDCOUNT, MINUSCOUNT }}
>
{props.children}
</CountContext.Provider>
);
};
// ReducerToRedux.jsx,连接组件,
import React from 'react';
import Button from './Button';
import Show from './Show';
import { CountWrap } from './count';
function ReducerToRedux() {
return (
<div>
<CountWrap>
<Show />
<Button />
</CountWrap>
</div>
);
}
export default ReducerToRedux;
// Show.jsx 显示当前数值的组件
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { count } = useContext(CountContext);
return (
<div>现在的计数器值为:{count}</div>
);
}
export default ReducerToRedux;
// Button.jsx 按钮组件,可以实现计数器的增和减
import React, { useContext } from 'react';
import { CountContext } from './count';
function ReducerToRedux() {
const { dispatch, ADDCOUNT, MINUSCOUNT } = useContext(CountContext);
return (
<div>
<button
type="button"
onClick={() => { dispatch({ type: MINUSCOUNT }); }}
>点我-1</button>
<button
type="button"
onClick={() => { dispatch({ type: ADDCOUNT }); }}
>点我+1</button>
</div>
);
}
export default ReducerToRedux;
通过reducer和context实现计数器的功能,我们共用了四个文件,当然count.js这个文件本应该拆分成三个文件,常量单独定义一个文件,reducer纯函数也应该单独定一个文件,不过代码不多,就暂时合一块了。
在count.js中,我们导出CountContext和CountWrap,其中,CoutWrap就是provider,也就是只要被CountWrap包裹过的组件,就可以使用userContent取到传递数据,而CountContext就是用createContext新建的一个content,使用useContext取传递数据的时候会用到。同时,在这个文件中,我们还将从useReducer解构出的count和dispatch,以及常量增减通过provider传递给包裹组件,使被包裹的组件可以通过useContext取到这些数据。函数countReducer就是和redux中的reducer一样的纯函数,子组件dispatch action,reducer则是接受当前state和action,通过判断action,返回新的state。
在Button组件中,我们通过countContext取到dispatch及常量,改变count这个数值,在Show组件中,只是展示count,在ReducerToRedux文件中,是做一个连接器,用CountWrap包裹Button和Show组件。
可能说的有些啰嗦,看上去有些复杂,其实稍一整理,原理很简单,自己写一遍整理清楚逻辑使用上就很简单了。
五:useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
useMemo是函数式组件官方提供的性能优化的一个方法,接受两个参数,第一个参数是要执行的函数,第二个参数是state中的值或者父组件传下来的值,代表只有当第二个参数的值发生变化时,才执行函数。其中,第二个参数是数组,可以同时优化多个state或者父组件传下来的参数,首次渲染组件是,如果页面用到要优化的值,函数会执行。
我们还是以计数器以及年龄为例:
import React, { useState, useMemo } from 'react';
function Show({ count, age, clear }) {
function ageChange(value) {
console.log(value);
return value + 2;
}
const myAge = useMemo(() => ageChange(age), [age]);
return (
<div>
数量:{count} 我的年龄:{myAge}
<button
type="button"
onClick={() => { clear(); }}
>复原</button>
</div>
);
}
function HooksUseMome() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
function clear() {
setCnt(0);
setAge(16);
}
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>点击年龄+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>点击计数器+1</button>
<Show count={count} age={age} clear={clear} />
</div>
);
}
export default HooksUseMome;
在上面的例子中,父组件小女子初始年龄为16岁,而到子组件经过ageChange函数,返回我的年龄永远比小女子年龄大两岁。
但是如果没有useMemo,当父组件的计数器count值发生变化时,子组件的ageChange函数也会执行,这不是我们想要的结果,我们只想当小女子的年龄发生变化时,再执行ageChange函数。所以,用useMemo可以实现我们想要的效果。如上面代码所示const myAge = useMemo(() => ageChange(age), [age]);,使用useMemo,第二个参数是age,这样,只有当age发生变化时,才执行其中的函数。
在类组件中,有shouldComponentDidUpdata生命周期,我们可以在其中做监测,当检测到state值没发生变化时,直接不渲染组件,而useMemo和这个生命周期还有些许不同。它是当检测的state发生变化时而执行某些函数,避免额外的开销,节省性能。
六: useRef
在项目开发中,我们比较少用到ref,一般我们不直接操作DOM,都是通过状态来控制DOM,不过在某些情况下,可能还是会用到ref,这一节我们通过对input输入框数据的双向绑定来认识useRef。
import React, { useState, useRef } from 'react';
function HooksUseRef() {
const [inputValue, setInputValue] = useState();
const inputRef = useRef(null);
function inputChangeHandle(e) {
setInputValue(e.target.value);
}
function inputRefChangeHandle() {
console.log(inputRef.current.value);
}
return (
<div>
<div>
<input
value={inputValue}
onChange={inputChangeHandle}
type="text"
/>
<span>使用state绑定inputValue值</span>
</div>
<div>
<input
ref={inputRef}
onChange={inputRefChangeHandle}
type="text"
/>
<span>使用Ref绑定inputValue值</span>
</div>
</div>
);
}
export default HooksUseRef;
在上面的案例中,我们如果要取input的值,如果是state双向绑定,可以直接取inputValue,如果是用ref,则可以通过inputRef.current.value取到值
通过const inputRef = useRef(null);,我们获取到的是一个对象,而current属性就是其中的dom元素。
七: useCallBack
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
简而言之,useCallBack是用来缓存函数的,在class类中,我们通常在constructor中使用this.fn = this.fn.bind(this)来绑定this,是每次调用的fn都是之前的fn,而不用开辟新的函数。而useCallback同样有此功能,useCallBack和useMemo的不同点在于useMemo相当于缓存state,而useCallBack相当于缓存函数,官方给的解释是这样的useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。.
我们下面还是用计数器和年龄做例子:
import React, { useState, useEffect, useCallback } from 'react';
function Show({ countCallBack, ageCallBack }) {
const [count, setCount] = useState(() => { countCallBack(); });
const [age, setAge] = useState(() => { ageCallBack(); });
useEffect(() => {
setCount(countCallBack());
}, [countCallBack]);
useEffect(() => {
setAge(ageCallBack());
}, [ageCallBack]);
return (
<div>
数量:{count} 年龄:{age}
</div>
);
}
function HooksCallBack() {
const [count, setCnt] = useState(0);
const [age, setAge] = useState(16);
const countCallBack = useCallback(() => {
return count;
}, [count]);
const ageCallBack = useCallback(() => {
return age;
}, []);
return (
<div>
<p>小女子芳年{age}</p>
<p>你点击了{count}次</p>
<button
type="button"
onClick={() => { setAge(age + 1); }}
>点击年龄+1</button>
<button
type="button"
onClick={() => { setCnt(count + 1); }}
>点击计数器+1</button>
<Show countCallBack={countCallBack} ageCallBack={ageCallBack} />
</div>
);
}
export default HooksCallBack;
在上面的例子中,只有点击计数器按钮,子组件才会跟着更新,点击年龄按钮子组件则不跟着更新。使用useCallback如果没有依赖,则只会执行一次,只有依赖改变,才会返回新的函数,我们可以根据这个规则实现bind的效果。
八: useImperativeHandle
useImperativeHandle可以让你在使用ref时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用。
useImperativeHandle(ref, createHandle, [deps])
- 通过useImperativeHandle可以只暴露特定的操作
- 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
- 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
const JMInput = forwardRef((props, ref) => {
const inputRef = useRef()
// 作用: 减少父组件获取的DOM元素属性,只暴露给父组件需要用到的DOM方法
// 参数1: 父组件传递的ref属性
// 参数2: 返回一个对象,父组件通过ref.current调用对象中方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
}))
return <input type="text" ref={inputRef} />
})
export default function ImperativeHandleDemo() {
// useImperativeHandle 主要作用:用于减少父组件中通过forward+useRef获取子组件DOM元素暴露的属性过多
// 为什么使用: 因为使用forward+useRef获取子函数式组件DOM时,获取到的dom属性暴露的太多了
// 解决: 使用uesImperativeHandle解决,在子函数式组件中定义父组件需要进行DOM操作,减少获取DOM暴露的属性过多
const inputRef = useRef()
return (
<div>
<button onClick={() => inputRef.current.focus()}>聚焦</button>
<JMInput ref={inputRef} />
</div>
)
}
useImperativeHandle使用简单总结:
- 作用: 减少暴露给父组件获取的DOM元素属性, 只暴露给父组件需要用到的DOM方法
- 参数1: 父组件传递的ref属性
- 参数2: 返回一个对象, 以供给父组件中通过ref.current调用该对象中的方法
九:useLayoutEffect
布局副作用
- useEffect在浏览器渲染完成后执行
- useLayoutEffect在浏览器渲染前执行
特点:
- useLayoutEffect总是比useEffect先执行
- useLayoutEffect里面的任务最好影响了Layout(布局)
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";
function App() {
const [n, setN] = useState(0)
const time = useRef(null)
const onClick = ()=>{
setN(i=>i+1)
time.current = performance.now()
}
useLayoutEffect(()=>{ // 改成 useEffect 试试
if(time.current){
console.log(performance.now() - time.current)
}
})
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>Click</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
十:useDebugValue
useDebugValue 用于在 React 开发者工具(如果已安装,在浏览器控制台 React 选项查看)中显示 自定义 Hook 的标签。
useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
import React, { useState, useDebugValue } from 'react';
import ReactDOM from 'react-dom';
// 自定义 Hook
function useMyCount(num) {
const [ count, setCount ] = useState(0);
// 延迟格式化
useDebugValue(count > num ? '溢出' : '不足', status => {
return status === '溢出' ? 1 : 0;
});
const myCount = () => {
setCount(count + 2);
}
return [ count, myCount ];
}
function App() {
const [ count, seCount ] = useMyCount(10);
return (
<div>
{count}
<button onClick={() => seCount()}>setCount</button>
</div>
)
}
ReactDOM.render(<App />, root);
我们不推荐你向每个自定义 Hook 使用 useDebugValue,只有自定义 Hook 被复用时才最有意义。