2019 年 2 月 6 日,React 官方 推出 React v16.8.0,稳定的 Hooks 功能出世。
React Hooks 和 函数式组件的配合,更能适应函数式编程的思维。
y = f(x)
状态 state
视为 输入 x
,视图 UI
视为 输出 y
,编写的函数组件为 Fn
,那么可以写出这样一个式子:UI = Fn(state)
为什么说这三个 hook 是自变量呢?
因为他们如果值发生改变,会导致组件UI
更新,和依赖它们这些值的 Hook 发生变化。
按照我们上述的 UI = Fn(state)
,他们就相当于这些 state
// 创建 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 (
{val}
)
}
export default StateDemo
// 创建方式
/**
* 传入参数:
* 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 (
{reducerState.count}
)
}
export default ReducerDemo
在 useContext 推出之前,我们使用 createContext
的方式如下
// Father 组件
import { Component, createContext } from 'react';
export const Context = createContext();
class Father extends Component {
state = {
name: 'John'
}
render() {
return (
)
}
}
// Son 组件
import { Component } from 'react';
import { Context } from './Father'
// 类组件的写法如下
class Son extends Component {
render() {
return (
{
(value) => {value}
}
)
}
}
// 函数式组件的写法如下
function Son(props) {
return (
{
(value) => {value}
}
)
}
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 (
{contextValue}
)
}
export default Son;
为什么说他们是因变量呢?
因为组件state
的变化,可能引起他们的变化。
它们依赖于一些值,会随着值的变化而重新执行
作用:用来缓存任意的值
性能优化:可以使用 useMemo 来阻止昂贵的、资源密集型的功能不必要地运行
/**
* 输入参数
* fn{Function}: 形如 () => value,返回的值,将作为 useMemo 的输出
* dependencies{Array | undefined}: useMemo 依赖的所有自变量,任意一个自变量变化,都会让 useMemo 重新计算返回值
* dependencies 为 undefined 时,函数组件每次执行,useMemo 都会重新计算返回值
* 输出参数:
* memoriedValue{any}:输入参数 fn 返回的值
*/
const memoizedValue = useMemo(fn, dependencies);
如下组件,新增 todo 列表会卡顿,原因是每次都触发 expensiveCalculation
解决方式这里列举两种:
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 (
My Todos
{todos.map((todo, index) => {
return {todo}
;
})}
Count: {count}
Expensive Calculation
{calculation}
);
};
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 (
My Todos
{todos.map((todo, index) => {
return {todo}
;
})}
Count: {count}
Expensive Calculation
{calculation}
);
};
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 (
Count: {count}
Expensive Calculation
{calculation}
);
};
export default App
import { useState } from "react";
const Todo = () => {
const [todos, setTodos] = useState([]);
const addTodo = () => {
setTodos((t) => [...t, "New Todo"]);
};
return (
My Todos
{todos.map((todo, index) => {
return {todo}
;
})}
)
}
export default Todo;
作用:用来缓存函数
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 中,常做的副作用操作有
setState
、修改 ref 指向由于 useEffect 是在组件渲染完成之后调用的,所以在这个时机,进行副作用的操作
useEffect(()=>{
// 执行需要的副作用操作
// 返回的函数,会在该组件被卸载时调用
return () => {
// 组件卸载时,执行的副作用操作
}
}, dependencies)
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 (
)
}
export default EffectDemo
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
由于 useRef 返回的 ref 对象,该对象在组件的整个生命周期都保持不变,只能手动改变值。
⭐ ref 值的变化,不会引起组件的更新。
补充:createRef 创建的 ref,如果是在组件内声明的,组件更新时,会创建新的 ref
为什么要叫追踪的 useRef 呢,因为它创建的值只能手动改变,它不会变化。
利用这个特点,我们把 DOM 元素 / 某些数据 存放在 ref 上。即使组件更新,引用值还是没变。
相当于我们追踪了某个东西,不管他跑到那,都追着他不放。
或者叫做 用于访问的 ref 也不错。
当我们用 ref 绑定了组件内部某个数据,暴露给组件外界使用时,外界可以访问组件内部的数据。
为什么 react 官方说,少点用 ref 呢?
个人理解是,react 希望组件的编写更加符合函数式编程,如果外界可以访问组件内部的数据,甚至修改组件内部数据。那么根据函数式编程的 UI = Fn(state)
就变成了
Fn = f(ref)
与 UI = Fn(state)
Fn
会变得不确定,怎么符合函数式编程的思想:固定输入(state)
产生 固定输出(UI)
呢
/*
* 输入参数:
* initalValue{any}: 任何的数据。
* 返回参数:
* refContainer{object}: { current: initialValue }
*/
const refContainer = useRef(initialValue);
const RefDemo = () => {
const [count, setCount] = useState(0)
const ref = useRef();
useEffect(() => {
console.log(ref); // ref.current = {count}
}, [])
return (
{count}
)
}
import { useRef, Component, useEffect } from 'react';
const Father = () => {
const classRef = useRef();
useEffect(() => {
console.log(classRef);
}, [])
return (
{ classRef.current = a }} />
{/* 方式二:
*/}
)
}
class SonClassCompoent extends Component {
render() {
return (sonClass)
}
}
export default Father
3.1 追踪函数式组件中的 DOM 元素
import { useRef, forwardRef, useEffect } from 'react';
const SonFunctionCompoent = (props, ref) => {
return (sonFunction)
}
// 函数式组件用 forwardRef 包裹一层
// forwardRef 会将外界传进来的 ref 属性,转发给函数式组件 SonFunctionCompoent 的第二个参数
const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
const Father = () => {
const fnRef = useRef();
useEffect(() => {
console.log(fnRef);
}, [])
return (
)
}
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 (
)
}
const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
const Father = () => {
const fnRef = useRef();
return (
)
}
export default Father
先来看看 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 (
{/* 阻止 form 表单默认跳转行为 */}
)
}
const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
const Father = () => {
const fnRef = useRef();
return (
)
}
export default Father
hooks 专注的就是逻辑复用,使我们的项目,不仅仅停留在组件复用的层面上。
自定义 hooks 让我们可以将一段通用的逻辑存封起来。
我们自定义的 hooks 大概应该长这样
hook 本质就是一个函数。
每次组件更新,都会导致执行自定义 hook。
useFetch
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设计模式及其实战》