React 之 Hooks解析

news2024/11/23 2:43:48

一、概念

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
);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1003560.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Postman拦截浏览器请求

项目上线之后&#xff0c;难免会有BUG。在出现问题的时候&#xff0c;我们可能需要获取前端页面发送请求的数据&#xff0c;然后在测试环境发送相同的数据将问题复现。手动构建数据是挺麻烦的一件事&#xff0c;所以我们可以借助Postman在浏览器上的插件帮助拦截请求&#xff0…

2023最新PDF阅读器评测

评测说明 本人程序员&#xff0c;平时阅读为主。以下为主观实际体验感受为主。 软件选择 以无广、可免费使用为基本要求。Adobe Reader 自不必说。 体验软件 SumatraPDF 特点&#xff1a;简洁。开源免费的小个子软件&#xff0c;当前最新安装包只有7M&#xff0c;启动速度很…

【初阶算法4】——归并排序的详解,及其归并排序的扩展

目录 前言 学习目标&#xff1a; 学习内容&#xff1a; 一、介绍归并排序 1.1 归并排序的思路 1.2 归并排序的代码 1.2.1 mergesort函数部分 1.2.2 process函数部分 1.2.3 merge函数部分 二、AC两道经典的OJ题目 题目一&#xff1a;逆序对问题 题目二&#xff1…

笔记本选购指南

大学生笔记本电脑选购指南 文章目录 笔记本分类指标排行 了解自身需求理工科文科艺术总结 参考指标品牌CPU显卡屏幕其他 购买渠道推荐游戏本Redmi G 锐龙版联想G5000惠普光影精灵9天选4锐龙版联想R7000P暗影精灵9联想拯救者R9000P 全能本华硕无畏PRO15联想小新Pro14 2023 轻薄本…

react ant ice3 实现点击一级菜单自动打开它下面最深的第一个子菜单

1.问题 默认的如果没有你的菜单结构是这样的&#xff1a; [{children: [{name: "通用配置"parentId: "1744857774620672"path: "basic"}],name: "系统管理"parentId: "-1"path: "system"} ]可以看到每层菜单的p…

期权投资的优势有哪些方面?

随着金融市场的不断演变&#xff0c;越来越多的金融衍生品出现在人们的视线中&#xff0c;特别是上证50ETF期权可以做空T0的交易模式吸引了越来越多的朋友&#xff0c;那么期权投资的优势有哪些方面&#xff1f; 期权是投资市场中一个非常重要的投资方式&#xff0c;期权投资能…

SOLIDWORKS装配体如何使用全局变量

客户痛点&#xff1a;随着人力资源价格的增长&#xff0c;设计的时间需要减少时间&#xff0c;提高设计效率。 数据问题&#xff1a;以前单个数据都需要建立单独的数据结构&#xff0c;装配体的模型都要重新建立。 需要解决的问题&#xff1a;能够快速地完成3D模型及装配体的…

TensorFlow 03(Keras)

一、tf.keras tf.keras是TensorFlow 2.0的高阶API接口&#xff0c;为TensorFlow的代码提供了新的风格和设计模式&#xff0c;大大提升了TF代码的简洁性和复用性&#xff0c;官方也推荐使用tf.keras来进行模型设计和开发。 1.1 tf.keras中常用模块 如下表所示: 1.2 常用方法 …

机器学习——协同过滤算法(CF)

机器学习——协同过滤算法&#xff08;CF&#xff09; 文章目录 前言一、基于用户的协同过滤1.1. 原理1.2. 算法步骤1.3. 代码实现 二、基于物品的协同过滤2.1. 原理2.2. 算法步骤2.3. 代码实现 三、比较与总结四、实例解析总结 前言 协同过滤算法是一种常用的推荐系统算法&am…

清理 Ubuntu 系统的 4 个简单步骤

清理 Ubuntu 系统的 4 个简单步骤 现在&#xff0c;试试看这 4 个简单的步骤&#xff0c;来清理你的 Ubuntu 系统吧。 这份精简指南将告诉你如何清理 Ubuntu 系统以及如何释放一些磁盘空间。 如果你的 Ubuntu 系统已经运行了至少一年&#xff0c;尽管系统是最新的&#xff0c;…

2003-2022年黄河流域TCI、VCI、VHI、TVDI逐年1km分辨率数据集

摘要 黄河流域大部分属于干旱、半干旱气候,先天水资源条件不足,是中国各大流域中受干旱影响最为严重的流域。随着全球环境和气候变化,黄河流域的干旱愈加频繁,对黄河流域的干旱监测研究已经成为当下的热点。本数据集基于MODIS植被和地表温度产品,通过对逐年数据进行去云、…

Mendix使用Upload image新增修改账户头像

学习Mendix中级文档&#xff0c;其中有个管理我的账号功能&#xff0c;确保账号主任可以修改其头像&#xff0c;接下来记录如何实现账户头像的上传和修改。根据文档的步骤实现功能&#xff5e;&#xff5e; 新建GeneralExtentions模块&#xff0c;给GeneralExtentions添加两个模…

MapTR v2文章研读

MapTR v2论文来了&#xff0c;本文仅介绍v2相较于v1有什么改进之处&#xff0c;如果想了解v1版本的论文细节&#xff0c;可见链接。 相较于maptr&#xff0c;maptr v2改进之处&#xff1a; 在分层query机制中引进解耦自注意力机制&#xff0c;有效降低了内存消耗&#xff1b;…

Spring中如何解决循环依赖问题

一、什么是循环依赖 循环依赖也叫循环引用&#xff0c;是指bean之间形成相互依赖的关系&#xff0c;由此&#xff0c;bean对象在属性注入时便会产生循环。这种循环依赖会导致编译器无法编译代码&#xff0c;从而无法运行程序。为了避免循环依赖&#xff0c;我们在开发过程中需…

视频号视频下载工具有那些?我们怎么下载视频号里面的视频

本篇文章给大家谈谈视频号视频下载工具&#xff0c;以及视频号视频如何下载?对应的知识点&#xff0c;希望对各位有所帮助。 视频号里面的视频可以下载吗&#xff1f; 视频号官方首先是不提供下载功能的&#xff0c;但是很多第三方可以提供视频号的视频下载功能。 早期版本视…

【力扣每日一题】2023.9.12 课程表Ⅳ

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 今天是课程表系列题目的最后一题&#xff0c;因为我在题库里找不到课程表5了&#xff0c;所以今天的每日一题就是最后一个课程表了。 题…

小节5:Python列表list常用操作

1、对列表的基本认知&#xff1a; 列表list&#xff0c;是可变类型。比如&#xff0c;append()函数会直接改变列表本身&#xff0c;往列表里卖弄添加元素。所以&#xff0c;list_a list_a.append(123)就是错误的。如果想删除列表中的元素&#xff0c;可以用remove()函数&…

基于微信小程序的宠物寄养平台,附源码、数据库

1. 简介 本文正是基于微信小程序开发平台&#xff0c;针对宠物寄养的需求,本文设计出一个包含寄养家庭分类、寄养服务管理、宠物档案、交流论坛的微信小程序,以此帮助宠物寄养的实现,促进宠物寄养工作的进展。 2 开发技术 微信小程序的运行环境分为渲染层和逻辑层&#xff0…

仿照Everything实现的文件搜索工具--SearchEverything

一、项目介绍 项目名称&#xff1a;SearchEverything 项目简介&#xff1a;SearchEverything是仿照Everything实现的一款桌面级的文件搜索软件,它是Everything的增强版&#xff0c;支持跨平台的使用。 项目功能&#xff1a; 1.选择文件夹后&#xff0c;多线程扫描文件夹下的…

学会这个技能,写字楼立马高级起来!

在当今现代化社会中&#xff0c;写字楼已成为商业和行政活动的中心。成千上万的人们每天涌入这些高楼大厦&#xff0c;从事各种各样的工作&#xff0c;以实现公司和组织的目标。然而&#xff0c;与这种繁忙的办公环境一样&#xff0c;也带来了一系列的安全挑战和管理难题。 随着…