2019 年 2 月 6 日,React 官方 推出 React v16.8.0,稳定的 Hooks 功能出世。
class 组件 和 函数组件
class 组件的缺点
组件间的状态逻辑很难复用
组件间如果有 state 的逻辑是相似的,class 模式下基本都是用高阶组件来解决的。 虽然能解决问题,但是我们需要再组件外部再包一层元素,会导致层级很冗余
复杂业务的有状态组件会越来越复杂
随着业务功能的增加,组件需要写的逻辑越来越多,一个生命周期中充斥着各种逻辑。 后期维护,或者想拆分成更小的组件,十分麻烦
监听和定时器的操作,分散在多个区域,修改代码容易漏,维护很麻烦
- this 指向的问题
函数组件的 Hook
React Hooks 和 函数式组件的配合,更能适应函数式编程的思维。
- 数学上定义的函数公式是:
y = f(x) 如果将
状态 state视为输入 x,视图 UI视为输出 y,编写的函数组件为Fn,那么可以写出这样一个式子:UI = Fn(state)将 React 的 Hooks,按自变量和因变量划分
自变量
为什么说这三个 hook 是自变量呢?
因为他们如果值发生改变,会导致组件UI更新,和依赖它们这些值的 Hook 发生变化。
按照我们上述的 UI = Fn(state),他们就相当于这些 state
useState
基本使用
// 创建 state 的方式const [state, setState] = useState(defaultState)// 更新方式一:传值setState(nextState);// 更新方式二:传函数// 传入的函数,会接收到一个参数是原先 state 的值,该函数的返回值将作为更新后 state 的值setState(preState => nextState);
示例
// 写一个组件:input 框的值改变,p 标签的值跟着改变import { useState } from 'react';function StateDemo(props) {const [val, setVal] = useState('');const changeHandler = (e) => setVal(e.target.value)return (<div><input onChange={changeHandler} /><p>{val}</p></div>)}export default StateDemo
useReducer
基本使用
// 创建方式/*** 传入参数:* reducer{Function}: 形如 (state, action) => {},该函数返回的值作为更新的 reducerState* initialArg{any}: 若无 init 初始化函数,则 initialArg 直接作为 reducerState。* 若有 init 初始化函数,则 initialArg 作为 init 参数* init{Function}: init 函数的返回值,作为初始化的 reducerState* 输出参数* reducerState{any}: 状态* dispatch{Function}: 用来更新状态*/const [reducerState, dispatch] = useReducer(reducer, initialArg, init);
示例
import { useReducer } from 'react';const reducer = (state, action) => {const { count } = state;switch (action.type) {case 'increment':return { count: count + 1 }case 'decrement':return { count: count - 1 }default:return { count: count }}}const init = (initialArg) => {return { count: initialArg }}function ReducerDemo(props) {const [reducerState, dispatch] = useReducer(reducer, 0, init);return (<div><p>{reducerState.count}</p><button onClick={() => dispatch({ type: 'increment' })}>递增</button><button onClick={() => dispatch({ type: 'decrement' })}>递减</button></div>)}export default ReducerDemo
useContext
在 useContext 推出之前,我们使用 createContext的方式如下
// Father 组件import { Component, createContext } from 'react';export const Context = createContext();class Father extends Component {state = {name: 'John'}render() {return (<Context.Provider value={this.state.name}><Son /></Context.Provider>)}}
// Son 组件import { Component } from 'react';import { Context } from './Father'// 类组件的写法如下class Son extends Component {render() {return (<Context.Consumer>{(value) => <p>{value}</p>}</Context.Consumer>)}}// 函数式组件的写法如下function Son(props) {return (<Context.Consumer>{(value) => <p>{value}</p>}</Context.Consumer>)}export default Son;
在 useContext 推出之后,我们使用 createContext 的方法有了变化
基本使用
useContext需要和 createContext配合使用
// 创建一个 context 实例const ContextInstance = createContext(defaultValue);// 使用 useContext 获取到 context 实例的值const contextValue = useContext(ContextInstance);
示例
利用上述Father和Son例子,用useContext将 Son改写
// Son 组件import { Component } from 'react';import { Context } from './Father'function Son(props) {const contextValue = useContext(Context);return (<p>{contextValue}</p>)}export default Son;
因变量
为什么说他们是因变量呢?
因为组件state的变化,可能引起他们的变化。
它们依赖于一些值,会随着值的变化而重新执行
useMemo
基本用法
作用:用来缓存任意的值
性能优化:可以使用 useMemo 来阻止昂贵的、资源密集型的功能不必要地运行
/*** 输入参数* fn{Function}: 形如 () => value,返回的值,将作为 useMemo 的输出* dependencies{Array | undefined}: useMemo 依赖的所有自变量,任意一个自变量变化,都会让 useMemo 重新计算返回值* dependencies 为 undefined 时,函数组件每次执行,useMemo 都会重新计算返回值* 输出参数:* memoriedValue{any}:输入参数 fn 返回的值*/const memoizedValue = useMemo(fn, dependencies);
示例
如下组件,新增 todo 列表会卡顿,原因是每次都触发 expensiveCalculation
解决方式这里列举两种:
- 使用 useMemo
- 状态下移
import { useState } from "react";// 繁重的计算const expensiveCalculation = (num) => {console.log("Calculating...");for (let i = 0; i < 1000000000; i++) {num += 1;}return num;};const App = () => {const [count, setCount] = useState(0);const [todos, setTodos] = useState([]);const calculation = expensiveCalculation(count);const increment = () => {setCount((c) => c + 1);};const addTodo = () => {setTodos((t) => [...t, "New Todo"]);};return (<div><div><h2>My Todos</h2><button onClick={addTodo}>Add Todo</button>{todos.map((todo, index) => {return <p key={index}>{todo}</p>;})}</div><hr /><div>Count: {count}<button onClick={increment}>+</button><h2>Expensive Calculation</h2>{calculation}</div></div>);};export default App
使用 useMemo
import { useState, useMemo } from "react";// 繁重的计算const expensiveCalculation = (num) => {console.log("Calculating...");for (let i = 0; i < 1000000000; i++) {num += 1;}return num;};const App = () => {const [count, setCount] = useState(0);const [todos, setTodos] = useState([]);// 使用 useMemoconst calculation = useMemo(() => expensiveCalculation(count), [count]);const increment = () => {setCount((c) => c + 1);};const addTodo = () => {setTodos((t) => [...t, "New Todo"]);};return (<div><div><h2>My Todos</h2><button onClick={addTodo}>Add Todo</button>{todos.map((todo, index) => {return <p key={index}>{todo}</p>;})}</div><hr /><div>Count: {count}<button onClick={increment}>+</button><h2>Expensive Calculation</h2>{calculation}</div></div>);};export default App
状态下移
将 todo 列表抽成一个组件,todo 的状态变化,不会引起
expensiveCalculation的重新计算
import { useState } from "react";import Todo from './Todo'// 繁重的计算const expensiveCalculation = (num) => {console.log("Calculating...");for (let i = 0; i < 1000000000; i++) {num += 1;}return num;};const App = () => {const [count, setCount] = useState(0);const calculation = useMemo(() => expensiveCalculation(count), [count]);const increment = () => {setCount((c) => c + 1);};return (<div><Todo /><hr /><div>Count: {count}<button onClick={increment}>+</button><h2>Expensive Calculation</h2>{calculation}</div></div>);};export default App
import { useState } from "react";const Todo = () => {const [todos, setTodos] = useState([]);const addTodo = () => {setTodos((t) => [...t, "New Todo"]);};return (<div><h2>My Todos</h2><button onClick={addTodo}>Add Todo</button>{todos.map((todo, index) => {return <p key={index}>{todo}</p>;})}</div>)}export default Todo;
useCallback
基本用法
useCallback(fn, deps)相当于useMemo(() => fn, deps)
/*** 输入参数* fn{Function}: 形如 () => value,这个函数将作为 useCallback 的输出* dependencies{Array | undefined}: useCallback 依赖的所有自变量,任意一个自变量变化,都会让 useCallback 重新生成一个函数返回* dependencies 为 undefined 时,函数组件每次执行,useCallback 都会重新生成一个函数返回* 输出参数:* memorieFn{Function}:参数 fn*/const memorieFn = useCallback(fn, dependencies);
useEffect
在了解 useEffect 之前,我们先来了解一下什么是 副作用。
在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响 —— 维基百科
副作用是函数调用过程中除了返回值以外,对外部上下文的影响。
基本用法
在 useEffect 中,常做的副作用操作有
- 操作 DOM 元素
- 修改数据:比如
setState、修改 ref 指向 - 发送 HTTP 请求
由于 useEffect 是在组件渲染完成之后调用的,所以在这个时机,进行副作用的操作
useEffect(()=>{// 执行需要的副作用操作// 返回的函数,会在该组件被卸载时调用return () => {// 组件卸载时,执行的副作用操作}}, dependencies)
示例
操作 DOM
import { useEffect } from 'react';const EffectDemo = () => {useEffect(() => {const img = document.getElementById('img');img.src = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F30%2F90%2F40%2F309040a0602c672cebc6ab3a1bbbc8cd.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646197473&t=21786df0de7c0a297437b6d14bbfd5af';}, [])return (<img id="img" src="" alt="" />)}export default EffectDemo
发送 HTTP 请求并修改数据 ```jsx import { useState, useEffect } from ‘react’; import axios from ‘axios’; const EffectDemo = () => { const [lists, setList] = useState([]) useEffect(() => { async function fetchData() { return await axios.get(‘https:/test.com/api/getList’); } const data = fetchData(); setList(data) }, [])
return (
-
{lists.map(item =>
- {item.name} )}
export default EffectDemo
<a name="qMkSt"></a># 用于追踪的 useRef由于 useRef 返回的 ref 对象,该对象在组件的整个生命周期都保持不变,只能手动改变值。> ⭐ ref 值的变化,不会引起组件的更新。> 补充:createRef 创建的 ref,如果是在组件内声明的,组件更新时,会创建新的 ref为什么要叫追踪的 useRef 呢,因为它创建的值只能手动改变,它不会变化。<br />利用这个特点,我们把 DOM 元素 / 某些数据 存放在 ref 上。即使组件更新,引用值还是没变。<br />相当于我们追踪了某个东西,不管他跑到那,都追着他不放。> 或者叫做 用于访问的 ref 也不错。> 当我们用 ref 绑定了组件内部某个数据,暴露给组件外界使用时,外界可以访问组件内部的数据。>> 为什么 react 官方说,少点用 ref 呢?> 个人理解是,react 希望组件的编写更加符合函数式编程,如果外界可以访问组件内部的数据,甚至修改组件内部数据。那么根据函数式编程的 `UI = Fn(state)` 就变成了 `Fn = f(ref)` + `UI = Fn(state)`。> `Fn`会变得不确定,怎么符合函数式编程的思想:固定输入`(state)` 产生 固定输出`(UI)`呢<a name="tETXu"></a>## 基本用法```javascript/** 输入参数:* initalValue{any}: 任何的数据。* 返回参数:* refContainer{object}: { current: initialValue }*/const refContainer = useRef(initialValue);
示例
1. 追踪某个 DOM 元素
const RefDemo = () => {const [count, setCount] = useState(0)const ref = useRef();useEffect(() => {console.log(ref); // ref.current = <div id="div">{count}</div>}, [])return (<ul><button onClick={() => setCount(c => c + 1)}>{count}</button><div ref={ref} >{count}</div></ul>)}
2. 追踪类组件实例
import { useRef, Component, useEffect } from 'react';const Father = () => {const classRef = useRef();useEffect(() => {console.log(classRef);}, [])return (<div><SonClassCompoent ref={(a) => { classRef.current = a }} />{/* 方式二:<SonClassCompoent ref={classRef} />*/}</div>)}class SonClassCompoent extends Component {render() {return (<div >sonClass</div>)}}export default Father
3. 配合 forwardRef 追踪函数式组件中的数据
3.1 追踪函数式组件中的 DOM 元素
import { useRef, forwardRef, useEffect } from 'react';const SonFunctionCompoent = (props, ref) => {return (<div ref={ref}>sonFunction</div>)}// 函数式组件用 forwardRef 包裹一层// forwardRef 会将外界传进来的 ref 属性,转发给函数式组件 SonFunctionCompoent 的第二个参数const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);const Father = () => {const fnRef = useRef();useEffect(() => {console.log(fnRef);}, [])return (<div><SonFnWithForwardRef ref={fnRef} /></div>)}export default Father
3.2 追踪函数式组件的 state
import { useRef, forwardRef, useEffect, useState } from 'react';const SonFunctionCompoent = (props, ref) => {const [count, setCount] = useState(0);useEffect(() => {ref.current = count;}, [count, ref])return (<div><button onClick={() => setCount(c => c + 1)}> {count}</button></div >)}const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);const Father = () => {const fnRef = useRef();return (<div><SonFnWithForwardRef ref={fnRef} /><button onClick={() => console.log(fnRef)}>打印 fnRef</button></div>)}export default Father
4. 配合 useImperativeHandle 追踪函数式组件的自定义 ref
先来看看 useImperativeHandle 的用法
/*** 输入参数:* ref{Ref 实例}: 函数式组件外部传入的 ref* createHandle{Function}: 该函数返回的值,将作为 ref.current 的值* deps: 依赖的参数,参数变化,重新计算 ref*/useImperativeHandle(ref, createHandle, [deps])
如果需要限制外部访问组件内部,特定数据的属性方法,可以考虑使用这个函数

import { useRef, forwardRef, useState, useImperativeHandle } from 'react';const SonFunctionCompoent = (props, ref) => {const [data, setData] = useState({});useImperativeHandle(ref, () => {return {name: data.name,pwd: data.pwd}}, [data])const handleSubmit = (e) => {const formElement = e.target;const nameElement = formElement[0];const pwdElement = formElement[1];const data = {name: nameElement.value,pwd: pwdElement.value}setData(data);}return (<div><form action="" target="iframe" onSubmit={handleSubmit}><label name="name"><input type="text" placeholder="name" /></label><label name="password"><input type="password" placeholder="password" /></label><button onClick={() => { }}>保存</button ></form>{/* 阻止 form 表单默认跳转行为 */}<iframe title="none" name="iframe" style={{ display: 'none' }}></iframe></div >)}const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);const Father = () => {const fnRef = useRef();return (<div><SonFnWithForwardRef ref={fnRef} /><button onClick={() => console.log(fnRef)}>打印 fnRef</button></div>)}export default Father
自定义 hook
概念
hooks 专注的就是逻辑复用,使我们的项目,不仅仅停留在组件复用的层面上。
自定义 hooks 让我们可以将一段通用的逻辑存封起来。
我们自定义的 hooks 大概应该长这样
自定义 hook 的执行时机
hook 本质就是一个函数。
每次组件更新,都会导致执行自定义 hook。
示例
- 用于获取请求的
useFetch```javascript import { useState, useCallback, useEffect, useRef } from ‘react’ import axios from ‘axios’
export const useFetch = (options) => { const [loading, setLoad] = useState(false); const [data, setData] = useState(); const [error, setError] = useState(‘’); const fetchConfig = useRef(options); // 缓存请求配置 /**
- 缓存请求执行函数
- data{any}: 当 isReset 为 true 时,请求配置为 data
isReset{boolean}: 是否需要重置 */ const run = useCallback((data, isReset = false) => { return new Promise(async (resolve, reject) => { setLoad(true); if (data) { if (isReset) fetchConfig.current = data; else {
if (fetchConfig.method.toLowerCase() === 'get') {fetchConfig.current.params = data;} else {fetchConfig.current.data = data;}
} } try { const res = await axios(data); setLoad(false); setData(res) resolve(res); } catch (error) { setLoad(false); setError(error); reject(error); } }) }, [])
// 如果第一次有具体的请求数据才发 useEffect(() => { if (options.data || options.params) { setLoad(true); axios(fetchConfig.current).then(res => { setLoad(false); setData(res) }).catch(err => { setLoad(false); setError(err); }) } return () => options.data = null; // eslint-disable-next-line }, [])
return { loading, data, error, run };
参考资料
《函数式组件与类组件有何不同?》
《useEffect 完整指南》
《React useMemo Hook》
《新年第一篇:一起来简单聊一下副作用 - effect》
《ahooks —— 一个好用的 hook 库》
《玩转react-hooks,自定义hooks设计模式及其实战》
