React 之 Hooks解析

一、概念

1. class组件的优势

  • class组件可以定义自己的state,用来保存组件自己内部的状态
    • 函数式组件不可以,因为函数每次调用都会产生新的临时变量
  • class组件有自己的生命周期我们可以在对应的生命周期中完成自己的逻辑,比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次
    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求
  • class组件可以在状态改变时只会重新执行render函数以及我们希望重新调用的生命周期函数componentDidUpdate等
    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次

2. Class组件存在的问题

  • 复杂组件变得难以理解
    • 随着业务的增多,我们的class组件会变得越来越复杂
    • 比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在componentWillUnmount中移除)
    • 这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度
  • 组件复用状态很难
    • 在前面为了一些状态的复用需要通过高阶组件
    • 像之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用
    • 或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,代码就会存在很多嵌套
    • 这些代码让我们不管是编写和设计上来说,都变得非常困难

3. 为什么需要Hook

Hook 是 React 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期)

hooks:

  • 它可以让我们在不编写class的情况下使用state以及其他的React特性
  • 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决
  • 完全可选的:你无需重写任何已有代码就可以在一些组件中尝试 Hook。但是如果你不想,你不必现在就去学习或使用 Hook
  • 100% 向后兼容的:Hook 不包含任何破坏性改动
  • 现在可用:Hook 已发布于 v16.8.0

Hook的使用场景:

  • Hook的出现基本可以代替我们之前所有使用class组件的地方
  • 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它
  • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

4. 计数器案例

  • 函数式组件结合hooks让整个代码变得非常简洁
  • 并且再也不用考虑this相关的问题

类组件实现

// 快捷键 => rpce

import React, { PureComponent } from 'react';

export class CountClass extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      counter: 0
    };
  }
  counterChange(num) {
    this.setState({
      counter: this.state.counter + num
    });
  }
  render() {
    // 快捷键 => dob
    const { counter } = this.state;
    return (
      <>
        <div>当前记数 : {counter}</div>
        <button onClick={(e) => this.counterChange(1)}>+1</button>
        <button onClick={(e) => this.counterChange(-1)}>-1</button>
      </>
    );
  }
}

export default CountClass;

Hook实现

// 快捷键盘 => rmc

import { memo, useState } from 'react';

const App = memo(() => {
  const [counter, setCounter] = useState(0);
  return (
    <>
      <div>当前计数 : {counter}</div>
      <button onClick={(e) => setCounter(counter + 1)}>+1</button>
      <button onClick={(e) => setCounter(counter - 1)}>-1</button>
    </>
  );
});

export default App;

Class组件和Hook对比

二、useState

State Hook     =>     用来创建state

1. 解析

useState来自react,需要从react中导入,它是一个hook

  • 参数:初始化值,如果不设置为undefined
    • 只有在第一次渲染的时候执行
  • 返回值:数组,包含两个元素
    • 元素一:当前状态的值(第一调用为初始化值)
    • 元素二:设置状态值的函数
  • 调用设置状态值的函数
    • 设置一个新的值
    • 组件重新渲染,并且根据新的值返回DOM结构( 相当于重新执行render函数 )

2. 规则

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用

3. 详解

State Hook的API就是 useState : 

  • useState会帮助我们定义一个 state变量,useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同
    • 一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留
  • useState接受唯一一个参数,在第一次组件被调用时使用来作为初始化值
    • 如果没有传递参数,那么初始化值为undefined
  • useState的返回值是一个数组,我们可以通过数组的解构,来完成赋值会非常方便
  // 1. 直接传入
const [count, setCount] = useState(0);
setCount(100);
const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ x: 100, y: 100 });

// 2. 传入方法 => 方法会立即执行
const [name, setName] = useState(() => {
  return 'hello';
});

三、useEffect

Effect Hook     =>     用来创建生命周期

1. 概念

Effect Hook 可以完成一些类似于class中生命周期的功能

  • 网络请求
  • 手动更新DOM
  • 一些事件的监听
  • 对于完成这些功能的Hook被称之为 Effect Hook 

2. 解析

useEffect的解析:

  • 通过useEffect的Hook,可以告诉React需要在渲染后执行某些操作
  • useEffect要求传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
  • 默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

3. 案例

页面的title总是显示counter的数字

类组件实现

// 快捷键 => rpce

import React, { PureComponent } from 'react';

export class CountClass extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      counter: 66
    };
  }
  componentDidMount() {
    document.title = this.state.counter;
  }

  // 1. 使用这个生命周期函数的时候
  componentDidUpdate() {
    document.title = this.state.counter;
  }

  counterChange(num) {
    this.setState(
      {
        counter: this.state.counter + num
      },
      () => {
        // 2. 或者使用这个回调函数
        document.title = this.state.counter;
      }
    );
  }
  render() {
    // 快捷键 => dob
    const { counter } = this.state;
    return (
      <>
        <div>当前记数 : {counter}</div>
        <button onClick={(e) => this.counterChange(1)}>+1</button>
        <button onClick={(e) => this.counterChange(-1)}>-1</button>
      </>
    );
  }
}

export default CountClass;

Hook实现

// 快捷键盘 => rmc

import { memo, useState, useEffect } from 'react';

const App = memo(() => {
  const [counter, setCounter] = useState(999);
  // 1. 直接赋值,因为每次都会重新渲染,所以每次都会重新赋值 => 不推荐
  // document.title = counter;

  // 2. 使用useEffect,当前传入的回调函数会在组件被渲染完成后,自动执行
  useEffect(() => {
    // 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等
    document.title = counter;
  });

  return (
    <>
      <div>当前计数 : {counter}</div>
      <button onClick={(e) => setCounter(counter + 1)}>+1</button>
      <button onClick={(e) => setCounter(counter - 1)}>-1</button>
    </>
  );
});

export default App;

4. 需要清除Effect

在class组件中,某些副作用的代码,需要在componentWillUnmount中进行清除

在useEffect中 : 

  • 传入的回调函数A本身可以有一个返回值,这个返回值是另外一个回调函数B
    • type EffectCallback = () => (void | (() => void | undefined))
  • 这是 effect 可选的清除机制每个 effect 都可以返回一个清除函数
    • 如此可以将添加和移除订阅的逻辑放在一起
    • 它们都属于 effect 的一部分
  • React 会在组件更新和卸载的时候执行清除操作
useEffect(() => {

  // 1. 事件坚听、定时器、网络请求、订阅消息、手动修改DOM、修改ref等等   =>   第一次只执行这里,
  const unsubcribe = sotre.subscribe(()=>{

  })
  function foo(){}
  eventBus.on('xxx',foo)

  // 2. 返回一个函数,这个函数会在组件要重新渲染或者被销毁的时候自动执行   =>   之后都是先执行这里,再执行上面的
  return ()=>{
    unsubcribe()
    eventBus.off('xxx',foo)
  }
    
});

5. 使用多个Effect

一个函数式组件中,可以存在多个useEffect

React 将按照 effect 声明的顺序依次调用组件中的每一个 effect

// 1. 修改标题
useEffect(() => {
  document.title = counter;
});

// 2. 监听事件总线
useEffect(() => {
  function foo() {}
  eventBus.on('xxx', foo);
  return () => {
    eventBus.off('xxx', foo);
  };
});

// 3. 监听redux中的数据变化
useEffect(() => {
  const unsubcribe = sotre.subscribe(() => {});
  return () => {
    unsubcribe();
  };
});

6. Effect性能优化

问题

默认情况下,useEffect的回调函数会在每次渲染时都重新执行,但是这会导致两个问题

  • 某些代码只是希望执行一次即可,类似于componentDidMount和componentWillUnmount中完成的事情
    • 比如网络请求、订阅和取消订阅
  • 多次执行也会导致一定的性能问题

解决

useEffect实际上有两个参数

  • 参数一:执行的回调函数
  • 参数二:该useEffect在哪些state发生变化时,才重新执行(受谁的影响)
    • 如果每次都要执行  =>  只要重新渲染组件,都会执行
      • 不传第二个参数即可
    • 如果只需要执行一次
      • 不依赖任何的内容,可以传入一个空的数组
      • 相当于componentDidMount和componentWillUnmount
    • 如果根据修改的值执行对应的effect
      • 传入一个数组,数组中放需要监听的数据

案例

受count影响的Effect  =>  会重新执行

// 快捷键盘 => rmc

import { memo, useState, useEffect } from 'react';

const App = memo(() => {
  const [counter, setCounter] = useState(999);
  const [isShow, setIsShow] = useState(true);

  // 监听counter的变化
  useEffect(() => {
    document.title = counter;
    console.log('监听counter的变化');
  }, [counter]);

  // 监听isShow的变化
  useEffect(() => {
    console.log('监听isShow的变化');
  }, [isShow]);

  //  监听事件总线
  useEffect(() => {
    console.log('事件总线');
  }, []);

  //  监听redux中的数据变化
  useEffect(() => {
    console.log('订阅数据');
  }, []);

  //  发起网络请求
  useEffect(() => {
    console.log('发起网络请求');
  }, []);

  return (
    <>
      <div>当前计数 : {counter}</div>
      <button onClick={(e) => setCounter(counter + 1)}>+1</button>
      <button onClick={(e) => setCounter(counter - 1)}>-1</button>
      <button onClick={(e) => setIsShow(!isShow)}>切换 {isShow ? '显示' : '隐藏'}</button>
    </>
  );
});

export default App;

 四、useContext

在之前的开发中,要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context
  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context

Context Hook允许通过Hook来直接获取某个Context的值

Context传入的值更改时,会触发子组件的重新渲染

1. 创建context

import { createContext } from 'react';

const counterContext = createContext();

const themeContext = createContext();

export { counterContext, themeContext };

2. 配置context

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { counterContext, themeContext } from './context';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <counterContext.Provider value={{ counter: 666 }}>
    <themeContext.Provider value={{ theme: 'dark', color: 'red' }}>
      <App />
    </themeContext.Provider>
  </counterContext.Provider>
);

3. 使用context

import React, { memo, useContext } from 'react';
// 1. 导入context
import { CounterContext, ThemeContext } from './context';

const App = memo(() => {
  // 2. 使用context
  const count = useContext(CounterContext);
  const theme = useContext(ThemeContext);
  return (
    <>
      <div>App</div>
      <div style={{ color: theme.color }}>count: {count.counter}</div>
      <div>theme: {theme.color}</div>
    </>
  );
});

export default App;

五、useReducer

useReducer不是redux的某个替代品

useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分
  • 或者这次修改的state需要依赖之前的state时,也可以使用

1. 使用useState

import React, { memo, useState } from 'react';

const App = memo(() => {
  const [count, setCount] = useState(0);

  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
      <button onClick={(e) => setCount(count - 1)}>-1</button>
      <button onClick={(e) => setCount(count + 5)}>+5</button>
      <button onClick={(e) => setCount(count - 5)}>-5</button>
      <button onClick={(e) => setCount(count + 100)}>+100</button>
    </>
  );
});

export default App;

2. 使用useReducer 

import React, { memo, useReducer } from 'react';

// 1. 定义 reducer 函数
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, counter: state.counter + 1 };
    case 'decrement':
      return { ...state, counter: state.counter - 1 };
    case 'add_number':
      return { ...state, counter: state.counter + action.num };
    case 'sub_number':
      return { ...state, counter: state.counter - action.num };
    default:
      return state;
  }
}

const App = memo(() => {
  // 2. 使用 useReducer 函数,传入 reducer 函数和初始值,得到 state 和 dispatch 函数
  const [state, dispatch] = useReducer(reducer, { counter: 0, friends: [], user: {} });

  return (
    <>
      {/* 3. 使用 state 中的数据, 通过 state.属性名 来获取 */}
      <h2>当前计数: {state.counter}</h2>
      {/* 4. 使用 dispatch 函数,传入 action 对象,来触发 reducer 函数,从而改变 state */}
      <button onClick={(e) => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={(e) => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={(e) => dispatch({ type: 'add_number', num: 5 })}>+5</button>
      <button onClick={(e) => dispatch({ type: 'sub_number', num: 5 })}>-5</button>
      <button onClick={(e) => dispatch({ type: 'add_number', num: 100 })}>+100</button>
    </>
  );
});

export default App;

六、useCallback

useCallback实际的目的是为了进行性能的优化 => 返回值是优化后的函数

如何进行性能的优化 : 

  • useCallback会返回一个函数的 memoized(记忆的) 值
  • 依赖不变的情况下,多次定义的时候,返回的值是相同的 

1. 初始

import React, { memo, useState } from 'react';

const App = memo(() => {
  const [count, setCount] = useState(0);

  return (
    <>
      <h2>当前计数: {count}</h2>
      {/* 每次点击+1,都会重新渲染App组件,导致子组件也会重新渲染,该箭头函数也会重新创建,浪费性能 */}
      <button onClick={(e) => setCount(count + 1)}>+1</button>
    </>
  );
});

export default App;

2. 更改

import React, { memo, useState, useCallback } from 'react';

const App = memo(() => {
  const [count, setCount] = useState(0);

  // useCallback 用于缓存函数,但是不会缓存函数中的变量,也就是传入的函数依然会重新创建,所以这样写是没用的
  // 和原来的写法一样,没有任何优化
  const handleBtnClick = useCallback((e) => {
    setCount(count + 1);
  });

  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={handleBtnClick}>+1</button>
    </>
  );
});

export default App;

3. 修复

import React, { memo, useState, useCallback } from 'react';

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
  // const handleBtnClick = useCallback(function (e) {
  //   setCount(count + 1);
  // }, []);

  // 传入第二个参数,依赖项数组,当依赖项发生变化时,才会重新创建函数,否则复用之前的函数,避免了闭包陷阱
  const handleBtnClick = useCallback(function (e) {
    setCount(count + 1);
  }, [count]);
  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={handleBtnClick}>+1</button>
    </>
  );
});

export default App;

4. 解释

上方做法依然没有进行优化,当count改变的时候,依然会创建新的函数

和普通的没有区别

当把函数从父组件传递给子组件,父组件有其他数据更改时,该优化才能生效

普通函数效果

  • 当点击修改count时
    • 不管点击父组件还是子组件,因为handleBtnClick重新创建,所以都会重新渲染
  • 当点击修改name的时候
    • 子组件也会修改
    • 因为App组件重新执行,handleBtnClick重新创建,子组件接收的props更改了
    • 但这是不应该的

import React, { memo, useState, useCallback } from 'react';

const Home = memo((props) => {
  console.log('Home组件被调用');

  // 3. 子组件通过props接收父组件传递过来的函数
  const { handleBtnClick } = props;
  return (
    <>
      <h2>Home组件</h2>
      {/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
      <button onClick={handleBtnClick}>Home : +1</button>
        
      {/* 假如有100个组件 => 全都会重新渲染 */}
    </>
  );
});

const App = memo(() => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('jack');

  // 1. 使用普通方法定义的函数
  const handleBtnClick = (e) => setCount(count + 1);

  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={handleBtnClick}>+1</button>
      <h2>当前名称: {name}</h2>
      <button onClick={(e) => setName(Math.random())}>修改名称</button>
      <hr />

      {/* 2. 把函数传递给子组件,子组件通过props接收 */}
      <Home handleBtnClick={handleBtnClick} />
    </>
  );
});

export default App;

使用useCallback效果

  • 当点击修改count时
    • 不管点击父组件还是子组件,因为handleBtnClick重新创建,所以都会重新渲染
  • 当点击修改name的时候
    • 子组件并不会重新渲染
    • 因为count没有更改,所以handleBtnClick函数不会重新创建,props没有改变

import React, { memo, useState, useCallback } from 'react';

const Home = memo((props) => {
  console.log('Home组件被调用');

  // 3. 子组件通过props接收父组件传递过来的函数
  const { handleBtnClick } = props;
  return (
    <>
      <h2>Home组件</h2>
      {/* 4. 子组件通过props接收父组件传递过来的函数,并且调用 */}
      <button onClick={handleBtnClick}>Home : +1</button>

      {/* 假如有100个组件 => 该组件不重新渲染,其下的组件也都不会,大大优化了 */}
    </>
  );
});

const App = memo(() => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('jack');

  // 1. 通过useCallback包裹函数,把函数传递给子组件
  const handleBtnClick = useCallback(
    function (e) {
      setCount(count + 1);
    },
    [count]
  );

  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={handleBtnClick}>+1</button>
      <h2>当前名称: {name}</h2>
      <button onClick={(e) => setName(Math.random())}>修改名称</button>
      <hr />

      {/* 2. 把函数传递给子组件,子组件通过props接收 */}
      <Home handleBtnClick={handleBtnClick} />
    </>
  );
});

export default App;

5. 进一步优化

当count发生改变的时候,也使用同一个函数

做法一

将count依赖移除掉 => 缺点 : 闭包陷阱


// 小心闭包陷阱 => 当不传依赖的时候,函数只会创建一次。依赖项中的 count 一直是初始值 0,所以每次点击按钮,count 都为 1,而不是累加 => 没有创建新的函数
const handleBtnClick = useCallback(function (e) {
  setCount(count + 1);
}, []);

做法二 

做法一  + 使用useRef => 特点 : 在组件多次渲染时,返回的是同一个值

import React, { memo, useState, useCallback, useRef } from 'react';

const Home = memo((props) => {
  console.log('Home组件被调用,只会在组件第一次渲染时调用');
  const { handleBtnClick } = props;
  return (
    <>
      <h2>Home组件</h2>
      <button onClick={handleBtnClick}>Home : +1</button>
    </>
  );
});

const App = memo(() => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('jack');

  // 1. useRef 保存的值,在组件重新渲染时,不会发生改变
  const countRef = useRef();
  // 2. 但是,可以通过修改 ref.current 的值,来达到保存数据的目的
  countRef.current = count;

  const handleBtnClick = useCallback(
    (e) => {
      setCount(countRef.current + 1);
    },
    // 3. 去除依赖项,让 useCallback 每次都返回同一个函数
    []
  );

  return (
    <>
      <h2>当前计数: {count}</h2>
      <button onClick={handleBtnClick}>+1</button>
      <h2>当前名称: {name}</h2>
      <button onClick={(e) => setName(Math.random())}>修改名称</button>
      <hr />

      <Home handleBtnClick={handleBtnClick} />
    </>
  );
});

export default App;

6. 总结

  • 使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化
    •  不会,和普通函数的定义没有区别
  • 使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化
    • ​​​​​​​会,当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化后的函数传递给子组件

七、useMemo

useMemo实际的目的也是为了进行性能的优化 => 返回值是一个值

进行大量的计算操作,可以使用useMemo进行优化

1. 不依赖任何的值

只会被执行一次

import React, { memo, useMemo, useState } from 'react';

// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
function calcNumTotal(num) {
  console.log('calcNumTotal的计算过程被调用~');
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 2.不依赖任何的值, 进行计算 => 只会被执行一次
  const result = useMemo(() => {
    return calcNumTotal(50);
  }, []);

  return (
    <>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
    </>
  );
});

export default App;

2. 依赖count

每次count发生变化, 都会重新计算

import React, { memo, useMemo, useState } from 'react';

// 1. 写在函数组件外面的代码, 只会被执行一次,防止被多次创建
function calcNumTotal(num) {
  console.log('calcNumTotal的计算过程被调用~');
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}

const App = memo(() => {
  const [count, setCount] = useState(0);

  // 2.依赖count, 进行计算 => 每次count发生变化, 都会重新计算
  const result = useMemo(() => {
    return calcNumTotal(count * 2);
  }, [count]);

  return (
    <>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>
    </>
  );
});

export default App;

3. useMemo和useCallback的对比

  /**
   * useMemo和useCallback的对比. useCallback返回的是一个函数, useMemo返回的是一个值
   * 1. useCallback(fn, []) => fn
   * 2. useMemo(() => fn, []) => fn()
   * 这两种写法表示的意思是一样的
   */
  function fn() {}
  // 返回值是一个函数 => useCallback
  const increment = useCallback(fn, []);
  // 返回值是一个值 => useMemo
  const increment2 = useMemo(() => fn, []);

4. 对子组件渲染进行优化

如果对子组件传入相同内容的对象时,可以进行优化 => 因为每次执行函数,都会创建新对象,子组件就会重新渲染

如果传递给子组件的是个值的情况下,值不改变,子组件就不会重新渲染

import React, { memo, useMemo, useState } from 'react';

// 创建一个子组件
const HelloWorld = memo(function (props) {
  console.log('HelloWorld被渲染~');
  return <h2>Hello World</h2>;
});

function calcNumTotal(num) {
  console.log('calcNumTotal的计算过程被调用~');
  let total = 0;
  for (let i = 1; i <= num; i++) {
    total += i;
  }
  return total;
}

const App = memo(() => {
  const [count, setCount] = useState(0);

  const result = useMemo(() => {
    return calcNumTotal(50);
  }, []);

  // 1. 每次都会创建一个新的对象,导致子组件的props发生变化
  // const info = { name: 'why', age: 18 }; // 会导致子组件的props发生变化,从而导致子组件的重新渲染

  // 2. 使用useMemo对子组件渲染进行优化,只有当info发生变化的时候,才会重新创建新的对象,否则使用缓存的对象 => 优化子组件的渲染
  const info = useMemo(() => ({ name: 'why', age: 18 }), []); // 不会导致子组件的props发生变化,从而不会导致子组件的重新渲染

  return (
    <>
      <h2>计算结果: {result}</h2>
      <h2>计数器: {count}</h2>
      <button onClick={(e) => setCount(count + 1)}>+1</button>

      <HelloWorld result={result} info={info} />
    </>
  );
});

export default App;

5. 总结

  • 进行大量的计算操作,是否有必须要每次渲染时都重新计算
    • ​​​​​​​可以使用useMemo进行优化
  • 对子组件传递相同内容的对象时,使用useMemo进行性能的优化
    • 父组件有不变的静态对象时,可以进行优化

​​​​​​​八、useRef

useRef返回一个ref对象,返回的ref对象再组件的整个生命周期保持不变

ref是两种用法:

  • 用法一:引入DOM(或者组件,但是需要是class组件)元素
  • 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变 

引入DOM

import React, { memo, useRef } from 'react';

const App = memo(() => {
  // 1. useRef() 生成一个 ref 对象
  const titleRef = useRef();

  //3. 通过 ref 对象的 current 属性获取引用的元素
  const getTitleRef = () => {
    console.log(titleRef.current);
  };
  
  return (
    <>
      {/* 2. 将 ref 对象传递给需要引用的元素 */}
      <div ref={titleRef}>App111</div>
      <button onClick={getTitleRef}>获取titleRef</button>
    </>
  );
});

export default App;

保存数据

使用ref保存上一次的某一个值

import React, { memo, useCallback, useRef, useState } from 'react';

const App = memo(() => {
  const [count, setCount] = useState(0);

  const countRef = useRef(0);
  countRef.current = count;

  const handleClick = useCallback((e) => {
    // 如果直接使用 count,会有闭包问题 !!! => 因为这里不依赖外界,所以只会创建一次函数, count 是在 handleClick 函数创建时就已经存在了,而不是每次渲染时才创建
    // setCount(count + 1);

    // 如果使用 countRef.current,就不会有闭包问题 !!! => 因为这里依赖外界,所以每次渲染都会创建新的函数, countRef.current 是在 handleClick 函数执行时才存在的
    setCount(countRef.current + 1);
  }, []);
  return (
    <>
      <div>App : {count}</div>
      <button onClick={handleClick}>+1</button>
    </>
  );
});

export default App;

九、useImperativeHandle

在父组件中,通过传递ref绑定子组件,获取的权限太高

可以通过useImperativeHandle,来约束外界的权限

import React, { memo, useRef, forwardRef, useImperativeHandle } from 'react';

// 1. 创建子组件 => 接收ref
const Child = memo(
  forwardRef((props, ref) => {
    // 2. 创建自己内部的ref
    const childRef = useRef();

    // 4. 暴露给父组件的方法 => 通过useImperativeHandle暴露给父组件
    useImperativeHandle(ref, () => ({
      focus: () => {
        childRef.current.focus();
      },
      setValue(value) {
        childRef.current.value = value;
      }
    }));

    // 3. 将自己内部的ref绑定
    return <input ref={childRef} />;
  })
);

const App = memo(() => {
  // 5. 创建父组件的ref
  const parentRef = useRef();

  function handleDOM() {
    // 6. 调用子组件暴露给父组件的方法 => parentRef.current拿到的是useImperativeHandle返回的对象
    // console.log(parentRef.current)
    parentRef.current.focus();
    parentRef.current.setValue('哈哈哈');

    // 无法直接操作子组件的value,因为没有暴露给父组件
    // parentRef.current.value = ""

  }
  return (
    <>
      <Child ref={parentRef}></Child>
      <button onClick={handleDOM}>DOM操作</button>
    </>
  );
});

export default App;

十、useLayoutEffect

useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已

  • useEffect
    • 会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新
  • useLayoutEffect
    • 会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新 

如果希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect

官方更推荐使用useEffect而不是useLayoutEffect

import React, { memo, useEffect, useLayoutEffect } from 'react';

const App = memo(() => {

  useLayoutEffect(() => {
    console.log('useLayoutEffect', '第二执行');
  });

  useEffect(() => {
    console.log('useEffect', '第三执行');
  });

  console.log('App', '第一执行');

  return <div>App</div>;
});

export default App;

十一、自定义Hook

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性

注意 : 名称需要用useXXX

1. 打印生命周期

所有的组件在创建和销毁时都进行打印

import { memo, useEffect, useState } from 'react';

// 1. 自定义hook,命名必须以use开头 => 生命周期
const useLife = (name) => {
  useEffect(() => {
    console.log(name + '被创建');
    return () => {
      console.log(name + '被销毁');
    };
  }, [name]);
};

const FirstComponent = memo(() => {
  // 2. 使用自定义hook
  useLife('firstComponent');
  return <h1>firstComponent</h1>;
});

const SecondComponent = memo(() => {
  // 2. 使用自定义hook
  useLife('secondComponent');
  return <h1>secondComponent</h1>;
});

const App = memo(() => {
  // 2. 使用自定义hook
  useLife('App');
  const [isShow, setIsShow] = useState(true);

  return (
    <>
      <h1>App</h1>

      <button onClick={() => setIsShow(!isShow)}>切换</button>
      {isShow && <FirstComponent />}
      {isShow && <SecondComponent />}
    </>
  );
});

export default App;

2. Context的共享数据

创建context/index.js

import { createContext } from 'react';

const CounterContext = createContext();

const ThemeContext = createContext();

export { CounterContext, ThemeContext };

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import { CounterContext, ThemeContext } from './context';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <CounterContext.Provider value={{ counter: 666 }}>
    <ThemeContext.Provider value={{ theme: 'dark', color: 'red' }}>
      <App />
    </ThemeContext.Provider>
  </CounterContext.Provider>
);

App.jsx

import { memo, useEffect, useContext } from 'react';
import { CounterContext, ThemeContext } from './context';

// 1. 自定义hook,命名必须以use开头 => context抽取共享
const useSelfContext = (name) => {
  const counterContext = useContext(CounterContext);
  const themeContext = useContext(ThemeContext);
  return { counterContext, themeContext };
};

const FirstComponent = memo(() => {
  // 2. 使用自定义hook
  const { counterContext, themeContext } = useSelfContext();
  return (
    <>
      <h1>firstComponent</h1>
      <div>
        counterContext {'=>'} {counterContext.counter}
      </div>
      <div>
        themeContext {'=>'} {themeContext.theme}
      </div>
    </>
  );
});

const SecondComponent = memo(() => {
  // 2. 使用自定义hook
  const { counterContext, themeContext } = useSelfContext();
  return (
    <>
      <h1>secondComponent</h1>
      <div>
        counterContext {'=>'} {counterContext.counter}
      </div>
      <div>
        themeContext {'=>'} {themeContext.theme}
      </div>
    </>
  );
});

const App = memo(() => {
  return (
    <>
      <h1>App</h1>

      <FirstComponent />
      <SecondComponent />
    </>
  );
});

export default App;

3. 获取滚动位置

import { memo, useEffect, useContext, useState } from 'react';

// 1. 自定义hook,命名必须以use开头 => 获取滚动位置
const useScrollPosition = () => {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(
    () => {
      function scrollFn() {
        console.log('scroll', window.scrollY, window.scrollX);
        setPosition({ x: window.scrollX, y: window.scrollY });
      }
      window.addEventListener('scroll', scrollFn);
      return () => {
        window.removeEventListener('scroll', scrollFn);
      };
    },
    // 不依赖,只执行一次
    []
  );
  return position;
};

const FirstComponent = memo(() => {
  // 2. 使用自定义hook
  const position = useScrollPosition();
  return (
    <>
      <h1>
        firstComponent : {position.x} - {position.y}
      </h1>
    </>
  );
});

const SecondComponent = memo(() => {
  return (
    <>
      <h1>secondComponent</h1>
    </>
  );
});

const App = memo(() => {
  return (
    <div style={{ height: '2000px' }}>
      <h1>App</h1>

      <FirstComponent />
      <SecondComponent />
    </div>
  );
});

export default App;

4. localStorage数据存储

import { memo, useEffect, useState } from 'react';

// 自定义hook,命名必须以use开头 => localStorage数据存储
const useLocalStorage = (key) => {
  // 1. 通过 localStorage 获取数据,初始化 state
  const [data, setData] = useState(() => {
    return JSON.parse(localStorage.getItem(key)) || '';
  });

  // 2. 监听 localStorage 变化,更新 state
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(data));
  }, [data, key]);

  // 3. 返回 state 和更新 state 的函数
  return [data, setData];
};

const SecondComponent = memo(() => {
  // 1. 使用自定义 hook
  const [name, setName] = useLocalStorage('name');
  return (
    <>
      <h1>secondComponent : {name}</h1>
      <button onClick={() => setName(Math.random().toFixed(2))}>setName</button>
    </>
  );
});

const App = memo(() => {
  const [age, setAge] = useLocalStorage('age');

  return (
    <div>
      <h1>App</h1>
      <h1>age : {age}</h1>
      <button onClick={() => setAge((Math.random() + 10).toFixed(2))}>setAge</button>
      <hr />
      <SecondComponent />
    </div>
  );
});

export default App;

十二、redux hooks

在之前的redux开发中,为了让组件和redux结合起来,我们使用了react-redux中的connect:

  • 但是这种方式必须使用高阶函数结合返回的高阶组件
  • 并且必须编写:mapStateToProps和 mapDispatchToProps映射的函数

在Redux7.1开始,提供了Hook的方式,再也不需要编写connect以及对应的映射函数了

配置

安装

npm i react-redux @reduxjs/toolkit

目录结构

store/modules/counter.js

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    counter: 66,
    message: 'Hello Redux'
  },
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementAction(state, { payload }) {
      state.counter += payload;
    },
    messageChangeAction(state, { payload }) {
      state.message = payload;
    }
  }
});

export default counterSlice.reducer;

export const { incrementAction, messageChangeAction } = counterSlice.actions;

store/index.js

import { configureStore } from '@reduxjs/toolkit';

import counterReducer from './modules/counter';

const store = configureStore({
  reducer: {
    counter: counterReducer
    // Add the generated reducer as a specific top-level slice
  }
});

export default store;

index.js 

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';

import { Provider } from 'react-redux';
import store from './store';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

之前的使用方法

import React, { memo } from 'react';
import { connect } from 'react-redux';
import { incrementAction } from './store/modules/counter';

const App = memo((props) => {
  // 4. 接收状态和dispatch
  const { counter, increment } = props;
  return (
    <>
      <div>App</div>
      <div>counter: {counter}</div>
      <button onClick={() => increment(5)}>+5</button>
      <button onClick={() => increment(-5)}>-5</button>
    </>
  );
});

// 1. 映射状态
const mapStateToProps = (state) => ({
  counter: state.counter.counter
});

// 2. 映射dispatch
const mapDispatchToProps = (dispatch) => ({
  increment(num) {
    dispatch(incrementAction(num));
  }
});

// 3. connect高阶组件
export default connect(mapStateToProps, mapDispatchToProps)(App);

hooks中使用

  • useSelector : 将state映射到组件中
    • ​​​​​​​参数
      • 参数一:将state映射到需要的数据中 => 是个回调函数
      • 参数二:可以进行比较来决定是否组件重新渲染
    • useSelector默认会比较我们返回的两个对象是否相等
      • 比较方式  const refEquality = (a, b) => a === b
      • 也就是必须返回两个完全相等的对象才可以不引起重新渲染
  • useDispatch : 接获取dispatch函数
  • useStore : 获取当前的store对象

基本使用

import React, { memo } from 'react';
// 1. 导入
import { useSelector, useDispatch } from 'react-redux';
import { incrementAction } from './store/modules/counter';

const Home = memo((props) => {
  const state = useSelector((state) => ({
    message: state.counter.message
  }));

  console.log('Home render');
  return (
    <>
      <div>Home</div>
      <div>message: {state.message}</div>
    </>
  );
});

const App = memo((props) => {
  // 2. 获取store中的数据, 通过useSelector映射到组件中
  const state = useSelector((state) => ({
    counter: state.counter.counter
  }));

  // 3. 获取dispatch, 通过useDispatch映射到组件中
  const dispatch = useDispatch();
  const incrementHandle = (num) => {
    // 4. 调用dispatch, 传入action
    dispatch(incrementAction(num));
  };

  console.log('App render');

  return (
    <>
      <div>App</div>
      <div>counter: {state.counter}</div>
      <button onClick={(e) => incrementHandle(5)}>+5</button>
      <button onClick={(e) => incrementHandle(-5)}>-5</button>
      <hr />
      <Home />
    </>
  );
});

export default App;

存在问题

子组件home使用了memo : memo包裹的组件,只有当props改变时,才会重新渲染

但是App组件更改了counter后,Home组件也跟着刷新了

Home组件更改message后,App也重新渲染了

 

原因 : 因为useSelector默认监听的是整个state,只要有一个地方改变了,全部重新渲染

解决方法

在使用useSelector时,第二个参数传入浅层比较函数shallowEqual即可

// 导入入shallowEqual
import { useSelector, useDispatch, shallowEqual } from 'react-redux';

// ...

const state = useSelector(
  (state) => ({
    message: state.counter.message
  }),
  shallowEqual
);

// ...

const state = useSelector(
  (state) => ({
    counter: state.counter.counter
  }),
  shallowEqual
);