React学习记录

186 阅读7分钟

React学习记录

Hooks篇章

useState

  1. 定义:
  2. 使用:

const [value,setValue] = useState();

  1. 注意事项
  • setValue修改变量之后,会记录一张未修改变量时的快照,仅在下一次渲染时与快照对比去更新状态。如果在调用setValue之后读取value依然是快照记录的值。
  • React将state的更新加入一个队列,当下一次渲染的时候遍历队列去更新state
const [age,setAge] = useState(42);
function handleClick() {  
    setAge(age + 1); // setAge(42 + 1)  
    setAge(age + 1); // setAge(42 + 1)  
    setAge(age + 1); // setAge(42 + 1)  
}
// 可修改为
function handleClick() {  
    setAge(a => a + 1); // setAge(42 => 43)  
    setAge(a => a + 1); // setAge(43 => 44)  
    setAge(a => a + 1); // setAge(44 => 45)  
}
  • 如果新值与当前值通过[Object.is]比较是相同的,那 React 将跳过重新渲染该组件及其子组件。
  • 对于对象和数组的修改需要先将当前值解构然后加上新值,或者使用 useImmer

useRef

  1. 定义:允许使用一个渲染时不需要的值
  2. 使用:

const ref = useRef(initialValue)

  1. 注意事项
  • 只有一个属性current,当更改该ref.current属性时,React 不会重新渲染组件。
  • 总是读取当前最新的值(与 useState 对比,即改即变),可以缓存最新值,配合 useCallback 或者 useMemo 使用
    const ageRef = useRef({age:42});
    function handleClick(){
        ageRef.current.age++;//42=>43
        ageRef.current.age++;//43=>44    
    }
    
  • 操作 DOM,可以将 Ref 绑定到 DOM 上,然后通过 Ref 操作 DOM(ps:如果将ref绑定到子组件上,子组件需要使用forwardRef包裹)
    import { useRef, forwardRef } from 'react';
    export default function App() {
        const inputRef = useRef(null);
        const handleAddClick = ()=>{
            inputRef.current.focus();
            console.log(inputRef.current);
        }
        return (
            <>
                <MyInput ref={inputRef}></MyInput>
                <button onClick={handleAddClick}>inputRef</button>
            </>
            )
      }
    
      const MyInput = forwardRef((props,ref)=>{
        return (
          <input ref={ref} />
        )
      })
    
    
  • 避免重新创建内容
    ...
    //尽管new Options()的结果仅用于初次渲染,但每次渲染都会调用次函数,可能造成浪费
    const ref = useRef(new Options());
    // =>
    const ref = useRef(null);
    if(ref.current === null){
        ref.current = new Options();
    }
    //ref.current通常,不允许在渲染期间写入或读取。但是,在这种情况下没问题,因为结果始终相同,并且条件仅在初始化期间执行,因此它是完全可预测的。
    

缓存 Hook

React中,父组件的state改变会引起页面的重新渲染,不管子组件有没有更新,也会递归的重新渲染所有的子组件,导致不必要的浪费,这时可以使用useMemo缓存子组件需要费时的计算结果,或者使用useCallback缓存一些必要的函数,优化性能。

useMemo

  1. 定义:useMemo是React Hook中的一个功能,允许在重新渲染之间缓存结果,只有在依赖项改变的时候重新计算结果
  2. 使用:

const cachedValue = useMemo(calculateValue,dependencies)

import { useRef, useState, useEffect,useMemo } from "react";

export default function ChildCpn() {
    const [optionArray, setOptionArray] = useState([
    { "label": "功能异常", "value": "1" }, 
    { "label": "产品建议", "value": "2" }, 
    { "label": "陪审内容不感兴趣", "value": "3" }, 
    { "label": "不合理信息举报", "value": "4" }, 
    { "label": "其他", "value": "5" }
    ]);
    
    // 当组件重新渲染的时候,useMemo会根据optionArray是否变化选择是否重新计算
    const memoHtml = useMemo(() =>{
        console.log('memo test');
        return (
        <div>
            {optionArray.map((item) => {
                return (
                    <div key={item.value}>
                        {item.label}
                    </div>
                );
            })}
        </div>
        )
    },[optionArray])
    return (
        <>
            <title>我是test组件</title>
            {memoHtml}
        </>
    )
}

React.memo

  1. 定义:当props不变时,避免组件的重新渲染(props做的浅比较[Object.is])
  2. 使用:

const SomeComponent = memo(function SomeComponent(props) {

useCallback

  1. 定义:useCallback是React Hook中的一个功能,它允许您在重新渲染之间缓存函数定义。
  2. 使用:

const cachedFn = useCallback(fn, dependencies)

综合案例

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

const MyComponent = React.memo(({ name, age}) => {

  // 只有当name变化时,该函数才会被重新定义/缓存
  const handleClick = useCallback(()=>{
    console.log(`点击了${name}`);
  },[name])
  // 只有age变化,该缓存结果才会重新计算
  const doubleAge = useMemo(() => {
    console.log('计算年龄');
    return age * 2;
  }, [age]);

  console.log('重新渲染 MyComponent');

  return (
    <div>
      <p>姓名:{name}</p>
      <p>年龄:{age}</p>
      <p>双倍年龄:{doubleAge}</p>
      <button onClick={handleClick}>测试useCallback,打印name</button>
    </div>
  );
});

export default function App() {
  const [name, setName] = useState('张三');
  const [age, setAge] = useState(18);
  const [count, setCount] = useState(0)

  console.log('重新渲染 App');

  return (
    <div>
      <MyComponent name={name} age={age} />
      <button onClick={() => setName('李四'+Math.random())}>修改姓名</button>
      <button onClick={() => setAge(age + 1)}>增加年龄</button>
      <hr></hr>
      {/* 由于子组件使用了React.memo,因此count的变化不会引起子组件的重新渲染 */}
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>计数器+1,触发App组件重新渲染</button>
    </div>
  );
}

缓存的弊端如下:

  1. 过度优化
  2. 可读性降低
  3. 滥用导致性能问题:本身这些 Hooks 也会存在一些性能消耗

useEffect

  1. 定义:组件与外部系统同步,eg:setInterval()、clearInterval()、事件监听
  2. 使用:

useEffect(setup,dependencies?)

  • 参数:
    • setup:当你的组件被添加到 DOM 时,React 将运行setup函数。在每次使用更改的依赖项重新渲染后,React 将首先使用旧值运行清理函数(如果你提供了它),然后使用新值运行你的设置函数。在你的组件从 DOM 中移除后,React 将运行你的清理函数。可以选择返回一个清理函数(当依赖为空时,该清理函数类似生命周期的unmount)。
    • dependencies:依赖项 模拟生命周期,useMount和useUnMount
  1. useMount
import { useEffect } from 'react';

const useMount = (fn) => {
    useEffect(() => {
        fn();
    }, []);
};

export default useMount;
  1. useUnMount
import { useEffect,useRef } from 'react';

const useUnMount = (fn) => {
    // 使用useRef保持fn为最新函数
    const ref = useRef(fn);
    ref.current = fn;
    useEffect(() => {
        return () => {
            ref.current();
        }
    }, [])
};

export default useUnMount;

useContext

  1. 定义:从组件中读取和订阅上下文
  2. 使用:

const value = useContext(SomeContext)

  1. 用法
    • 将数据深入传递到树中
    • 更新通过上下文传递的数据
//demo
const CurrentUserContext = createContext(null);
export default function App(){
    // 与state结合起来,更新通过上下文传递的数据
    const [currentUser,setCurrentUser] = useState(null);
    return (
        <CurrentUserContext.Provider
            value={{
                currentUser,
                setCurrentUser
             }}
        >
            <Form />
        </CurrentUserContext.Provider>
    )
}
function Form({ children }) {
  return (
    <Panel title="Welcome">
      <LoginButton />
    </Panel>
  );
}
function LoginButton() {
  // 数据深入传递到树中(后代组件中)
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (currentUser !== null) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'Advika' })
    }}>Log in as Advika</Button>
  );
}
function Panel({ title, children }) {
  return (
    <section className="panel">
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
}

严格模式下组件首次渲染两次

React 在严格模式(StrictMode)下将组件首次渲染两次的原因是为了帮助开发者发现潜在的问题。这种做法可以暴露出一些可能在单次渲染中不易察觉的问题,例如副作用、不纯的函数等。

以下是使用严格模式可能带来的好处:

  1. 检测副作用:在开发模式下,严格模式会在首次渲染和更新时分别执行两次渲染。这有助于发现组件内可能存在的意外副作用。

  2. 检测不纯的函数:如果组件的渲染结果依赖于外部状态或不纯的函数,那么在严格模式下进行两次渲染可能会导致不一致的结果。这有助于开发者发现并修复这些问题。

  3. 强制使用最佳实践:严格模式会警告开发者关于过时或不安全的 API 的使用。这有助于确保开发者遵循最佳实践,编写更健壮、更易维护的代码。

需要注意的是,严格模式下的双重渲染仅在开发环境中进行,生产环境中不会有额外的性能开销。因此,在开发过程中使用严格模式可以帮助开发者更早地发现和修复潜在问题,提高代码质量。

// 因为 React 两次调用你的更新函数,你会看到 todo 被添加了两次,所以你会知道有一个错误。
setTodos(prevTodos => {  
    // 🚩 Mistake: mutating state  
    prevTodos.push(createTodo());  
});

setTodos(prevTodos => {  
    // ✅ Correct: replacing with new state  
    return [...prevTodos, createTodo()];  
});

React如何处理组件结构以及保存或者重置state

  • 只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 特别注意的是在UI树的位置而不是在JSX中的位置
  • 相同位置的不同组件会使 state 重置
    Q:以下是为什么你不应该把组件函数的定义嵌套起来的原因。
    A:这是因为每次 MyComponent 渲染时都会创建一个 不同内置组件 函数。你在相同位置渲染的是 不同 的组件,所以 React 将其下所有的 state 都重置了。这样会导致 bug 以及性能问题。为了避免这个问题, 永远要将组件定义在最上层并且不要把它们的定义嵌套起来。
  • 相同位置重置相同组件的state
    • 将组件渲染在不同的位置
    • 使用 key 标识每个组件的的身份