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([]);
// 使用 useMemo
const 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设计模式及其实战》