react 基础

189 阅读13分钟

一、react 基础API

1. useEffect

  1. 浏览器重新绘制之后触发
  2. 参数(setup,dependencies?)
  3. 异步执行
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

2. useLayoutEffect

  1. 浏览器重新绘制之前触发
  2. 参数(setup,dependencies?)
  3. 同步执行,会阻塞DOM渲染
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

3. useReducer

使用场景:

  1. 组件数据状态逻辑复杂时
  2. 数据状态值包含多个,多属性嵌套、多属性关联时
  3. 数据状态的前后依赖性强时
  4. 数据状态的操作分散在多个处理函数时

优点:

  1. 可读性强,结构清晰
  2. 纯函数,状态独立,状态集中管理/操作
  3. dispatch 稳定性强,性能好,无需额外的缓存处理
  4. 同步输入输出,无newState的副作用

示例:

const [state, dispatch] = useReducer(reducerFn, initiaValue, inituaValueFn?)

reducerFn:处理函数,参数:state, action,返回值:新的state
initiaValue:state默认值
inituaValueFn:默认值函数,用于初始化state,优先执行,非必填


const reducerfn = (state, action) => {
	if (action==='inc') {
		return state++
	}
	if (action==='dec') {
		return state--
	}
}
const [state, dispatch] = useReducer(reducerfn, -1, ()=> {
	// 高于默认值的优先级
	return 0
})

<Button onClick={() => dispatch('inc')}>增加</Button>
<Button onClick={() => dispatch('dec')}>减少</Button>

4. useSyncExternalStore

useSyncExternalStore 用于从外部存储(例如状态管理库、浏览器 API等)获取状态并在组件中同步显示。跟踪外部状态。

useSyncExternalStore 使用场景:

  1. 订阅外部 store 例如(redux,mobx,Zustand,jotai)
  2. 订阅浏览器AP| 例如(online,storage,location, history hash)等

抽离逻辑,编写自定义hooks

const res = useSyncExternalStore(subscribe, getSnapshot, getSeeverSnapShot?)
sybscribe:订阅数据源的变化,接受一个回调函数在数据源更新时调用此函数
getSnapShot:获取当前数据源的快照(当前信息)
getSeeverSnapShot:服务端使用

// hooks/useStorage.tsx
import { useSyncExternalStore } from 'react'
export const useStorage = (key: string, initiaValue: number) => {
	const subscribe = (callback: () => void) => {
		window.addEventListener('storage', callback)
		return () => {
			window.removeEventListener('storage', callback)
		}
	}
	const getSnapShot = () => {
		return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : initiaValue
	}
	const data = useSyncExternalStore(subscribe, getSnapShot)
	const setData = (value: any) => {
		localStorage.setItem(key, JSON.stringify(value))
		// 手动触发eventStorage事件,通知订阅
		window.dispatchEvent(new Event('storage'))
	}
	return [data, setData]
}

// views/page.tsx
import { useStorage } from "./hooks/useStorage";
function App() {
  const [token, setToken] = useStorage('token', 0)
  return (
    <>
      <h1>hooks useStorage</h1>
      <div>
        <p> token is {token} </p>
        <button onClick={() => setToken(token + 1)}> token ++ </button>
        <button onClick={() => setToken(token - 1)}> token -- </button>
      </div>
    </>
  )
}
export default App

5. useTransition

useTransition 用于优化用户界面响应性的 Hook,它允许你在不阻塞用户交互的前提下,优雅地处理状态更新带来的加载状态(如“挂起”或“过渡”)

useTransition 作用:

  • 状态更新通常是同步的,调用 setState 会立即重新渲染组件树,可能会导致页面卡顿,特别是在处理复杂计算或大量数据更新时。useTransition 将某些状态更新标记为“非紧急”(过渡态),从而优先处理更紧急的 UI 更新(比如输入框的响应) 。

useTransition 使用场景:

  1. 数据加载时的“pending”状态展示
  2. 页面切换时的平滑过渡
  3. 大型表单提交等场景

6. useDeferredValue

useDeferredValue 用于优化用户界面响应性的 Hook,它允许你在不阻塞渲染的前提下,延迟更新某些非紧急的状态值,从而保持 UI 的流畅性。

useDeferredValue 作用:

  • 延迟使用新的值进行渲染,优先显示旧值,直到浏览器空闲时再更新为新值。

useDeferredValue 使用场景:

  1. 搜索输入框的自动补全

  2. 列表滚动时的平滑更新

  3. 大量数据展示时的渐进式渲染

  4. useDeferred 对比 useTransition

特性useDeferredValueuseTransition
控制对象值(value)状态更新(state update)
是否需要包装副作用
是否有 isPending 状态
适用场景延迟更新某个值(如列表、文本)控制状态更新优先级,显示 loading 状态

8. useImperativeHandle & forwardRef

用于自定义暴露子组件方法或属性给父组件调用的 Hook,它通常与 forwardRef 一起使用。

import { useImperativeHandle, forwardRef, useRef } from "react";

// 子组件,必须配合 forwardRef
const Child_ = forwardRef((props, ref)=>{
  const Child_reff = useRef(null)
  
  const focusInput = () => { 
    Child_reff.current!.focus()
  }
  const selectInput = () => {
    Child_reff.current!.select()
  }
  const BlurInput = () => {
    Child_reff.current!.blur()
  }
  // 暴露方法给父组件
  useImperativeHandle(
    ref, 
    () => ({
      focus: focusInput,
      select: selectInput,
      blur: BlurInput,
    }), 
    []
  )
  
  return <input ref={Child_reff} type="text" />
})

// 父组件
function Parent_() {
  const inputRef = useRef(null);
  const handleFocus = () => {
    inputRef.current!.focus(); // 调用子组件方法
  };
  const handleSelect = () => {
    inputRef.current!.select(); // 调用子组件方法
  };
  const handleBlur = () => {
    inputRef.current!.blur(); // 调用子组件方法
  };
  return (
    <>
        <div>
          <h1>hooks useImperativeHandle forwardRef</h1>
          <p> useImperativeHandle  </p>
          <br />
          <Child_ ref={inputRef} />
          <button onClick={handleFocus}>输入框聚焦</button>
          <button onClick={handleSelect}>选中输入框文本</button>
          <button onClick={handleBlur}>输入框失焦</button>
      </div>
    </>
  )
}

export default Parent_

9. useRef

  • 使用场景:

    1. 访问 DOM 元素,获取对真实 DOM 的引用,进行操作(如聚焦、动画等)
    2. 定时器
  • 特点:

    1. 值改变不会触发重新渲染 re-rander
    2. 保持引用不变,保存状态或变量,这些值不会因组件更新而重置
    3. 避免闭包中捕获旧的 props/state 值的问题

10. useContext

useContext 是一个 Hook(钩子)函数,它允许你在 函数组件 中访问和使用 React 的 Context(上下文)

  • 优点:

    1. 解决数据多层组件嵌套的繁琐问题
    2. 全局状态的管理更直观便捷
  • 缺点:

    1. 即便引用的数据未变化,消费的子组件也会引起重复渲染

      解决方式:

      1. 子组件使用memo包装
      2. 把context要传递值用useMemo包装,传递的函数用useCallback包装
    2. 不适用于高频的数据状态更新(也是性能问题)
    3. 状态跟踪不方便不直观,不利于复用(尽可能拆分context范围)
  • 示例 useContext + useReducer

    //../TestContextProvider.tsx
    import { FC, ReactNode, createContext, useReducer } from 'react'
    const initState = { name: 'rubin', age: 28 }
    const reducer = (state: any, action: any) => {
      switch (action.type) {
        case 'setName':
          return { ...state, name: action?.payload }
        case 'setAge':
          return { ...state, age: action?.payload }
        case 'clear':
          return { ...state, name: '', age: '' }
        case 'init':
          return initState
        default:
          return state
      }
    }
    export const TestContext = createContext<{ state: any; actions: any }>({ state: {}, actions: {} })
    
    const TestContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
      const [state, dispatch] = useReducer(reducer, initState)
      return (
        <TestContext.Provider
          value={{
            state,
            actions: {
              setName: (payload: string) => dispatch({ type: 'setName', payload: payload }),
              setAge: (payload: number) => dispatch({ type: 'setAge', payload: payload }),
              clear: () => dispatch({ type: 'clear' }),
              init: () => dispatch({ type: 'init' })
            }
          }}
        >
          <>{children}</>
        </TestContext.Provider>
      )
    }
    export default TestContextProvider
    
    //../入口组件.txs
    return (
        <ContextUser.Provider value={{ data: contextData }}>
            <子组件1 />
            <子组件2 >
                <子组件21 />
                <子组件22 />
                <子组件23 />
            </子组件2 >
            <子组件3 />
        </ContextUser.Provider>
    )
    
    //../需要使用的组件.txs
    import { useContext } from 'react'
    import { TestContext } from '../TestContextProvider'
    const Context = () => {
      const { state, actions } = useContext(TestContext)
      return (
        <div className="tc">
          <h1>useContext + useReducer</h1>
          <br />
          <br />
          <h3>TestContext : {JSON.stringify(state)}</h3>
          <br />
          <br />
          <div>
            <button onClick={() => actions.setName('BIG RUBIN')}>setName</button>
            --
            <button onClick={() => actions.setAge(228)}>setAge</button>
            --
            <button onClick={() => actions.clear()}>clear</button>
            --
            <button onClick={() => actions.init()}>init</button>
          </div>
        </div>
      )
    }
    export default Context
    

11. useState

  • 惰性初始化,提供一个初始化函数,而不是直接提供初始值。
    // 初始化数据
    const initData = () => {
        let d = []
        for(i=0, i<100000000, i++) {
            d.push(i)
        }
        return d
    }
    合理写法,惰性初始化,只有首次初始化会渲染,避免不要的计算
    const [data, setData] = useState(() => initData())
    
    错误写法,当有别的数据更新时,会有不必要的重新计算
    const [data, setData] = useState(initData())
    
  • 更新函数
    1. 异步更新,这是传递一个更新函数,而不是一个状态,更新函数每次执行都会拿到最新的值
      多次调用 `setState(count + 1)`,它们都基于**同一个旧的 `count` 值**。
      即使被批处理,最终状态也只会是 `count + 1`const [age, setAge] = useState(20)
      const updateAge1 = () => {
          setAge((age) => age + 1)  // 20 + 1
          setAge((age) => age + 1)  // 21 + 1
          setAge((age) => age + 1)  // 22 + 1
      }
      
    2. 同步更新,react 会合并更新任务,多个同样的状态只执行一次。
      `setState(prevCount => prevCount + 1)`React 会将这些 `updater` 函数收集起来。
      在批处理时,它会按顺序应用这些函数。
      
      const [age, setAge] = useState(20)
      const updateAge1 = () => {
          setAge(age + 1)  // 20 + 1
          setAge(age + 1)  // 20 + 1
          setAge(age + 1)  // 20 + 1
      }
      

12. useCallBack

  • 使用场景:
    1. 子组件传递的函数用useCallback包装
    2. 复杂计算的函数
  • 特点:
    1. 缓存函数引用,避免重复创建函数
    2. 传递函数给子组件,避免子组件不必要的渲染
    3. 避免闭包问题,useContext传递的函数(useCallBack包裹),值(useMemo)

13. HOC

HOC(higher-order-compontent),传入一个组件返回一个新组件,新组件会为原组件添加一些属性。

  • 适用场景:
    1. 在不修改原组件的情况下增强其功能时。
    2. 例如,权限控制、日志记录等。
  • HOC 属性代理
    1. 不改变state props
    2. 传入不同数据做不同的逻辑展示,不破坏原组件逻辑
  • HOC 反向继承
    1. 拦截生命周期
    2. 劫持原组件数据,进行修改state, props
    3. 修改rander渲染
    4. 在原组件前后添加额外的内容

三、react 概念理解

1. 函数组件和类组件的本质区别

  • 类组件
    1. 面向对象编程
    2. 依赖this和生命周期
  • 函数组件
    1. 函数式编程
    2. 摆脱this
    3. 依赖hooks
    4. 代码简洁
  • 函数组件中的闭包陷阱
    1. 正确设置依赖项数组
    2. 使用函数式更新 useState
    3. 使用 useRef

2. Props数据为何不可改变

  1. 单向数据流,易于维护和数据状态追踪,避免数据管理混乱
  2. 父组件改变时Props会返回新的数据源(内存引用地址不同),只需对比内存索引是否一致(性能提升)

3. react错误边界(Error Boundary),仅类组件可用

  • 功能:捕获组件内的JS错误。
  • 目的:
    1. 避免错误导致页面白屏或者渲染崩溃
    2. 展示友好的错误提示,引导/提示用户操作
    3. 隔离错误,防止错误蔓延
    4. 可在此处上报错误日志
  • 实现:错误边界必须是类组件(class component),并且定义以下其中1-2个生命周期方法:
    1. static getDerivedStateFromError() 更新state展示降级UI
    2. componentDidCatch() 记录错误

示例:

`src/components/ErrorBoundary.js`

import React from 'react';
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  // 捕捉错误的核心
  componentDidCatch(error, errorInfo) {
    console.error("Error caught by ErrorBoundary:", error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return <h1>很抱歉,发生了一些错误。</h1>;
    }
    return this.props.children;
  }
}
export default ErrorBoundary;


`src/views/app.js`
import React from 'react';
import ErrorBoundary from './components/ErrorBoundary';

function App() {
  return (
      <ErrorBoundary>
        <BrokenComponent />
      </ErrorBoundary>
  );
}

export default App;

4. react-router 路由模式

  1. HashRouter:使用 URL 的 hash 部分(即 # 后面的内容)来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载。
    • 优点:无需服务端资源配置;支持HTML5之前的不支持History API兼容问题。
    • 缺点:带“#”不美观,不符合RESTful原则
  2. BrowserRouter:利用浏览器的 History API(pushStatereplaceState 等)来进行URL管理,允许创建真实的URL路径而不需要重新加载页面。
    • 优点:不带“#”,清晰美观。
    • 缺点:需要服务器端的支持,以确保当用户直接访问或刷新页面时能够正确地提供相应的HTML文档
  3. MemoryRouter:不在地址栏中显示任何路径信息,所有的路由变化都保存在内存中。
    • 优点:适用于某些非浏览器环境下的测试或渲染场景,如单元测试或者在Node.js环境中运行的应用程序
    • 缺点:用户无法通过地址栏直接访问某个状态。

5. BrowserRouter 实现原理

BrowserRouter 依赖于 HTML5 提供的 History API 事件来动态地更新浏览器地址栏中的 URL 而不触发页面刷新。

  • 实现流程:
    1. 依赖 HTML5 提供的 History API 监听 url 变化,如: history.pushState history.replaceState window.onpopstate
    2. 创建一个上下文,其中包含当前的路由信息(如路径名、搜索参数等),并监听 URL 变化。
    3. 拿到当前的路由信息后,匹配路由并加载渲染对应的组件。
    4. BrowserRouter 维护了一个内部的历史堆栈,用于追踪用户的浏览历史。每当一个新的路由被激活时,相关信息会被推入这个堆栈中。允许用户使用浏览器的“前进”和“后退”按钮在不同的视图之间导航,同时保持应用状态的一致性。

6. react中的状态管理

  1. useContext + reducer

    • 适用于:中小型项目,状态数据有限的项目(主题切换,token身份等)
    • 优点:原生React语法,学习成本低
    • 缺点:性能浪费,消费的子组件不管依赖值是否变化都会重复渲染
  2. redux / redux toolkit

    • 适用于:大型项目,需要有严格的数据流管理
    • 优点:强大的调试工具,成熟的技术生态,丰富的中间件,RTK提供简化的代码模板
    • 缺点:虽然有RTK,代码仍有些繁重
  3. Zustand

    • 适用于:中、大型项目,快速,小巧
    • 优点:
      • 轻量级(包体积小)
      • 灵活性极简API,无代码模板,不需要actions reducers dispatchers
      • 高性能,高频优化(短暂更新),细分订阅状态切片,优化渲染
      • 可以脱离react 上下文使用,可在js文件中使用
    • 保持数据不变性,使用 import { immer } from 'zustand/middleware/immer'
    • 数据持久化,使用 import { persist } from 'zustand/middleware'
    // zustandStore.js
    import { create } form 'zustand'
    import { persist } from 'zustand/middleware'
    import { immer } from 'zustand/middleware/immer'
    // 111111111111111
    export const useUserStore = create<State & Action>()(
      // 保持数据不变性 immer
      immer(
        // 数据持久化 persist
        persist(
          (set, get) => ({
            ...initialState,
            settoken: (token: string) => {
              set((state) => ({ ...state, token }))
            },
            incnum: () => {
              set((state) => ({ ...state, num: get().num + 1 }))
            },
            decnum: () => {
              set((state) => ({ ...state, num: get().num - 1 }))
            },
            syncData: async () => {
              set((state) => ({ ...state, isPending: true }))
              const d = await new Promise((resolve) => {
                setTimeout(() => {
                  resolve({ title: Math.fround(Math.random() * 100).toFixed(2) })
                }, 2000)
              })
              set((state) => ({ ...state, isPending: false, data: [...state.data, d] }))
            }
          }),
          {
            name: 'zustand-storage' // persist 持久化,存储名称
          }
        )
      )
    )
    // 222222222222222222222222
    const useStore = create((set) => {
        count:0,
        increment: () => set({count: state.count+1}),
        msg:'nihao',
        setMsg: (newMsg) => set({msg: newMsg})
    })
    export default useStore
    
    // 使用
    impprt useStore form './zustandStore'
    const App = ()=> {
        const count = useStore((count)=> state.count)
        const msg = useStore((msg)=> state.msg)
        const increment = useStore((increment)=> state.increment)
        const setMag = useStore((setMag)=> state.setMag)
        return (
            <div>
                <div>count:{count}</div>
                <div>msg:{msg}</div>
                <button onclick={increment}>increment</button>
                <button onclick={()=> setMag('22222222')}>setMag</button>
            </div>
        )
    }
    
  4. Jotai: 原子化

    • 优点:
      • 更轻量、更灵活,并且与 React 的函数组件和 Hook 高度契合
      • 基于原子(Atoms),状态被定义为独立的“原子”,多个组件可以独立读写。
      • 与 React Hook 集成,使用 useAtom Hook 来读取和更新状态。
      • 可以组合多个原子来构建更复杂的状态逻辑
      • 体积小(<1KB),无依赖
      • 支持异步设置原子初始状态
    • 数据持久化,使用 import { atomWithStorage } from 'jotai/utils'
    // storejotai.js
    export const Acount = atom<number>(0)
    const Abasecount = atom<string>('10086:::')
    export const Acombinecount = atom<string>((get) => get(Abasecount) + get(Acount))
    export const APending = atom<boolean>(false)
    export const Acostom = atom<string>(
      (get) => 'Acostom:::' + (get(Acount) + 1000),
      (get, set) => {
        set(Acount, get(Acount) + 2)
      }
    )
    export const Aobj = atom<State | null>(null, (get, set, action, data) => {
      switch (action) {
        case 'name':
          set(Aobj, { ...get(Aobj), name: data })
          break
        case 'age':
          set(Aobj, { ...get(Aobj), age: data })
          break
      }
    })
    export default counterAtom
    
    // 使用
    import { useAtom } from 'jotai';
    impprt counterAtom form './storejotai'
    const App = ()=> {
        const [count, setCount] = useAtom(counterAtom)
        return (
            <div>
              <p>Count: {count}</p>
              <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
              <button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
            </div>
        )
    }
    

7. Immer.js 在数据管理中的意义

核心特点:

  1. immer 是一个不可变数据的可变更新库。
  2. 自动生成不可变的更新结果,使用“可变”的方式来更新状态,内部生成一个不可变的新对象。

使用场景:

  1. 在不可变更新的场景中,处理嵌套对象或复杂状态结构。 
  2. Immer 可与redux结合使用,zustand/jotai 也有类似immer的实现逻辑。

性能:

  • Immer 使用 ProxyES5 的克隆方式,对性能有一定影响。

8. React 性能观察工具

  1. React DevTools Profiler
    • 作用:
      1. 可视化组件渲染耗时
      2. 识别不必要的组件重渲染
      3. 分析commit阶段性能。
    • 功能:
      1. 火焰图显示组件层级及各组件渲染时间
      2. 按渲染耗时组件排序
      3. 查看组件再多次提交中的渲染情况
  2. chrome DevTools Performance
    • 作用:
      1. 分析整体应用性能:js执行,渲染,网络等。
      2. 识别JS长任务。
      3. 分析布局抖动和高频绘制风暴
    • 功能:
      1. Timings 标记组件生命周期和更新。
      2. JS调用栈、事件处理、渲染过程。

9. React 性能优化

  1. 常见性能瓶颈

    1. 不必要的组件重复渲染 re-rander
    2. 庞大的组件树嵌套
    3. 大量的数据处理和渲染
    4. 首屏加载时间过长
  2. 优化方式

    1. useMemo 缓存值,根据依赖项更新
    2. useCallback 缓存函数,根据依赖项更新
    3. memo 缓存组件,组件浅比较更新渲染
    4. lazy&Suspense 组件懒加载,动态导入组件会抛出Promise查找最近的<Suspense>边界,该边界处显示其 fallback 属性定义的内容
    5. 虚拟化列表,仅渲染可视区域内的DOM,减少性能开销