2019 年 2 月 6 日,React 官方 推出 React v16.8.0,稳定的 Hooks 功能出世。

class 组件 和 函数组件

为什么要有 hook?

class 组件的缺点

  1. 组件间的状态逻辑很难复用

    组件间如果有 state 的逻辑是相似的,class 模式下基本都是用高阶组件来解决的。 虽然能解决问题,但是我们需要再组件外部再包一层元素,会导致层级很冗余

  2. 复杂业务的有状态组件会越来越复杂

    随着业务功能的增加,组件需要写的逻辑越来越多,一个生命周期中充斥着各种逻辑。 后期维护,或者想拆分成更小的组件,十分麻烦

  3. 监听和定时器的操作,分散在多个区域,修改代码容易漏,维护很麻烦

  4. this 指向的问题

    函数组件的 Hook

React Hooks 和 函数式组件的配合,更能适应函数式编程的思维。

  1. 数学上定义的函数公式是:y = f(x)
  2. 如果将状态 state视为 输入 x视图 UI视为 输出 y,编写的函数组件为 Fn,那么可以写出这样一个式子:UI = Fn(state)

  3. 将 React 的 Hooks,按自变量和因变量划分

React 的 Hook - 图1

自变量

为什么说这三个 hook 是自变量呢?
因为他们如果值发生改变,会导致组件UI更新,和依赖它们这些值的 Hook 发生变化。
按照我们上述的 UI = Fn(state),他们就相当于这些 state

useState

基本使用

  1. // 创建 state 的方式
  2. const [state, setState] = useState(defaultState)
  3. // 更新方式一:传值
  4. setState(nextState);
  5. // 更新方式二:传函数
  6. // 传入的函数,会接收到一个参数是原先 state 的值,该函数的返回值将作为更新后 state 的值
  7. setState(preState => nextState);

示例

  1. // 写一个组件:input 框的值改变,p 标签的值跟着改变
  2. import { useState } from 'react';
  3. function StateDemo(props) {
  4. const [val, setVal] = useState('');
  5. const changeHandler = (e) => setVal(e.target.value)
  6. return (
  7. <div>
  8. <input onChange={changeHandler} />
  9. <p>{val}</p>
  10. </div>
  11. )
  12. }
  13. export default StateDemo

useReducer

基本使用

  1. // 创建方式
  2. /**
  3. * 传入参数:
  4. * reducer{Function}: 形如 (state, action) => {},该函数返回的值作为更新的 reducerState
  5. * initialArg{any}: 若无 init 初始化函数,则 initialArg 直接作为 reducerState。
  6. * 若有 init 初始化函数,则 initialArg 作为 init 参数
  7. * init{Function}: init 函数的返回值,作为初始化的 reducerState
  8. * 输出参数
  9. * reducerState{any}: 状态
  10. * dispatch{Function}: 用来更新状态
  11. */
  12. const [reducerState, dispatch] = useReducer(reducer, initialArg, init);

示例

  1. import { useReducer } from 'react';
  2. const reducer = (state, action) => {
  3. const { count } = state;
  4. switch (action.type) {
  5. case 'increment':
  6. return { count: count + 1 }
  7. case 'decrement':
  8. return { count: count - 1 }
  9. default:
  10. return { count: count }
  11. }
  12. }
  13. const init = (initialArg) => {
  14. return { count: initialArg }
  15. }
  16. function ReducerDemo(props) {
  17. const [reducerState, dispatch] = useReducer(reducer, 0, init);
  18. return (
  19. <div>
  20. <p>{reducerState.count}</p>
  21. <button onClick={() => dispatch({ type: 'increment' })}>递增</button>
  22. <button onClick={() => dispatch({ type: 'decrement' })}>递减</button>
  23. </div>
  24. )
  25. }
  26. export default ReducerDemo

useContext

在 useContext 推出之前,我们使用 createContext的方式如下

  1. // Father 组件
  2. import { Component, createContext } from 'react';
  3. export const Context = createContext();
  4. class Father extends Component {
  5. state = {
  6. name: 'John'
  7. }
  8. render() {
  9. return (
  10. <Context.Provider value={this.state.name}>
  11. <Son />
  12. </Context.Provider>
  13. )
  14. }
  15. }
  1. // Son 组件
  2. import { Component } from 'react';
  3. import { Context } from './Father'
  4. // 类组件的写法如下
  5. class Son extends Component {
  6. render() {
  7. return (
  8. <Context.Consumer>
  9. {
  10. (value) => <p>{value}</p>
  11. }
  12. </Context.Consumer>
  13. )
  14. }
  15. }
  16. // 函数式组件的写法如下
  17. function Son(props) {
  18. return (
  19. <Context.Consumer>
  20. {
  21. (value) => <p>{value}</p>
  22. }
  23. </Context.Consumer>
  24. )
  25. }
  26. export default Son;

在 useContext 推出之后,我们使用 createContext 的方法有了变化

基本使用

useContext需要和 createContext配合使用

  1. // 创建一个 context 实例
  2. const ContextInstance = createContext(defaultValue);
  3. // 使用 useContext 获取到 context 实例的值
  4. const contextValue = useContext(ContextInstance);

示例

利用上述FatherSon例子,用useContextSon改写

  1. // Son 组件
  2. import { Component } from 'react';
  3. import { Context } from './Father'
  4. function Son(props) {
  5. const contextValue = useContext(Context);
  6. return (
  7. <p>{contextValue}</p>
  8. )
  9. }
  10. export default Son;

因变量

为什么说他们是因变量呢?
因为组件state的变化,可能引起他们的变化。
它们依赖于一些值,会随着值的变化而重新执行

useMemo

基本用法

作用:用来缓存任意的值

性能优化:可以使用 useMemo 来阻止昂贵的、资源密集型的功能不必要地运行

  1. /**
  2. * 输入参数
  3. * fn{Function}: 形如 () => value,返回的值,将作为 useMemo 的输出
  4. * dependencies{Array | undefined}: useMemo 依赖的所有自变量,任意一个自变量变化,都会让 useMemo 重新计算返回值
  5. * dependencies 为 undefined 时,函数组件每次执行,useMemo 都会重新计算返回值
  6. * 输出参数:
  7. * memoriedValue{any}:输入参数 fn 返回的值
  8. */
  9. const memoizedValue = useMemo(fn, dependencies);

示例

如下组件,新增 todo 列表会卡顿,原因是每次都触发 expensiveCalculation

解决方式这里列举两种:

  1. 使用 useMemo
  2. 状态下移
  1. import { useState } from "react";
  2. // 繁重的计算
  3. const expensiveCalculation = (num) => {
  4. console.log("Calculating...");
  5. for (let i = 0; i < 1000000000; i++) {
  6. num += 1;
  7. }
  8. return num;
  9. };
  10. const App = () => {
  11. const [count, setCount] = useState(0);
  12. const [todos, setTodos] = useState([]);
  13. const calculation = expensiveCalculation(count);
  14. const increment = () => {
  15. setCount((c) => c + 1);
  16. };
  17. const addTodo = () => {
  18. setTodos((t) => [...t, "New Todo"]);
  19. };
  20. return (
  21. <div>
  22. <div>
  23. <h2>My Todos</h2>
  24. <button onClick={addTodo}>Add Todo</button>
  25. {todos.map((todo, index) => {
  26. return <p key={index}>{todo}</p>;
  27. })}
  28. </div>
  29. <hr />
  30. <div>
  31. Count: {count}
  32. <button onClick={increment}>+</button>
  33. <h2>Expensive Calculation</h2>
  34. {calculation}
  35. </div>
  36. </div>
  37. );
  38. };
  39. export default App

使用 useMemo

  1. import { useState, useMemo } from "react";
  2. // 繁重的计算
  3. const expensiveCalculation = (num) => {
  4. console.log("Calculating...");
  5. for (let i = 0; i < 1000000000; i++) {
  6. num += 1;
  7. }
  8. return num;
  9. };
  10. const App = () => {
  11. const [count, setCount] = useState(0);
  12. const [todos, setTodos] = useState([]);
  13. // 使用 useMemo
  14. const calculation = useMemo(() => expensiveCalculation(count), [count]);
  15. const increment = () => {
  16. setCount((c) => c + 1);
  17. };
  18. const addTodo = () => {
  19. setTodos((t) => [...t, "New Todo"]);
  20. };
  21. return (
  22. <div>
  23. <div>
  24. <h2>My Todos</h2>
  25. <button onClick={addTodo}>Add Todo</button>
  26. {todos.map((todo, index) => {
  27. return <p key={index}>{todo}</p>;
  28. })}
  29. </div>
  30. <hr />
  31. <div>
  32. Count: {count}
  33. <button onClick={increment}>+</button>
  34. <h2>Expensive Calculation</h2>
  35. {calculation}
  36. </div>
  37. </div>
  38. );
  39. };
  40. export default App

状态下移

将 todo 列表抽成一个组件,todo 的状态变化,不会引起 expensiveCalculation的重新计算

  1. import { useState } from "react";
  2. import Todo from './Todo'
  3. // 繁重的计算
  4. const expensiveCalculation = (num) => {
  5. console.log("Calculating...");
  6. for (let i = 0; i < 1000000000; i++) {
  7. num += 1;
  8. }
  9. return num;
  10. };
  11. const App = () => {
  12. const [count, setCount] = useState(0);
  13. const calculation = useMemo(() => expensiveCalculation(count), [count]);
  14. const increment = () => {
  15. setCount((c) => c + 1);
  16. };
  17. return (
  18. <div>
  19. <Todo />
  20. <hr />
  21. <div>
  22. Count: {count}
  23. <button onClick={increment}>+</button>
  24. <h2>Expensive Calculation</h2>
  25. {calculation}
  26. </div>
  27. </div>
  28. );
  29. };
  30. export default App
  1. import { useState } from "react";
  2. const Todo = () => {
  3. const [todos, setTodos] = useState([]);
  4. const addTodo = () => {
  5. setTodos((t) => [...t, "New Todo"]);
  6. };
  7. return (
  8. <div>
  9. <h2>My Todos</h2>
  10. <button onClick={addTodo}>Add Todo</button>
  11. {todos.map((todo, index) => {
  12. return <p key={index}>{todo}</p>;
  13. })}
  14. </div>
  15. )
  16. }
  17. export default Todo;

useCallback

作用:用来缓存函数

基本用法

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

  1. /**
  2. * 输入参数
  3. * fn{Function}: 形如 () => value,这个函数将作为 useCallback 的输出
  4. * dependencies{Array | undefined}: useCallback 依赖的所有自变量,任意一个自变量变化,都会让 useCallback 重新生成一个函数返回
  5. * dependencies 为 undefined 时,函数组件每次执行,useCallback 都会重新生成一个函数返回
  6. * 输出参数:
  7. * memorieFn{Function}:参数 fn
  8. */
  9. const memorieFn = useCallback(fn, dependencies);

useEffect

在了解 useEffect 之前,我们先来了解一下什么是 副作用。

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响 —— 维基百科

副作用是函数调用过程中除了返回值以外,对外部上下文的影响。

React 的 Hook - 图2

基本用法

在 useEffect 中,常做的副作用操作有

  • 操作 DOM 元素
  • 修改数据:比如 setState、修改 ref 指向
  • 发送 HTTP 请求

    由于 useEffect 是在组件渲染完成之后调用的,所以在这个时机,进行副作用的操作

  1. useEffect(()=>{
  2. // 执行需要的副作用操作
  3. // 返回的函数,会在该组件被卸载时调用
  4. return () => {
  5. // 组件卸载时,执行的副作用操作
  6. }
  7. }, dependencies)

示例

  1. 操作 DOM

    1. import { useEffect } from 'react';
    2. const EffectDemo = () => {
    3. useEffect(() => {
    4. const img = document.getElementById('img');
    5. 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';
    6. }, [])
    7. return (
    8. <img id="img" src="" alt="" />
    9. )
    10. }
    11. export default EffectDemo
  2. 发送 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

  1. <a name="qMkSt"></a>
  2. # 用于追踪的 useRef
  3. 由于 useRef 返回的 ref 对象,该对象在组件的整个生命周期都保持不变,只能手动改变值。
  4. > ⭐ ref 值的变化,不会引起组件的更新。
  5. > 补充:createRef 创建的 ref,如果是在组件内声明的,组件更新时,会创建新的 ref
  6. 为什么要叫追踪的 useRef 呢,因为它创建的值只能手动改变,它不会变化。<br />利用这个特点,我们把 DOM 元素 / 某些数据 存放在 ref 上。即使组件更新,引用值还是没变。<br />相当于我们追踪了某个东西,不管他跑到那,都追着他不放。
  7. > 或者叫做 用于访问的 ref 也不错。
  8. > 当我们用 ref 绑定了组件内部某个数据,暴露给组件外界使用时,外界可以访问组件内部的数据。
  9. >
  10. > 为什么 react 官方说,少点用 ref 呢?
  11. > 个人理解是,react 希望组件的编写更加符合函数式编程,如果外界可以访问组件内部的数据,甚至修改组件内部数据。那么根据函数式编程的 `UI = Fn(state)` 就变成了 `Fn = f(ref)` + `UI = Fn(state)`。
  12. > `Fn`会变得不确定,怎么符合函数式编程的思想:固定输入`(state)` 产生 固定输出`(UI)`呢
  13. <a name="tETXu"></a>
  14. ## 基本用法
  15. ```javascript
  16. /*
  17. * 输入参数:
  18. * initalValue{any}: 任何的数据。
  19. * 返回参数:
  20. * refContainer{object}: { current: initialValue }
  21. */
  22. const refContainer = useRef(initialValue);

示例

1. 追踪某个 DOM 元素

  1. const RefDemo = () => {
  2. const [count, setCount] = useState(0)
  3. const ref = useRef();
  4. useEffect(() => {
  5. console.log(ref); // ref.current = <div id="div">{count}</div>
  6. }, [])
  7. return (
  8. <ul>
  9. <button onClick={() => setCount(c => c + 1)}>{count}</button>
  10. <div ref={ref} >{count}</div>
  11. </ul>
  12. )
  13. }

2. 追踪类组件实例

  1. import { useRef, Component, useEffect } from 'react';
  2. const Father = () => {
  3. const classRef = useRef();
  4. useEffect(() => {
  5. console.log(classRef);
  6. }, [])
  7. return (
  8. <div>
  9. <SonClassCompoent ref={(a) => { classRef.current = a }} />
  10. {/* 方式二:
  11. <SonClassCompoent ref={classRef} />
  12. */}
  13. </div>
  14. )
  15. }
  16. class SonClassCompoent extends Component {
  17. render() {
  18. return (<div >sonClass</div>)
  19. }
  20. }
  21. export default Father

3. 配合 forwardRef 追踪函数式组件中的数据

3.1 追踪函数式组件中的 DOM 元素
image.png

  1. import { useRef, forwardRef, useEffect } from 'react';
  2. const SonFunctionCompoent = (props, ref) => {
  3. return (<div ref={ref}>sonFunction</div>)
  4. }
  5. // 函数式组件用 forwardRef 包裹一层
  6. // forwardRef 会将外界传进来的 ref 属性,转发给函数式组件 SonFunctionCompoent 的第二个参数
  7. const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
  8. const Father = () => {
  9. const fnRef = useRef();
  10. useEffect(() => {
  11. console.log(fnRef);
  12. }, [])
  13. return (
  14. <div>
  15. <SonFnWithForwardRef ref={fnRef} />
  16. </div>
  17. )
  18. }
  19. export default Father

3.2 追踪函数式组件的 state
image.png

  1. import { useRef, forwardRef, useEffect, useState } from 'react';
  2. const SonFunctionCompoent = (props, ref) => {
  3. const [count, setCount] = useState(0);
  4. useEffect(() => {
  5. ref.current = count;
  6. }, [count, ref])
  7. return (
  8. <div>
  9. <button onClick={() => setCount(c => c + 1)}> {count}</button>
  10. </div >
  11. )
  12. }
  13. const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
  14. const Father = () => {
  15. const fnRef = useRef();
  16. return (
  17. <div>
  18. <SonFnWithForwardRef ref={fnRef} />
  19. <button onClick={() => console.log(fnRef)}>打印 fnRef</button>
  20. </div>
  21. )
  22. }
  23. export default Father

4. 配合 useImperativeHandle 追踪函数式组件的自定义 ref

先来看看 useImperativeHandle 的用法

  1. /**
  2. * 输入参数:
  3. * ref{Ref 实例}: 函数式组件外部传入的 ref
  4. * createHandle{Function}: 该函数返回的值,将作为 ref.current 的值
  5. * deps: 依赖的参数,参数变化,重新计算 ref
  6. */
  7. useImperativeHandle(ref, createHandle, [deps])

如果需要限制外部访问组件内部,特定数据的属性方法,可以考虑使用这个函数

image.png

  1. import { useRef, forwardRef, useState, useImperativeHandle } from 'react';
  2. const SonFunctionCompoent = (props, ref) => {
  3. const [data, setData] = useState({});
  4. useImperativeHandle(ref, () => {
  5. return {
  6. name: data.name,
  7. pwd: data.pwd
  8. }
  9. }, [data])
  10. const handleSubmit = (e) => {
  11. const formElement = e.target;
  12. const nameElement = formElement[0];
  13. const pwdElement = formElement[1];
  14. const data = {
  15. name: nameElement.value,
  16. pwd: pwdElement.value
  17. }
  18. setData(data);
  19. }
  20. return (
  21. <div>
  22. <form action="" target="iframe" onSubmit={handleSubmit}>
  23. <label name="name">
  24. <input type="text" placeholder="name" />
  25. </label>
  26. <label name="password">
  27. <input type="password" placeholder="password" />
  28. </label>
  29. <button onClick={() => { }}>保存</button >
  30. </form>
  31. {/* 阻止 form 表单默认跳转行为 */}
  32. <iframe title="none" name="iframe" style={{ display: 'none' }}></iframe>
  33. </div >
  34. )
  35. }
  36. const SonFnWithForwardRef = forwardRef(SonFunctionCompoent);
  37. const Father = () => {
  38. const fnRef = useRef();
  39. return (
  40. <div>
  41. <SonFnWithForwardRef ref={fnRef} />
  42. <button onClick={() => console.log(fnRef)}>打印 fnRef</button>
  43. </div>
  44. )
  45. }
  46. export default Father

自定义 hook

概念

hooks 专注的就是逻辑复用,使我们的项目,不仅仅停留在组件复用的层面上。
自定义 hooks 让我们可以将一段通用的逻辑存封起来。
我们自定义的 hooks 大概应该长这样
React 的 Hook - 图6

自定义 hook 的执行时机

hook 本质就是一个函数。
每次组件更新,都会导致执行自定义 hook。

示例

  1. 用于获取请求的 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 {

    1. if (fetchConfig.method.toLowerCase() === 'get') {
    2. fetchConfig.current.params = data;
    3. } else {
    4. fetchConfig.current.data = data;
    5. }

    } } 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设计模式及其实战》