Vanson's Eternal Blog

React夯实基础

React basic.png
Published on
/118 mins read/---

React生命周期

挂载阶段

  • componentWillMount:在渲染之前执行,用于根组件中的 App 级配置。
  • componentDidMount:在第一次渲染之后执行,可以在这里做 AJAX 请求、DOM 的操作、状态更新以及设置事件监听器。

更新阶段

  • componentWillReceiveProps:在组件接受到新的状态(Props)时被触发,一般用于业务组件状态更新时子组件的重新渲染。
  • shouldComponentUpdate:确定是否更新组件。默认情况下,它返回 true。如果确定在 state 或 props 更新后组件不需要重新渲染,则可以返回 false,这是一个提高性能的方法。
  • componentWillUpdate:在 shouldComponentUpdate 返回 true 确定要更新组件之前执行。
  • componentDidUpdate:主要用于更新 DOM 以响应 props 或 state 更改。

卸载阶段

  • componentWillUnmount:用于取消任何的网络请求,或删除与组件关联的所有事件监听器。

React16后删除Will生命周期

生命周期方法的误解和误用

这些方法经常被开发者误解,导致它们被用于不适合的场景,例如在 componentWillReceiveProps 中处理状态更新逻辑,容易引发难以追踪的错误。

异步渲染机制的引入

React 16 引入了异步渲染机制(如 Fiber 架构),允许组件的渲染过程被中断、暂停甚至回溯。这导致 Will 相关生命周期方法可能被多次调用,引发以下问题:

  • 重复的事件监听:可能导致内存泄漏。
  • 重复的网络请求:浪费资源。
  • 状态更新的不确定性:调用顺序变得不可预测。

替代方案的引入

React 提供了新的生命周期方法和替代方案。

  • componentDidMount:推荐将网络请求和事件绑定移到此处。
  • getDerivedStateFromProps:替代 componentWillReceiveProps,用于处理 props 更新逻辑。
  • getSnapshotBeforeUpdate:替代 componentWillUpdate,在 DOM 更新前捕获快照,并将结果传递给 componentDidUpdate。

AJAX放在哪个生命周期

类组件

  • componentDidMount:这是最常见的选择,因为组件已经挂载到 DOM 上,可以进行数据获取操作,并且不会引起额外的渲染。
  • componentDidUpdate:如果需要在组件更新后重新获取数据,可以使用这个方法。但要注意避免无限循环,通常需要添加条件判断。
  • componentWillUnmount:如果 AJAX 请求是异步的,可以在组件卸载前取消请求,以避免内存泄漏。

Hooks

  • useEffect:在 React Hooks 中,useEffect 是处理副作用(如 AJAX 请求)的合适选择。默认情况下,useEffect 在组件渲染后执行,类似于 componentDidMount。
  • 依赖数组:可以通过指定依赖数组来控制 useEffect 的执行时机。如果依赖数组为空,useEffect 只会在组件挂载和卸载时执行,类似于 componentDidMount 和 componentWillUnmount。
  • 清理函数:如果 AJAX 请求是异步的,可以在 useEffect 的返回值中添加清理函数,用于取消请求,避免内存泄漏。
// 类组件
class MyComponent extends React.Component {
  componentDidMount() {
    this.fetchData();
  }
 
  componentDidUpdate(prevProps, prevState) {
    if (this.props.id !== prevProps.id) {
      this.fetchData();
    }
  }
 
  componentWillUnmount() {
    // 取消请求
  }
 
  fetchData() {
    // AJAX 请求逻辑
  }
 
  render() {
    // 渲染逻辑
  }
}
 
// Hooks
function MyComponent({ id }) {
  const [data, setData] = useState(null);
 
  useEffect(() => {
    let isMounted = true;
 
    const fetchData = async () => {
      try {
        const response = await fetch(`/api/data/${id}`);
        if (isMounted) {
          const result = await response.json();
          setData(result);
        }
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };
 
    fetchData();
 
    return () => {
      isMounted = false;
      // 取消请求
    };
  }, [id]);
 
  return (
    // 渲染逻辑
  );
}
 

Virtual DOM

定义

Virtual DOM 是一种以 JavaScript 对象形式存在的对 DOM 的描述。它创建的目的是为了更高效地将虚拟节点渲染到页面视图中。虚拟 DOM 对象的节点与真实 DOM 的属性一一对应。

Virtual DOM 是一种 JavaScript 对象,用于在内存中表示真实的 DOM 结构。它并不是实际的 DOM 节点,而是一个轻量级的副本。

更新效率

Virtual DOM 的更新速度比直接操作真实 DOM 快得多,因为它避免了频繁的 DOM 操作。

操作限制

Virtual DOM 本身无法直接更新 HTML,它需要通过与真实 DOM 的对比(diff 算法)来决定如何高效地更新页面。

更新机制

当元素更新时,Virtual DOM 会通过 JSX 重新渲染组件,并与之前的虚拟 DOM 进行对比,只更新有变化的部分。

操作简单性

Virtual DOM 操作相对简单,开发者可以通过声明式编程来描述 UI,而不需要直接操作 DOM。

内存效率

Virtual DOM 通常占用较少的内存,因为它只是真实 DOM 的一种轻量级表示。

工作过程

  • 重新渲染 UI:每当底层数据发生改变时,整个 UI 都会在 Virtual DOM 中重新渲染。这一步骤确保了 Virtual DOM 始终反映最新的应用状态。
  • 计算差异:通过 diff 算法,比较当前的 Virtual DOM 和上一个版本的 Virtual DOM 之间的差异。这一步骤确定了哪些部分需要更新到真实 DOM。
  • 更新真实 DOM:根据计算出的差异,只更新真实 DOM 中实际更改的内容。这一步骤确保了用户看到的 UI 是最新的,同时避免了不必要的 DOM 操作,从而提高了性能。

React路由

React 路由是‌用于构建单页应用(SPA)的第三方路由库‌,基于 React 的组件化思想设计。

它通过动态管理 URL 与页面组件的映射关系,实现‌无刷新页面切换‌,保持 UI 与 URL 同步。

核心功能

  • 动态路由匹配‌:根据当前 URL 自动渲染对应的组件(如 /users 显示用户列表页)。
  • ‌无刷新导航‌:使用 Link 组件或编程式导航(如 useNavigate)跳转页面,不重新加载浏览器。
  • ‌嵌套路由‌:支持子路由配置,实现复杂页面布局(如仪表盘的侧边栏和内容区独立管理)。
  • ‌路由参数与查询‌:通过 URL 传递参数(如 /user/:id)或查询字符串(如 ?page=2),动态加载数据。

核心组件与 API

  • BrowserRouter‌:基于 HTML5 History API 的路由容器,监听 URL 变化。
  • ‌Route:定义 URL 路径与组件的映射规则。
  • ‌Link:替代 <a> 标签实现导航跳转(避免页面刷新)。
  • ‌useNavigate‌:Hook 函数,支持编程式导航(如跳转、前进、后退)。
  • ‌useParams‌:获取 URL 中的动态参数。
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
 
function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/user/:id" element={<UserProfile />} />
      </Routes>
    </BrowserRouter>
  );
}
 
 

结合React Router实现路由守卫

可以通过结合 React Router 和自定义逻辑来实现路由守卫(路由拦截)。

路由守卫可以用来控制用户访问某些页面的权限,例如只有登录用户才能访问某些路由。

实现思路

  • 自定义路由组件:创建一个自定义的 PrivateRoute 组件,用于包裹需要保护的路由。
  • 权限判断:在 PrivateRoute 中,通过判断用户的登录状态或权限来决定是否允许访问目标页面。
  • 重定向:如果用户没有权限访问目标页面,重定向到登录页面或其他指定页面。
import React, { useState, useEffect, useCallback } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom'
 
// 步骤1:创建身份验证上下文
const AuthContext = React.createContext(null)
 
// 步骤2:创建自定义 Hook 管理认证状态
const useAuth = () => {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
 
  // 使用 useCallback 缓存函数避免重复创建
  const login = useCallback(async (credentials) => {
    try {
      // 模拟异步登录请求
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(credentials),
      })
      const userData = await response.json()
      setUser(userData)
    } catch (error) {
      setUser(null)
      throw error
    }
  }, [])
 
  const logout = useCallback(() => {
    // 同步清理操作
    setUser(null)
    localStorage.removeItem('token')
  }, [])
 
  // 在挂载时检查认证状态(演示 useEffect 异步操作)
  useEffect(() => {
    let isMounted = true // 防止组件卸载后设置状态
    const abortController = new AbortController()
 
    const checkAuth = async () => {
      try {
        // 模拟 token 验证请求
        const response = await fetch('/api/check-auth', {
          signal: abortController.signal,
        })
        const data = await response.json()
 
        if (isMounted) {
          setUser(data.user)
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Auth check failed:', error)
        }
      } finally {
        if (isMounted) {
          setLoading(false)
        }
      }
    }
 
    checkAuth()
 
    // 清理函数:取消未完成的请求
    return () => {
      isMounted = false
      abortController.abort()
    }
  }, []) // 空依赖数组表示只运行一次
 
  return { user, loading, login, logout }
}
 
// 步骤3:路由守卫组件
const PrivateRoute = ({ children, requiredRoles }) => {
  const location = useLocation()
  const auth = React.useContext(AuthContext)
 
  // 处理加载状态
  if (auth.loading) {
    return <div>Loading authentication...</div>
  }
 
  // 条件判断逻辑
  if (!auth.user) {
    // 保存来源地址以便登录后跳转
    return <Navigate to="/login" state={{ from: location }} replace />
  }
 
  if (requiredRoles && !requiredRoles.includes(auth.user.role)) {
    return <Navigate to="/unauthorized" replace />
  }
 
  return children
}
 
// 步骤4:页面组件
const LoginPage = () => {
  const auth = React.useContext(AuthContext)
  const [isLoggingIn, setIsLoggingIn] = useState(false)
  const location = useLocation()
 
  const handleSubmit = async (e) => {
    e.preventDefault()
    setIsLoggingIn(true)
 
    try {
      await auth.login({
        /* 登录凭据 */
      })
      // 登录成功后跳转到原地址或首页
      const from = location.state?.from?.pathname || '/'
      return <Navigate to={from} replace />
    } finally {
      setIsLoggingIn(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      {/* 登录表单内容 */}
      <button disabled={isLoggingIn}>{isLoggingIn ? 'Logging in...' : 'Login'}</button>
    </form>
  )
}
 
const DashboardPage = () => <h1>Dashboard</h1>
const AdminPage = () => <h1>Admin Panel</h1>
 
// 步骤5:应用主组件
const App = () => {
  const auth = useAuth()
 
  return (
    <AuthContext.Provider value={auth}>
      <Router>
        <Routes>
          {/* 公开路由 */}
          <Route path="/login" element={<LoginPage />} />
 
          {/* 需要认证的路由 */}
          <Route
            path="/dashboard"
            element={
              <PrivateRoute>
                <DashboardPage />
              </PrivateRoute>
            }
          />
 
          {/* 需要管理员权限的路由 */}
          <Route
            path="/admin"
            element={
              <PrivateRoute requiredRoles={['admin']}>
                <AdminPage />
              </PrivateRoute>
            }
          />
 
          <Route path="*" element={<Navigate to="/dashboard" replace />} />
        </Routes>
      </Router>
    </AuthContext.Provider>
  )
}
 
export default App

受控组件和非受控组件

受控组件

  • 定义:受控组件是 React 控制的组件,表单元素(如 <input><textarea><select>)的值由 React 组件的状态管理。
  • 数据存储:表单元素的值存储在组件的状态(state)中,而不是直接存储在 DOM 中。
  • 更新方式:每当用户输入时,会触发一个事件处理函数(如 onChange),该函数会更新组件的状态,从而重新渲染组件并更新表单元素的值。
  • 优点:适合需要频繁验证或转换用户输入的场景,数据始终与组件状态同步。
import React, { useState } from 'react'
 
function ControlledComponent() {
  const [value, setValue] = useState('')
 
  const handleChange = (e) => {
    setValue(e.target.value)
  }
 
  return <input type="text" value={value} onChange={handleChange} />
}

非受控组件

  • 定义:非受控组件是表单数据由 DOM 处理,而不是由 React 组件管理的组件。
  • 数据存储:表单元素的值存储在 DOM 中,而不是组件的状态中。
  • 更新方式:通常使用 Refs 来获取表单元素的当前值。
  • 优点:适合简单的表单场景,减少了组件状态的管理开销。
import React, { useRef } from 'react'
 
function UncontrolledComponent() {
  const inputRef = useRef(null)
 
  const handleSubmit = (e) => {
    e.preventDefault()
    const value = inputRef.current.value
    console.log(value)
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} />
      <button type="submit">Submit</button>
    </form>
  )
}

state 和 props

相同点

  • 都是 JavaScript 对象:state 和 props 都是 JavaScript 对象,用于存储信息。
  • 用于保存信息:两者都用于保存组件所需的数据。
  • 触发渲染更新:当 state 或 props 发生变化时,组件会重新渲染。
对比点propsstate
定义从父组件传递给子组件的数据,是组件的外部属性。组件内部的状态,由组件自身管理和更改。
管理方式由父组件传递,子组件不能直接修改。由组件自身管理,通常在组件的构造函数中初始化。
可变性不可变,子组件不能直接修改接收到的 props可变,组件可以通过 setState(类组件)或 useState Hook(函数式组件)更新状态。
初始化在父组件中通过 props 传递给子组件。在组件的构造函数中初始化(类组件)或通过 useState Hook 初始化(函数式组件)。
用途用于从父组件向子组件传递数据。用于管理组件内部的状态,如表单输入、本地组件数据等。
触发渲染props 发生变化时,组件会重新渲染。state 发生变化时,组件会重新渲染。

组件通信

父组件到子组件的通信

1、 Props: 是父组件传递给子组件的数据,用于配置子组件的行为或外观。最常见的通信方式,适用于从父组件向子组件传递数据。

// 父组件
function ParentComponent() {
  const message = 'Hello from Parent'
  return <ChildComponent message={message} />
}
 
// 子组件
function ChildComponent(props) {
  return <div>{props.message}</div>
}

2、Ref:用于直接访问 DOM 元素或在组件树中引用特定的子组件。使用场景:适用于需要直接操作子组件的 DOM 或方法。

// 父组件
class ParentComponent extends React.Component {
  componentDidMount() {
    this.childRef.focus()
  }
 
  render() {
    return <ChildComponent ref={(ref) => (this.childRef = ref)} />
  }
}
 
// 子组件
class ChildComponent extends React.Component {
  focus() {
    // 自定义方法
  }
}

子组件到父组件的通信

1、回调函数

父组件通过 props 将回调函数传递给子组件,子组件通过调用这些回调函数将数据传递回父组件。使用场景:适用于子组件需要通知父组件某些事件或数据变化。

// 父组件
function ParentComponent() {
  const handleUpdate = (data) => {
    console.log(data)
  }
 
  return <ChildComponent onUpdate={handleUpdate} />
}
 
// 子组件
function ChildComponent(props) {
  return <button onClick={() => props.onUpdate('Data from Child')}>Update</button>
}

2、事件冒泡机制

利用 React 的合成事件系统,子组件触发的事件会冒泡到父组件。使用场景:适用于需要通过事件传递数据的场景。

// 父组件
function ParentComponent() {
  const handleClick = (e) => {
    console.log('Event bubbled up', e.target.value)
  }
 
  return (
    <div onClick={handleClick}>
      <ChildComponent />
    </div>
  )
}
 
// 子组件
function ChildComponent() {
  return <button value="Child Data">Click Me</button>
}

3、useImperativeHandle 和 forwardRef

定义:通过 useImperativeHandle 和 forwardRef,子组件可以暴露方法给父组件。 使用场景:适用于需要从父组件调用子组件的方法。

// 子组件
function ChildComponent(props, ref) {
  useImperativeHandle(ref, () => ({
    focus: () => {
      // 自定义方法
    },
  }))
 
  return <div>Child Component</div>
}
 
const ForwardedChild = forwardRef(ChildComponent)
 
// 父组件
function ParentComponent() {
  const childRef = useRef()
 
  return (
    <div>
      <ForwardedChild ref={childRef} />
      <button onClick={() => childRef.current.focus()}>Focus Child</button>
    </div>
  )
}

兄弟组件之间的通信

利用父组件通信:通过父组件作为中介,兄弟组件之间通过父组件的状态和回调函数进行通信。

// 父组件
function ParentComponent() {
  const [sharedState, setSharedState] = useState('Initial Data')
 
  return (
    <div>
      <ChildComponentA data={sharedState} onUpdate={setSharedState} />
      <ChildComponentB data={sharedState} />
    </div>
  )
}
 
// 子组件 A
function ChildComponentA(props) {
  return <button onClick={() => props.onUpdate('Updated Data')}>Update</button>
}
 
// 子组件 B
function ChildComponentB(props) {
  return <div>{props.data}</div>
}

不相关组件之间的通信

  • Context:通过 Context API 提供全局状态管理,适用于跨层级的组件通信。使用场景:适用于多个组件需要访问同一状态。
  • 全局变量:通过全局变量或对象存储共享状态。使用场景:适用于简单的状态共享,但不推荐在复杂应用中使用。
  • 观察者模式:通过观察者模式实现组件之间的解耦通信。使用场景:适用于需要解耦组件之间的直接依赖。
  • Redux:使用状态管理库(如 Redux、MobX 或 Dva)进行全局状态管理。使用场景:适用于复杂应用中的全局状态管理。

React 中的 Props

Props 是 React 中属性的简写,用于在组件之间传递数据。它们是只读的,必须保持纯(即不可变),并且总是在整个应用中从父组件传递到子组件。 子组件不能将 prop 送回父组件,这种单向数据流有助于维护组件的稳定性和可预测性。

Props 的特性

单向数据流:数据只能从父组件流向子组件,确保了数据流的可预测性。 不可变性:子组件不能直接修改 props,必须通过回调函数将数据传递回父组件:

function ChildComponent(props) {
  // 错误:不能直接修改 props
  // props.name = "New Name";
 
  // 正确:通过回调函数传递数据
  function handleUpdate() {
    props.onUpdate('New Name')
  }
 
  return <button onClick={handleUpdate}>Update Name</button>
}

父组件:

import React, { useState } from 'react'
import ChildComponent from './ChildComponent'
 
function ParentComponent() {
  const [user, setUser] = useState({
    name: 'John Doe',
    age: 30,
  })
 
  // 回调函数,用于更新用户数据
  const handleUpdateUser = (newUser) => {
    setUser(newUser)
  }
 
  return (
    <div>
      <h1>Parent Component</h1>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <ChildComponent user={user} onUpdateUser={handleUpdateUser} />
    </div>
  )
}
 
export default ParentComponent

子组件:

import React from 'react'
 
function ChildComponent(props) {
  // 子组件不能直接修改 props.user
  // 通过调用父组件传递的回调函数来请求更新
  const handleUpdate = () => {
    const newUser = {
      ...props.user, // 复制原始对象
      age: props.user.age + 1, // 更新年龄
    }
    props.onUpdateUser(newUser) // 调用回调函数,将新数据传递回父组件
  }
 
  return (
    <div>
      <h2>Child Component</h2>
      <p>Name: {props.user.name}</p>
      <p>Age: {props.user.age}</p>
      <button onClick={handleUpdate}>Update Age</button>
    </div>
  )
}
 
export default ChildComponent

Props 的验证

React 提供了 propTypes 来验证 props 的类型,确保组件接收到正确的数据类型:

import PropTypes from 'prop-types'
 
function ChildComponent(props) {
  return <h1>Hello, {props.name}!</h1>
}
 
ChildComponent.propTypes = {
  name: PropTypes.string.isRequired,
}

默认 Props

可以通过 defaultProps 为组件设置默认的 props 值:

function ChildComponent(props) {
  return <h1>Hello, {props.name}!</h1>
}
 
ChildComponent.defaultProps = {
  name: 'Guest',
}

函数式组件和 Hooks写法规范

React Hooks 的写法规范

  • 只在顶层调用 Hooks
    • 规范:不能在循环、条件语句或嵌套函数中调用 Hooks。
    • 原因:React 依赖 Hooks 的调用顺序来正确管理状态和副作用。如果在条件语句或循环中调用 Hooks,可能会导致调用顺序不一致,从而导致状态和副作用的不稳定。
  • 只在 React 函数式组件或自定义 Hooks 中调用 Hooks
    • 规范:不能在普通的 JavaScript 函数中调用 Hooks。
    • 原因:React 需要知道 Hooks 是在组件内部还是在自定义 Hooks 中调用的,以便正确地分配和管理 Hooks。
import React, { useState, useEffect } from 'react';
 
// 错误写法
const MyComponent = ({ condition }) => {
  // 不要在条件语句中调用 hooks
  if (condition) {
    const [state, setState] = useState(0);
  }
 
  return <div>My Component</div>;
};
 
// 正确写法
const MyComponent = ({ condition }) => {
  const [state, setState] = useState(0);
 
  useEffect(() => {
    if (condition) {
      // 根据 condition 执行副作用
    }
  }, [condition]);
 
  return <div>My Component</div>;
};
 

出现这些限制的原因

  • 调用顺序的依赖性:React 通过调用顺序来管理每个 Hook 的状态。每次组件渲染时,React 会根据 Hooks 的调用顺序为每个 Hook 分配状态。如果调用顺序发生变化,React 无法保证状态的一致性。
  • 性能优化和一致性:通过保证 Hooks 的调用顺序和位置一致,React 能够进行性能优化,例如跳过不必要的状态更新和副作用执行。同时,这也确保了状态和副作用的一致性和可预测性。
  • 调试和开发体验:统一的调用规范可以使调试和分析代码更容易。如果所有 Hooks 都在组件顶层调用,开发者可以轻松地跟踪和理解每个 Hook 的作用和状态变化。

自定义 Hooks

自定义 Hooks 也是遵循相同的规范。自定义 Hooks 是封装了多个内置 Hooks 的函数,它们本质上是复用状态逻辑的工具。

import { useState, useEffect } from 'react'
 
const useCustomHook = () => {
  const [state, setState] = useState(0)
 
  useEffect(() => {
    // 处理副作用
  }, [])
 
  return [state, setState]
}

在组件中使用自定义 Hook:

 
import React from 'react';
import useCustomHook from './useCustomHook';
 
const MyComponent = () => {
  const [state, setState] = useCustomHook();
 
  return <div>{state}</div>;
};
 

为什么使用 React Hooks?

  • 简洁性:Hooks 提供了一种更简洁的方式来管理状态和副作用,避免了使用类组件的繁琐。
  • 逻辑复用:通过自定义 Hooks,可以轻松提取和复用跨多个组件的有状态逻辑,无需使用高阶组件或渲染 props。
  • 简化生命周期管理:Hooks 如 useEffect 可以替代多个生命周期方法,使得代码更加直观和易读。
  • 性能优化:通过 useMemo 和 useCallback 等 Hooks,可以优化性能,减少不必要的组件渲染。
  • 更好的类型支持:Hooks 与 TypeScript 集成良好,提供更好的类型检查和代码提示。

react 函数式组件

在一个应用周期里,什么时机会被调用到呢,函数会被调用多少次?

React 函数式组件的调用时机

  • 初次渲染:当组件首次挂载到 DOM 上时,函数式组件会被调用一次。
  • Props 改变:当组件接收到新的 props 时,函数式组件会被重新调用。
  • State 改变:如果组件使用了 useState 钩子,当 state 发生变化时,函数式组件会被重新调用。
  • Context 改变:如果组件使用了 useContext 钩子,当上下文数据发生变化时,函数式组件会被重新调用。
  • 父组件重新渲染:如果父组件重新渲染,即使当前组件的 props 和 state 没有变化,当前组件也会被重新调用。

函数被调用的次数

在一个应用周期里,函数式组件的调用次数取决于上述情况的频率:

  • 初次渲染时调用一次。
  • 每次 props、state、context 改变时都会调用。
  • 父组件每次重新渲染时,子组件也会重新调用。
import React, { useState } from 'react'
 
const ChildComponent = ({ propValue }) => {
  console.log('ChildComponent is called')
  return <div>{propValue}</div>
}
 
const ParentComponent = () => {
  const [stateValue, setStateValue] = useState(0)
 
  return (
    <div>
      <ChildComponent propValue={stateValue} />
      <button onClick={() => setStateValue(stateValue + 1)}>Increment</button>
    </div>
  )
}
 
export default ParentComponent
  • 初次渲染 ParentComponent 时。
  • 每次点击按钮导致 stateValue 改变时(即 setStateValue 被调用时)。

函数式组件和类组件的对比

语法和设计思想

  • 函数式组件:基于函数式编程思想,使用函数定义组件。没有 this,逻辑更清晰,易于测试。
  • 类组件:基于面向对象编程思想,使用 class 定义组件。依赖 this 访问状态和方法,封装性较好,但不利于测试。

生命周期和状态变量

  • 类组件:
    • 使用 this.state 定义状态变量,通过 this.setState 更新状态。
    • 有明确的生命周期方法,如 componentDidMount、shouldComponentUpdate 和 componentWillUnmount。
  • 函数式组件:
    • 没有 this,使用 useState Hook 创建状态变量,通过解构返回的状态和更新函数进行操作。
    • 没有显式的生命周期方法,通过 useEffect 等 Hook 实现类似功能,如 useEffect 可以实现 componentDidMount 和 componentDidUpdate 的功能。

复用性

  • 类组件:
    • 通过高阶组件(HOC)或 render props 实现逻辑复用。
    • 适合复杂的逻辑复用场景,但代码可能更冗长。
  • 函数式组件:
    • 通过自定义 Hook 实现逻辑复用,代码更简洁,复用性更高。
    • 注意事项:
      • 避免在循环、条件判断或嵌套函数中调用 Hook,确保调用顺序稳定。
      • 不能在 useEffect 中使用 useState,否则会报错。

React 中 render()

render() 方法的主要职责是返回一个 React 元素(或元素树),描述组件在页面上的结构和外观。这个元素可以是原生的 DOM 元素(如 <div><button>)或其他 React 组件。

  • 描述组件的 UI 结构:通过返回 React 元素或元素树,定义组件在页面上的外观。
  • 保持纯净性:确保每次调用时返回的结果一致,避免副作用。
  • 支持虚拟 DOM:返回的 React 元素是虚拟 DOM 的一部分,React 会根据这些元素高效地更新真实 DOM。

React合成事件SyntheticEvent

事件委托机制

  • 统一绑定:React 不会在每个 DOM 元素上单独绑定事件处理函数,而是在应用的根容器(通常是 document 或一个顶层容器)上统一绑定事件监听器。这种设计减少了大量 DOM 节点的事件绑定开销。
  • 冒泡捕获:当事件触发时,浏览器的事件冒泡机制会将事件从目标元素逐层向上冒泡。React 的全局事件监听器捕获到这些事件后,根据事件冒泡路径查找并分发到各个组件上注册的事件处理函数。

事件对象封装

  • 统一接口:为了兼容不同浏览器,React 对原生事件对象进行封装,生成一个合成事件对象(SyntheticEvent)。该对象提供统一的属性和方法(如 preventDefault() 和 stopPropagation()),屏蔽了浏览器间的差异。
  • 代理原生事件:合成事件内部保存对原生事件的引用,开发者调用合成事件的方法时,实际会代理调用原生事件的方法,确保行为一致。

事件池机制

  • 对象复用:为了减少内存开销和频繁创建对象,React 采用事件池(Event Pooling)技术。事件处理完成后,合成事件对象会被重置并存储到池中,供下次事件复用。
  • 持久化:如果需要在异步操作中访问事件对象的属性,可以调用 event.persist() 方法取消事件池化,使得该合成事件不会被自动清空。

事件分发与调度

  • 分层分发:React 沿着事件冒泡路径查找注册的事件处理函数,并按照从内到外的顺序依次执行回调。这种分层分发机制确保了事件传播的正确性。
  • 调度控制:React 内部有自己的调度机制来控制事件回调的执行顺序,确保高优先级的用户交互事件能够及时响应。

跨平台和扩展性

  • 统一 API:React 的合成事件系统在 React Native 等平台上也提供了统一的事件处理接口,方便开发者跨平台编写一致的事件逻辑。
  • 扩展处理:通过封装的方式,React 可以在合成事件中加入额外的功能或优化,而无需改变开发者编写的代码。

commit 阶段做了什么?

在 React 的更新流程中,commit 阶段 是继 render(协调)阶段 之后的第二个阶段,主要负责将 render 阶段计算好的变更实际应用到 DOM 上,并处理相关的副作用。

应用 DOM 更新

1、实际操作 DOM

在 render 阶段,React 生成了一个新的 Fiber 树,其中记录了哪些节点需要更新、插入或删除。 commit 阶段会遍历这个 Fiber 树,并将所有变更同步地反映到真实的 DOM 上。

2、分阶段更新

commit 阶段通常分为三个子阶段:

  • Before Mutation Phase(变更前阶段):在开始 DOM 操作前,调用诸如 getSnapshotBeforeUpdate 等方法,捕获 DOM 更新前的信息。
  • Mutation Phase(变更阶段):这一阶段实际执行 DOM 的插入、删除和更新操作,同时更新 refs 等。
  • Layout Phase(布局阶段):在 DOM 更新完成后,执行 useLayoutEffect 回调,进行一些需要读取或测量 DOM 布局的操作。

调用生命周期方法和 Hook

1、生命周期方法 对于类组件,commit 阶段会调用 componentDidMount 或 componentDidUpdate 等生命周期方法,以便在组件更新后执行额外逻辑。

2、Effect Hooks

对于函数组件:

  • useEffect 中注册的回调会在 commit 阶段的合适时机被触发(通常在浏览器绘制之后异步执行)。
  • useLayoutEffect 则在布局阶段同步执行,确保在浏览器绘制前完成必要的同步 DOM 操作。

处理副作用与清理工作

1、副作用执行 这个阶段不仅仅是更新 DOM,还会执行一些副作用(effects),比如 ref 更新、第三方库的调用等,这些都是在确保 DOM 状态一致后再执行的。

2、清理旧副作用 在执行新的副作用之前,React 还会对比之前保存的 effect,并对不再需要的 effect 进行清理工作(例如调用 useEffect 返回的清理函数)。

总结

将 render 阶段计算出的差异同步地更新到 DOM 中。 调用生命周期方法和 Hook 以处理副作用和完成相关清理。 分为多个子阶段,确保在 DOM 操作前后都能正确地捕获、应用和调整状态。

React Fiber

React Fiber 是 React 团队在 2017 年引入的一项重大架构重写。

它的诞生旨在解决 React 15 及之前版本中的一些关键问题,尤其是在处理复杂、动态用户界面时的性能和响应能力。

解决的问题

  • 卡顿和不流畅:在渲染大规模组件树或处理复杂的动画和交互时,长时间的渲染过程会导致主线程被阻塞,进而导致界面卡顿或不响应用户操作。
  • 无法中断的渲染:一旦渲染开始,就无法中断。即使用户执行了更高优先级的操作(例如点击按钮),也必须等待当前渲染任务完成。这会导致糟糕的用户体验。
  • 难以管理的优先级:React 15 及之前的版本中,所有的更新都有相同的优先级。无法灵活地管理不同类型更新的优先级,例如用户输入应有比动画更高的优先级。

在 React 15 中,渲染 App 组件会同步处理整个组件树。而在 React Fiber 中,渲染过程可以被分解成小任务,例如:

class App extends React.Component {
  render() {
    return (
      <div>
        <Header />
        <MainContent />
        <Footer />
      </div>
    );
  }
}

先渲染 Header 组件。 如果有高优先级任务出现(如用户点击),暂停渲染 MainContent,处理用户点击事件。 完成高优先级任务后,再继续渲染 MainContent 和 Footer。

设计目标

  • 增量渲染:将渲染过程分解为可中断的小任务,每个任务只处理一小部分组件树。这样,React 可以在处理一部分任务后暂停,并处理其他更高优先级的任务(如用户交互)。
  • 优先级调度:引入优先级机制,允许更高优先级的任务(如用户输入)中断低优先级的任务(如动画或后台数据同步),从而提升用户体验。
  • 动画和布局的流畅性:提高动画和过渡效果的流畅性,通过优化调度和渲染策略,确保动画帧率稳定在 60fps。
  • 灵活的更新机制:提供更灵活的更新机制,支持时间切片(time slicing)和并发模式(concurrent mode),允许 React 在后台处理更新任务,不阻塞主线程。

Fiber 的工作原理

  • 增量渲染:React Fiber 将渲染过程分解为多个小任务,每个任务只处理一部分组件树。这使得 React 可以在任务之间检查并处理更高优先级的任务。
  • 优先级调度:React Fiber 引入了优先级机制,允许更高优先级的任务(如用户输入)中断低优先级的任务(如动画或后台数据同步)。
  • 时间切片:React Fiber 使用时间切片技术,将渲染工作分割成小块,每次只处理一部分任务,允许 React 在任务之间检查并处理更高优先级的任务。

渲染与提交阶段

Fiber 将更新过程分为两个阶段:

  • Render 阶段(协调阶段)
    • React 遍历 Fiber 树,执行组件函数,计算更新并生成新的 Fiber 节点。
    • 此阶段是纯计算的,不直接操作 DOM,可以被中断或暂停。
  • Commit 阶段
    • 将 Render 阶段的结果应用到 DOM 中。
    • 该阶段是同步的,确保最终 UI 的一致性。

中断与恢复 Fiber 的核心优势在于 任务中断与恢复: 任务拆分:Fiber 节点通过 child 和 sibling 指针将复杂的更新任务拆分为多个小任务(work units),每个任务可以在浏览器的空闲时间片中完成。 中断与恢复:当高优先级任务(如用户交互)出现时,React 可以暂停当前任务,处理高优先级任务后,通过 return 指针恢复到中断点,继续处理剩余任务。

Fiber 节点与数据结构

每个 React 组件对应一个 Fiber 节点,这些节点通过链表树结构连接,便于灵活遍历和操作:

  • child:指向当前节点的第一个子节点。
  • sibling:指向同层的下一个节点。
  • return:指向父节点。
  • alternate:实现“双缓存”机制,用于对比当前树与上一次渲染树,高效复用和更新。
  • effectTag:标记节点需要执行的操作(如插入、删除、更新),便于在 commit 阶段批量处理 DOM 更新。

Fiber 节点连接原理

遍历方式:

  • 深度优先遍历:通过 child 指针,React 可以逐层深入到最底层的节点,处理完最底层的节点后再通过 return 指针回溯到父节点,继续处理其他子节点。
  • 兄弟节点横向遍历:当一个节点没有子节点或者处理完所有子节点后,通过 sibling 指针跳转到同一层级的下一个节点,继续处理。
  • 回溯与衔接:return 指针确保在处理完子节点后能够正确返回到父节点,从而完成整个树的遍历。

假设有一个简单的 Fiber 树结构:

A
├── B
│   └── D
└── C
  • 从根节点 A 开始。
  • 通过 child 指针进入 B。
  • 通过 child 指针进入 D,处理 D。
  • 处理完 D 后,通过 return 指针返回到 B。
  • 通过 sibling 指针进入 C,处理 C。
  • 处理完 C 后,通过 return 指针返回到 A,完成整个遍历。

优化组件以利用 Fiber

  • 避免不必要的渲染:使用 React.memo、useCallback 和 useMemo 等工具来避免不必要的组件重新渲染。
  • 合理使用 Hooks:确保 Hooks 的调用顺序一致,避免在条件语句或循环中调用 Hooks。
  • 利用并发特性:在合适的场景下使用并发模式(Concurrent Mode),例如在处理复杂的用户交互或动画时

React Fiber 通过 时间分片 和 优先级调度,将复杂的渲染任务分解为可中断的小任务。它利用链表树结构高效管理组件树,并通过 Render 和 Commit 两个阶段实现异步渲染。

这种设计不仅提升了渲染性能,还确保了用户交互的响应性,是现代 React 应用流畅体验的关

Fiber调度

类式组件和函数式组件的区别

React Fragment

在 React 中,如果需要渲染多个元素,通常需要一个父元素来包裹它们,否则会报错。 这是因为 JSX 编译后会生成多个 React.createElement 调用,这在语法上是不合法的。

问题与解决

报错原因

JSX 编译后会生成多个 React.createElement 调用,这不符合语法规范。

// 编译前
const dom = (
  <ChildA />
  <ChildB />
  <ChildC />
);
 
// 编译后
const dom = (
  React.createElement(ChildA),
  React.createElement(ChildB),
  React.createElement(ChildC)
);
 

解决:使用 div 包裹

虽然可以解决编译问题,但会引入额外的 DOM 节点。

// 编译前
const dom = (
  <div>
    <ChildA />
    <ChildB />
    <ChildC />
  </div>
)
 
// 编译后
function MyComponent() {
  return React.createElement(
    'div',
    null,
    React.createElement(ChildA),
    React.createElement(ChildB),
    React.createElement(ChildC)
  )
}

解决:使用 Fragment

Fragment 可以将子列表分组,最终在渲染为真实 DOM 节点时会被忽略,不会引入额外节点。

// 编译前
const dom = (
  <React.Fragment>
    <ChildA />
    <ChildB />
    <ChildC />
  </React.Fragment>
)
 
// 编译后
function MyComponent() {
  return React.createElement(
    React.Fragment,
    null,
    React.createElement(ChildA),
    React.createElement(ChildB),
    React.createElement(ChildC)
  )
}

简写形式

<></>

const dom = (
  <>
    <ChildA />
    <ChildB />
    <ChildC />
  </>
)

ReactElement 类型

Fragment 对应的 ReactElement 元素类型为 Symbol('react.fragment')。

console.log(
  <>
    <div>1</div>
    <div>2</div>
  </>
)
// 输出:
// {
//   type: Symbol('react.fragment'),
//   ...
// }

高阶组件HOC

高阶组件(Higher-Order Component,HOC)是React中一种重用组件逻辑的高级技术。

定义

高阶组件是一个函数,它接受一个组件(或组件类)作为输入,并返回一个新的组件。这个新组件通常会:

  • 包装原始组件
  • 增强或修改原始组件的行为
  • 重用代码逻辑
import React from 'react';
 
// 高阶组件:日志记录功能
const withLogging = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log(`Component ${WrappedComponent.name} mounted`);
    }
 
    componentWillUnmount() {
      console.log(`Component ${WrappedComponent.name} unmounted`);
    }
 
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};
 
// 原始组件
const MyComponent = () => {
  return <div>My Component</div>;
};
 
// 使用HOC增强组件
const EnhancedComponent = withLogging(MyComponent);
 
// 使用增强后的组件
export default EnhancedComponent;
 

用法

  • withLogging 是一个HOC,它接受一个组件WrappedComponent作为参数。
  • withLogging 返回一个新的组件,这个新组件在WrappedComponent的基础上添加了日志记录功能。
  • MyComponent 是一个普通的React组件。
  • EnhancedComponent 是通过HOC增强后的组件,它具有日志记录功能。

随着React Hooks的引入,许多HOC的用例可以通过Hooks来实现。

高阶组件HOC应用场景

代码重用,逻辑和引导抽象

HOC允许你将通用逻辑抽象出来,避免重复代码。这使得组件更加模块化和可维护。

参考上述的 withLogging 实现。

渲染劫持

HOC可以在渲染过程中修改或增强组件的输出。这在需要动态修改组件显示内容时非常有用。

权限控制

import React from 'react';
 
// 高阶组件:权限控制
const withPermission = (WrappedComponent, requiredPermission) => {
  return class extends React.Component {
    render() {
      const { permissions } = this.props;
      if (!permissions.includes(requiredPermission)) {
        return <div>Access Denied</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
};
 
// 原始组件
const AdminPanel = () => {
  return <div>Admin Panel Content</div>;
};
 
// 使用HOC增强组件
const ProtectedAdminPanel = withPermission(AdminPanel, 'admin');
 
// 使用增强后的组件
export default ProtectedAdminPanel;
 

状态抽象和控制

HOC可以管理组件的状态,提供统一的状态管理。这在需要共享状态或复杂状态逻辑时非常有用。

状态提升

 
import React from 'react';
 
// 高阶组件:状态提升
const withState = (WrappedComponent, initialState) => {
  return class extends React.Component {
    state = initialState;
 
    render() {
      return <WrappedComponent {...this.props} state={this.state} />;
    }
  };
};
 
// 原始组件
const Counter = ({ state }) => {
  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={() => state.setCount(state.count + 1)}>Increment</button>
    </div>
  );
};
 
// 使用HOC增强组件
const StatefulCounter = withState(Counter, {
  count: 0,
  setCount: (newCount) => this.setState({ count: newCount }),
});
 
// 使用增强后的组件
export default StatefulCounter;
 
 

Props 控制

HOC可以修改或添加Props,增强组件的灵活性。这在需要为组件提供默认Props或动态Props时非常有用。

import React from 'react';
 
// 高阶组件:默认Props
const withDefaultProps = (WrappedComponent, defaultProps) => {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props} {...defaultProps} />;
    }
  };
};
 
// 原始组件
const Greeting = ({ name, message }) => {
  return <div>{message} {name}!</div>;
};
 
// 使用HOC增强组件
const DefaultGreeting = withDefaultProps(Greeting, {
  message: 'Hello',
});
 
// 使用增强后的组件
export default DefaultGreeting;
 

纯组件

纯组件(Pure Component)是一种特殊的组件,它们的渲染过程仅依赖于输入的props和state,而不会受到外部因素的影响。

纯组件的渲染过程是“纯”的,意味着只要输入相同,输出就一定相同。这种特性使得纯组件更容易理解和维护,同时也提高了应用的性能。

特点

  • 纯渲染逻辑:纯组件的渲染逻辑仅依赖于其props和state,不会受到外部状态或副作用的影响。
  • 性能优化:纯组件通过实现shouldComponentUpdate生命周期方法,避免了不必要的渲染,从而提高了性能。
  • 简单性:纯组件的逻辑简单,易于测试和维护。

实现

通过继承React.PureComponent来创建纯组件。React.PureComponent默认实现了shouldComponentUpdate方法,该方法会进行浅比较(shallow comparison)来判断props和state是否发生变化。 如果props或state没有变化,组件不会重新渲染。

import React, { PureComponent } from 'react';
 
class PureComp extends PureComponent {
  render() {
    console.log('PureComp rendered');
    return <div>Pure Component</div>;
  }
}
 
export default PureComp;
 

使用场景

  • 展示数据:当组件仅用于展示数据且不涉及复杂的交互逻辑时,纯组件是一个理想的选择。
  • 列表项:在渲染列表项时,纯组件可以避免不必要的渲染,提高性能。
  • 性能敏感的场景:在需要高性能的应用中,纯组件可以帮助减少不必要的渲染。

局限性

  • 复杂交互:纯组件不适合处理复杂的交互逻辑,因为它们不支持副作用。
  • 深比较:React.PureComponent的shouldComponentUpdate方法只进行浅比较,对于嵌套对象或数组的变化可能无法正确检测。

React 中 key 的重要性

特点:

  • 识别唯一的 Virtual DOM 元素:当列表项的顺序发生变化时,React 使用 key 来识别哪些项需要重新渲染,哪些项可以复用。
  • 驱动 UI 的数据识别:当列表项的 key 发生变化时,React 会重新渲染对应的DOM元素。
  • 提高渲染性能:通过 key,React 可以快速识别哪些项已经添加、删除或移动,从而只对这些项进行DOM操作,而不是对整个列表进行重新渲染。
  • 列表项的唯一性:避免使用数组索引作为 key,因为索引在列表项重新排序时会发生变化,导致性能问题。
import React from 'react';
 
const ListComponent = ({ items }) => {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
};
 
export default ListComponent;
 

在 React 组件中更新嵌套状态

您在 React 组件中有一个深度嵌套的状态对象,它表示用户信息,包括用户的地址。您的任务是在单击按钮时更新地址对象中的城市属性。 挑战在于确保在状态中仅更新城市值,而不直接改变原始状态。单击“更新城市”按钮后,UI 应反映新的城市值。 具体任务:单击“更新城市”按钮时,将城市值更新为“旧金山”,并确保此更改立即反映在 UI 中。

为了正确更新城市属性而不改变原始状态,您应该使用扩展运算符创建需要更新的状态的每个级别的浅表副本。

  • 初始化状态:使用useState钩子初始化一个嵌套状态对象,包含用户信息和地址信息。
  • 更新城市属性:
    • 定义updateCity函数,使用扩展运算符(...)创建状态对象的副本,逐步更新嵌套对象。
    • 通过setState函数更新状态,确保不可变性。
  • 渲染UI:
    • 返回一个包含用户信息和一个按钮的div元素。
    • 按钮的onClick事件绑定到updateCity函数。
 
import React, { useState } from 'react';
 
const NestedStateComponent = () => {
  // 初始化状态
  const [state, setState] = useState({
    user: {
      name: 'John Doe',
      address: {
        city: 'New York',
        zip: '10001'
      }
    }
  });
 
  // 更新城市属性的函数
  const updateCity = () => {
    // 使用扩展运算符创建状态的副本
    setState(prevState => ({
      ...prevState,
      user: {
        ...prevState.user,
        address: {
          ...prevState.user.address,
          city: 'San Francisco'
        }
      }
    }));
  };
 
  return (
    <div>
      <h1>{state.user.name}</h1>
      <h2>{state.user.address.city}</h2>
      <button onClick={updateCity}>Update City</button>
    </div>
  );
};
 
export default NestedStateComponent;
 

防止子组件不必要的重新渲染

在React中,当父组件重新渲染时,所有子组件也会重新渲染,即使它们的props没有发生变化。这会导致性能问题,尤其是在子组件渲染开销较大的情况下。

解决方案

使用 React.memo 来记忆子组件的渲染输出。React.memo 是一个高阶组件,它会比较组件的props,如果props没有变化,就不会重新渲染组件。

  • 初始渲染:当父组件首次渲染时,ChildComponent 会渲染一次,控制台输出 ChildComponent rendered。
  • 点击按钮:每次点击“Increment Count”按钮时,父组件的 count 状态更新,父组件会重新渲染。但由于 data prop 没有变化,ChildComponent 不会重新渲染,控制台不会再次输出 ChildComponent rendered。
import React, { useState, memo } from 'react';
 
// 使用 React.memo 包装 ChildComponent
const ChildComponent = memo(({ data }) => {
  console.log('ChildComponent rendered');
  return <div>{data.name}</div>;
});
 
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [data, setData] = useState({ name: 'John' });
 
  const incrementCount = () => {
    setCount(count + 1);
  };
 
  return (
    <div>
      <button onClick={incrementCount}>Increment Count</button>
      <p>Count: {count}</p>
      <ChildComponent data={data} />
    </div>
  );
};
 
export default ParentComponent;
 
  • React.memo:React.memo 是一个高阶组件,它会记忆组件的渲染输出。如果传递给组件的props没有变化,React.memo 会阻止组件重新渲染。
  • 性能优化:通过使用 React.memo,只有当 data prop 发生变化时,ChildComponent 才会重新渲染。这样可以避免不必要的渲染,提高性能。

防止父组件状态导致子重新渲染

在React中,当父组件重新渲染时,传递给子组件的函数 prop 会被重新创建,导致子组件认为 prop 发生了变化,从而重新渲染。

这会导致性能问题,尤其是在子组件渲染开销较大的情况下。

解决方案

使用 useCallback 来记忆事件处理函数,确保函数的引用在父组件重新渲染时保持不变。结合 React.memo,可以防止子组件不必要的重新渲染。

// 使用 useCallback 和 React.memo 来优化子组件渲染的代码示例:
import React, { useState, useCallback, memo } from 'react';
 
// 使用 React.memo 包装 ChildComponent
const ChildComponent = memo(({ onClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Child Button</button>;
});
 
const ParentComponent = () => {
  const [count, setCount] = useState(0);
 
  // 使用 useCallback 记忆 handleClick 函数
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 依赖数组为空,确保函数只创建一次
 
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};
 
export default ParentComponent;
 
 

解释

  • useCallback:useCallback 是一个 React Hook,它会记忆函数,确保在父组件重新渲染时,函数的引用保持不变。这可以防止子组件因为函数 prop 的引用变化而重新渲染。
  • React.memo:React.memo 是一个高阶组件,它会记忆组件的渲染输出。如果传递给组件的 props 没有变化,React.memo 会阻止组件重新渲染。
  • 性能优化:通过结合使用 useCallback 和 React.memo,可以确保子组件仅在实际 props 发生变化时重新渲染,从而避免不必要的渲染,提高性能。

验证

  • 初始渲染:当父组件首次渲染时,ChildComponent 会渲染一次,控制台输出 ChildComponent rendered。
  • 点击按钮:每次点击“Increment Count”按钮时,父组件的 count 状态更新,父组件会重新渲染。但由于 handleClick 函数的引用没有变化,ChildComponent 不会重新渲染,控制台不会再次输出 ChildComponent rendered。
  • 点击子组件按钮:点击“Child Button”时,控制台输出 Button clicked,验证事件处理函数的正确性。

React 懒加载的实现

React.lazy 和 Suspense

React.lazy 是一个函数,它接受一个返回 Promise 的函数(通常是动态导入 import()),并返回一个 React 组件。

Suspense 是一个 React 组件,用于捕获延迟加载组件的加载状态,并显示一个加载指示器(fallback UI)。

import React, { Suspense } from 'react'
import { render } from 'react-dom'
 
// 懒加载组件
const OtherComponent = React.lazy(() => import('./OtherComponent'))
 
function App() {
  return (
    <div>
      <h1>主组件</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  )
}
 
render(<App />, document.getElementById('root'))

动态导入

在 React.lazy 中使用 import() 进行动态导入。import() 返回一个 Promise,该 Promise 在模块加载完成后解析为模块对象。

const OtherComponent = React.lazy(() => import('./OtherComponent'))

错误边界(Error Boundaries)

问题背景:默认情况下,若一个组件在渲染期间发生错误,会导致整个组件树被卸载(页面白屏),这显然不是期望的结果。

为了处理懒加载组件的加载失败情况,可以使用错误边界组件。错误边界组件捕获子组件的错误并显示友好的错误信息。

作用:错误边界可以捕获子组件的错误,打印或上报错误信息,并渲染一个降级(备用)UI,避免整个应用崩溃。

class ErrorBoundary extends React.Component {
  state = { hasError: false }
 
  static getDerivedStateFromError(error) {
    return { hasError: true }
  }
 
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}
 
function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </ErrorBoundary>
  )
}

注意事项:

  • 仅支持类组件:错误边界目前只在类组件中实现,因为其依赖于 this.setState 的回调特性,而 useState 不支持回调。
  • 无法捕获的场景:
    • 自身的错误:错误边界无法捕获自身的错误。
    • 异步错误:无法捕获异步代码中的错误。
    • 事件中的错误:无法捕获事件处理函数中的错误。
    • 服务端渲染的错误:无法捕获服务端渲染中的错误。
  • 对 Hooks 的影响:错误边界组件本身必须是类组件,但它可以包裹使用了 Hooks 的组件,对其功能没有影响。

预加载(Preloading)

在某些情况下,可能希望在组件渲染之前预加载懒加载的组件。可以通过调用 React.lazy 返回的模块对象的 then 方法来实现预加载。

const LazyComponent = React.lazy(() => import('./LazyComponent'))
 
// 预加载
LazyComponent.preload = () => {
  return import('./LazyComponent')
}
 
// 在需要时预加载
LazyComponent.preload()

多个组件懒加载

可以同时懒加载多个组件,并为每个组件提供独立的加载状态。

const Component1 = React.lazy(() => import('./Component1'))
const Component2 = React.lazy(() => import('./Component2'))
 
function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading Component 1...</div>}>
        <Component1 />
      </Suspense>
      <Suspense fallback={<div>Loading Component 2...</div>}>
        <Component2 />
      </Suspense>
    </div>
  )
}

为什么useState返回数组?

解构赋值的灵活性

1、数组解构的简洁性

使用数组解构可以方便地命名状态变量和更新函数,代码更加简洁:const [count, setCount] = useState(0);

这种方式允许开发者自由命名变量,而不受返回值名称的限制。

2、对象解构的局限性

如果 useState 返回对象,开发者必须使用与返回值同名的属性,这会限制灵活性:

避免命名冲突

对象属性命名的潜在问题 如果 useState 返回对象,可能会导致命名冲突。例如,如果组件中已经有同名的属性,可能会引发意外行为:

const { count } = { count: 0 } // 可能与其他对象属性冲突

性能与优化

数组的性能优势:数组的访问和解构通常比对象更快,特别是在频繁更新的状态变量中,这有助于提高性能。

useState 是同步还是异步的?

结论

在 React 中,useState 的更新操作是异步的。 当你调用 setCount(count + 1) 时,React 并不会立即更新 count 的值,而是将这个更新操作放入一个批量更新队列中。 等到 React 处理完所有当前事件的更新操作后,才会批量执行这些更新,并重新渲染组件。

function Counter() {
  const [count, setCount] = useState(0);
 
  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 仍然是旧值
  };
 
  return <button onClick={handleClick}>Click me: {count}</button>;
}
 
  • setCount(count + 1) 触发了状态更新,但不会立即修改 count。
  • console.log(count) 仍然会打印旧值,因为 count 还没有被更新。

工作机制

  • 非真正异步‌:useState 的更新行为并非传统意义上的异步(如 Promise 或 setTimeout),而是 React 的「批量更新策略(Batching)」和「调度机制(Scheduler)」共同作用的结果。
  • ‌同步执行,异步生效‌:setState 调用本身是同步的,但状态的更新会被 React 加入「更新队列」,在 React 的调度周期结束后才会生效。

强制同步更新

绕过批处理的强制同步更新。

// React 18+ 使用 flushSync 强制同步更新
import { flushSync } from 'react-dom'
 
const handleClick = () => {
  flushSync(() => {
    setCount(count + 1) // 立即更新
  })
  console.log(count) // 可获取新值
}

这种方式确保了 prevCount 是最新的值,即使在多次调用 setCount 时也能正确地获取到最新的状态。

获取新值

1、使用函数式更新 当你需要基于当前的 state 值进行更新时,使用函数式更新:

setCount((prevCount) => prevCount + 1)

2、避免直接依赖 state 的值 在调用 setState 后,不要依赖 state 的值,因为它是异步更新的。如果需要处理更新后的值,可以使用 useEffect 或其他生命周期方法。

设计哲学

React 为了优化性能,会合并多个 setState 调用,减少不必要的渲染。 如果 useState 是同步的,每次 setState 都会导致组件重新渲染,这会显著影响性能。

useState 的源码实现?

useState 的本质

在 React 内部,「useState 实际上是对 useReducer 的封装」。其基本实现类似于下面这样:

function useState(initialState) {
  return useReducer(basicStateReducer, initialState)
}
 
function basicStateReducer(state, action) {
  // 如果传入的是函数,则调用它获得下一个 state,否则直接返回 action
  return typeof action === 'function' ? action(state) : action
}

这段代码展示了 useState 的精髓:「每次状态更新时,你可以传入新的状态值,也可以传入一个函数(接收当前状态返回新的状态)。」

内部 Hook 的数据结构

React 为每个组件维护一个与之对应的 「hook 链表」(挂载在 Fiber 对象上)。

// ReactFiberHooks.js
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current
  return dispatcher
}
 
function useState(initialState) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useState(initialState)
}
 
function useReducer(reducer, initialArg, init) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useReducer(reducer, initialArg, init)
}

每个 hook 保存了自己的状态值和一个更新队列。当你调用 useState 时,React 会在内部执行类似如下的逻辑:

挂载阶段(组件初次渲染时)

在组件初次挂载时,会调用 mountState,其大致实现如下:

function mountState(initialState) {
  // 创建一个新的 hook,并挂载到当前正在渲染的 fiber 上
  const hook = mountWorkInProgressHook()
 
  // 如果 initialState 为函数,则调用它以获得初始状态
  hook.memoizedState = typeof initialState === 'function' ? initialState() : initialState
 
  // 初始化更新队列
  hook.queue = {
    pending: null, // 待处理的更新(以循环链表存储)
    dispatch: null, // 更新函数
    last: null, // 指向最后一个更新
  }
 
  // 创建并绑定 dispatch 函数,后续用于添加更新
  const dispatch = (hook.queue.dispatch = dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    hook.queue
  ))
 
  return [hook.memoizedState, dispatch]
}

这里主要做了两件事:

  • 保存初始状态:(memoizedState)。
  • 创建一个更新队列:这个队列用于收集后续的 state 更新。

更新阶段(组件重新渲染时)

当组件重新渲染时,会调用 updateState,大致逻辑如下:

function updateState(initialState) {
  // 找到对应的 hook
  const hook = updateWorkInProgressHook()
  const queue = hook.queue
  const pending = queue.pending
 
  if (pending !== null) {
    let newState = hook.memoizedState
    // pending 是一个循环链表,依次应用每个更新
    let first = pending.next
    let update = first
    do {
      const action = update.action
      // 通过 reducer 计算下一个 state
      newState = typeof action === 'function' ? action(newState) : action
      update = update.next
    } while (update !== first)
 
    hook.memoizedState = newState
    // 清空更新队列
    queue.pending = null
  }
 
  return [hook.memoizedState, queue.dispatch]
}

这部分代码做了以下工作:

  • 获取当前状态:从 hook 的 memoizedState 获取当前状态。
  • 遍历更新队列:从 pending 开始,依次处理每个更新操作。
  • 应用更新:根据更新操作的类型(值或函数),计算新的状态。
  • 更新状态:将新的状态存储回 memoizedState。
  • 清空更新队列:将 pending 设置为 null,表示所有更新已处理完毕。

更新调度与批量处理

每次你调用 setState(也就是 dispatch 函数),React 并不会立刻同步更新 state,而是会将更新加入到 hook 的队列中,并等待合适的时机进行「批量处理」。这种设计可以优化性能,避免重复渲染。

在事件处理函数中,React 会批量合并多次更新,然后在下一次渲染时一次性应用;而在某些异步回调(如 setTimeout、Promise.then)中,React 18 之后也会自动批处理更新,这使得行为更趋一致。

小结

  • 核心原理:useState 是对 useReducer 的封装,内部采用了基本的 reducer 模式(basicStateReducer)。
  • 数据结构:每个 hook 记录了 state 和一个循环链表形式的更新队列。
  • 更新流程:调用 setState 时,将更新添加到队列中;渲染时遍历队列计算新的 state。

React VS Vue 有何区别

设计理念

  • React: 更倾向于函数式编程思想,推崇组件的不可变性和单向数据流。
  • Vue: 结合了响应式编程和模板系统,致力于简化开发过程。

组件化方式不同

  • React 组件包含状态和行为,所有组件共享一个状态树
  • Vue 每个组件都有自己的状态和行为,并且可以很容易将数据和行为绑定在一起

数据驱动方式不同

  • React 主要采用单向数据流,组件状态通过setState方法更新
  • Vue 支持双向数据绑定(使用v-model指令),适合于简化表单输入等场景。

模板语法不同

  • React 使用JSX(JavaScript XML),将标记语言与JavaScript逻辑混写。
  • Vue 使用基于HTML的模板语法,允许开发者使用纯HTML、CSS和JavaScript,支持指令

生命周期不同

  • React 生命周期:初始化、更新、卸载
  • Vue 生命周期:创建、挂载、更新、销毁

状态管理方式不同

  • React 状态管理通常通过使用Context API或引入如Redux、MobX的库来实现。
  • Vue 提供了Vuex作为官方的状态管理解决方案。

性能优化方式不同

  • React 性能优化:React.memo、shouldComponentUpdate
  • Vue 性能优化:keep-alive、v-if

响应式系统

  • React: 通过setState和useState等API显式地触发UI更新。
  • Vue: 通过其响应式系统自动追踪依赖并在数据变化时更新视图。

类型支持

  • React: 原生支持JavaScript,但可以很好地与TypeScript结合。
  • Vue: Vue 3提供了更好的TypeScript支持。

React 服务端渲染原理

服务端处理请求与数据获取

  • ‌请求匹配‌:Node.js 服务端接收客户端请求,解析请求的 URL 路径,根据路由配置(如 react-router)匹配对应的 React 组件。
  • ‌数据预取‌:执行组件定义的异步数据获取逻辑(如 getServerSideProps 或自定义数据加载方法),获取渲染所需数据。
  • ‌数据注入‌:将数据以以下形式注入组件:
    • Props‌:直接传递给组件。
    • 全局状态‌(如 Redux Store):初始化服务端 Store 并同步到客户端。
    • Context‌:通过 React Context 传递数据。

服务端渲染生成 HTML

  • 组件渲染为字符串‌:使用 react-dom/server 的 renderToString(React 17 及之前)或 renderToPipeableStream(React 18+ 流式渲染)方法,将 React 组件转换为 HTML 字符串。
  • ‌静态标记优化‌:若无需客户端交互,可用 renderToStaticMarkup 生成纯静态 HTML(无 data-reactid 属性)。
  • ‌数据序列化‌:将服务端获取的数据序列化为 JSON,注入到 HTML 中(如 <script>window.__INITIAL_STATE__ = ...</script>),供客户端复用。
import { renderToString } from 'react-dom/server'
import App from './App'
 
const data = fetchData() // 服务端数据预取
const html = renderToString(<App data={data} />)
const finalHTML = `
  <html>
    <body>
      <div id="root">${html}</div>
      <script>window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script>
    </body>
  </html>
`

客户端激活(Hydration)

  • HTML 输出与资源加载‌:服务端返回包含 HTML、数据及客户端 JavaScript 的响应,浏览器解析并加载资源。
  • ‌客户端渲染接管‌:客户端 React 使用 hydrateRoot(React 18+)或 hydrate(React 17 及之前)方法,对比服务端生成的 HTML 节点,‌复用现有 DOM 结构‌并绑定事件等交互逻辑。
  • ‌数据复用‌:从 window.__INITIAL_DATA__ 读取服务端预取数据,避免客户端重复请求。
%% SSR 核心流程示意图
graph TD
    A[客户端请求] --> B{服务端处理}
    subgraph 服务端
        B --> C[路由匹配]
        C --> D[数据预取]
        D --> E[组件渲染为 HTML]
        E --> F[数据序列化注入 HTML]
        F --> G[输出完整 HTML]
    end
    subgraph 客户端
        G --> H[浏览器解析 HTML/CSS]
        H --> I[加载客户端 JS]
        I --> J[数据复用与 Hydration]
        J --> K[绑定事件与交互]
        K --> L[完整单页应用]
    end
 
 

React 创建组件的方法

无状态函数组件

无状态函数组件是最简单的一种组件形式,它是一个纯函数,接收 props 作为参数,并返回要渲染的 React 元素。

无状态函数组件没有自己的状态(state)和生命周期方法。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

ES6 类组件

ES6 类组件使用 ES6 的类语法来定义组件。类组件可以包含状态(state)和生命周期方法,这使得它比无状态函数组件更强大,适用于需要管理状态和执行复杂逻辑的场景。

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}

React实现keep-alive

使用第三方库 react-activation‌

安装 react-activation

npm install react-activation

‌包裹根组件‌

在应用外层添加 AliveScope 上下文提供者:

import { AliveScope } from 'react-activation'
 
function App() {
  return (
    <AliveScope>
      <Router>{/* 路由配置 */}</Router>
    </AliveScope>
  )
}

缓存目标组件‌

<KeepAlive> 包裹需要缓存的组件,必须指定唯一 key:

import { KeepAlive } from 'react-activation'
 
function HomePage() {
  return (
    <KeepAlive key="UniqueHomeKey">
      <ExpensiveComponent />
    </KeepAlive>
  )
}

手动清除缓存‌(可选)

import { useAliveController } from 'react-activation'
 
function ClearCacheButton() {
  const { drop } = useAliveController()
  return <button onClick={() => drop('UniqueHomeKey')}>清除 Home 缓存</button>
}

React diff原理

初始化

  • React 首次渲染时,会创建一个虚拟 DOM 树。
  • 虚拟 DOM 是一个轻量级的 JavaScript 对象,用于表示实际 DOM 的结构。

用户交互

  • 用户与应用交互时,可能会触发组件的 setState 方法。
  • React 将这些更新标记为 "dirty",并记录下来。

批量更新

  • 在每个事件循环结束时,React 会检查所有标记为 "dirty" 的组件。
  • React 会将这些更新合并为一个批量操作,以减少 DOM 操作的次数。

树形结构比较

  • React 从根节点开始,逐层比较虚拟 DOM 树的节点。
  • 每一层只比较同级的节点,避免跨层级的比较。

Key 属性

  • 在列表结构中,React 使用 key 属性来快速识别每个元素。
  • 如果没有 key,React 会使用元素的索引作为默认 key,这可能导致性能问题。

组件匹配

  • React 只会比较相同类名(或函数名)的组件。
  • 如果组件的类名不同,React 会直接销毁旧组件并创建新组件。

选择性子树渲染

  • React 会根据 shouldComponentUpdate 的返回值决定是否更新组件及其子树。
  • 如果 shouldComponentUpdate 返回 false,React 将跳过该组件及其子树的渲染。

DOM 更新

  • React 会根据比较结果,计算出最小的 DOM 操作。
  • 实际的 DOM 操作只针对发生变化的部分,从而提高性能。
// 应用 Diff 结果
function patch(parentDom, patches) {
  patches.forEach((patch) => {
    switch (patch.type) {
      case 'REPLACE':
        const newDom = render(patch.newNode)
        parentDom.replaceChild(newDom, parentDom.firstChild)
        break
      case 'UPDATE':
        // 更新属性
        Object.entries(patch.propPatches).forEach(([key, value]) => {
          if (value === null) {
            patch.dom.removeAttribute(key)
          } else {
            patch.dom.setAttribute(key, value)
          }
        })
        // 递归更新子节点
        patchChildren(patch.dom, patch.childPatches)
        break
      case 'MOVE':
        const movedNode = parentDom.childNodes[patch.from]
        const targetNode = parentDom.childNodes[patch.to]
        parentDom.insertBefore(movedNode, targetNode)
        break
      case 'INSERT':
        const newDom = render(patch.newNode)
        parentDom.appendChild(newDom)
        break
      case 'DELETE':
        parentDom.removeChild(patch.oldChild.dom)
        break
    }
  })
}
flowchart TD
    A[开始Diff比较] --> B{同一层级比较?}
    B -->|是| C[比较节点类型]
    B -->|否| D[销毁旧节点,创建新节点]
    C --> E{类型相同?}
    E -->|是| F[更新节点属性]
    E -->|否| G[销毁旧节点,创建新节点]
    F --> H[处理子节点]
    H --> I{是否有子节点?}
    I -->|是| J[生成新旧子节点Key Map]
    I -->|否| K[结束]
    J --> L[遍历旧子节点]
    L --> M{新列表中存在相同Key?}
    M -->|是| N[移动节点到新位置]
    M -->|否| O[删除旧节点]
    N --> P[递归执行Diff]
    O --> Q[继续遍历]
    Q --> R{是否遍历完成?}
    R -->|否| L
    R -->|是| S[创建新增节点]
    S --> T[结束]
 
 
// 虚拟 DOM 节点结构
class VNode {
  constructor(type, props, key) {
    this.type = type // 节点类型(如 'div')
    this.props = props // 属性对象(含 children)
    this.key = key // 唯一标识
    this.dom = null // 对应的真实 DOM
  }
}
 
// Diff 入口函数
function diff(oldNode, newNode) {
  // 1. 节点类型不同:直接替换
  if (oldNode.type !== newNode.type) {
    return { type: 'REPLACE', newNode }
  }
 
  // 2. 更新属性
  const propPatches = diffProps(oldNode.props, newNode.props)
 
  // 3. Diff 子节点(核心)
  const childPatches = diffChildren(oldNode.props.children, newNode.props.children)
 
  return {
    type: 'UPDATE',
    propPatches,
    childPatches,
  }
}
 
// 属性比较
function diffProps(oldProps, newProps) {
  const patches = {}
 
  // 更新修改的属性
  for (const key in newProps) {
    if (key !== 'children' && oldProps[key] !== newProps[key]) {
      patches[key] = newProps[key]
    }
  }
 
  // 删除移除的属性
  for (const key in oldProps) {
    if (key !== 'children' && !(key in newProps)) {
      patches[key] = null // 标记删除
    }
  }
 
  return patches
}
 
// 子节点比较(含 Key 优化)
function diffChildren(oldChildren, newChildren) {
  const patches = []
 
  // 构建旧子节点 Key 映射表
  const oldKeyMap = {}
  oldChildren.forEach((child, index) => {
    const key = child.key || index
    oldKeyMap[key] = child
  })
 
  let lastIndex = 0
  newChildren.forEach((newChild, newIndex) => {
    const key = newChild.key || newIndex
    const oldChild = oldKeyMap[key]
 
    if (oldChild) {
      // 存在相同 Key:递归 Diff
      patches.push(diff(oldChild, newChild))
 
      // 判断是否需要移动
      if (oldChild._index < lastIndex) {
        patches.push({ type: 'MOVE', from: oldChild._index, to: newIndex })
      }
      lastIndex = Math.max(lastIndex, oldChild._index)
    } else {
      // 新增节点
      patches.push({ type: 'INSERT', newNode: newChild })
    }
  })
 
  // 标记删除的旧节点
  oldChildren.forEach((oldChild, index) => {
    if (!newChildren.some((newChild) => newChild.key === oldChild.key)) {
      patches.push({ type: 'DELETE', oldChild })
    }
  })
 
  return patches
}

React事件机制和原生DOM事件流

事件绑定方式

  • React:事件统一绑定到 document 上,通过事件委托机制分发事件。
  • 原生 DOM:事件直接绑定到具体的 DOM 元素上。

事件冒泡顺序

  • React:事件冒泡是基于虚拟 DOM 的逻辑,事件处理顺序由 React 控制。
  • 原生 DOM:事件按照捕获阶段 → 目标阶段 → 冒泡阶段的顺序传播。

性能优化

  • React:通过事件委托减少内存占用和性能开销,适合动态更新的场景。
  • 原生 DOM:每个 DOM 元素需要单独绑定事件监听器,动态更新时性能较差。

事件对象

  • React:使用合成事件对象(SyntheticEvent),跨浏览器兼容,自动回收。
  • 原生 DOM:使用浏览器提供的原生事件对象,需要手动管理。

开发体验

  • React:事件绑定语法统一,事件管理由 React 自动完成。
  • 原生 DOM:需要手动调用 addEventListener 和 removeEventListener,管理复杂。

事件优先级

  • React:事件处理函数在原生 DOM 的冒泡阶段之后执行。
  • 原生 DOM:事件处理顺序取决于绑定顺序和事件流阶段。

React 18 有哪些更新

  • 并发模式:允许 React 在渲染过程中中断并优先处理高优先级任务,提高应用性能和响应速度。
  • 更新 render API:引入新的根 API(如 createRoot),替代旧的 ReactDOM.render(),支持并发模式。
  • 自动批处理:优化批量更新机制,减少渲染次数,提升性能。
  • Suspense 支持 SSR:支持流式 SSR 和选择性注水,优化首屏加载和交互体验。
  • 新 API:
    • startTransition:管理非紧急更新,确保用户交互流畅。
    • useTransition:管理非紧急更新的过渡状态,提供加载状态标识。
    • useDeferredValue:延迟更新复杂状态,减少卡顿。
    • useId:生成跨服务端和客户端的唯一 ID,解决 SSR 中 ID 不一致问题。
  • 第三方库支持:提供 useSyncExternalStore Hook,支持第三方状态库(如 Redux)集成并发模式。

React Hooks要注意的问题和原因

只能在 React 函数中使用

  • 注意事项:Hooks 只能在 React 的函数组件或自定义 Hooks 中使用,不能在类组件或普通函数中使用。
  • 原因:Hooks 是为函数组件设计的,它们依赖于 React 的内部机制来管理状态和副作用。类组件有自己的状态和生命周期方法,因此不需要 Hooks。

只能在函数最外层调用

  • 注意事项:Hooks 必须在函数组件的最外层调用,不能在条件语句、循环或嵌套函数中调用。
  • 原因:React 依赖于 Hooks 的调用顺序来确保状态的一致性。如果 Hooks 在条件语句或循环中调用,可能会导致 Hooks 的调用顺序不一致,从而引发错误。

更新引用类型的状态时,必须返回新的引用

  • 注意事项:当使用 useState 存储引用类型(如对象或数组)时,更新状态时必须返回一个新的引用。
  • 原因:React 通过比较引用地址来判断状态是否发生变化。如果引用地址没有变化,React 会认为状态没有更新,从而不会触发重新渲染。

避免在 useEffect 中造成无限循环

  • 注意事项:在 useEffect 中,如果依赖数组中的值发生变化,useEffect 会重新执行。如果处理不当,可能会导致无限循环。
  • 原因:依赖数组中的值发生变化时,useEffect 会重新执行。如果 useEffect 中的代码又修改了依赖数组中的值,就会导致无限循环。

清理副作用

  • 注意事项:在 useEffect 中,如果创建了副作用(如订阅、定时器等),需要在清理函数中清除这些副作用。
  • 原因:如果不清理副作用,可能会导致内存泄漏或意外行为。

不要在 useEffect 中直接调用异步函

  • 注意事项:在 useEffect 中,如果需要执行异步操作(如 AJAX 请求),应该在 useEffect 内部定义异步函数并立即调用它。
  • 原因:useEffect 本身不能直接返回异步函数,否则会导致错误。

使用 useCallback 优化性能

  • 注意事项:当需要在子组件中使用父组件的函数时,可以使用 useCallback 来避免不必要的重新渲染。
  • 原因:useCallback 可以确保函数的引用在依赖数组没有变化时保持不变,从而避免子组件的不必要的重新渲染。

使用 useMemo 优化计算

  • 注意事项:对于复杂的计算,可以使用 useMemo 来缓存计算结果,避免每次渲染都重新计算。
  • 原因:useMemo 可以确保只有在依赖数组中的值发生变化时才重新计算,从而提高性能。
import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'
 
// 1. 只能在 React 函数中使用 ✅
const HookDemo = () => {
  // 2. 必须在最外层调用 ✅
  const [count, setCount] = useState(0)
  const [user, setUser] = useState({
    name: 'Alice',
    preferences: [],
  })
 
  // 3. 更新引用类型必须返回新引用 ✅
  const updateUser = () => {
    // 正确做法:创建新对象
    setUser((prev) => ({
      ...prev,
      age: 25,
    }))
 
    // 错误示范(不会触发更新):
    // user.age = 25;
    // setUser(user);
  }
 
  // 4. 避免 useEffect 无限循环 ✅
  useEffect(() => {
    // 正确:添加条件判断
    if (count < 10) {
      setCount((c) => c + 1)
    }
  }, [count]) // 确保依赖数组正确
 
  // 5. 清理副作用 ✅
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick')
    }, 1000)
 
    return () => clearInterval(timer)
  }, [])
 
  // 6. 正确处理异步操作 ✅
  useEffect(() => {
    const fetchData = async () => {
      try {
        // 模拟 API 请求
        const response = await fetch('https://api.example.com/data')
        const data = await response.json()
        console.log(data)
      } catch (error) {
        console.error('Fetch error:', error)
      }
    }
 
    fetchData()
  }, [])
 
  // 7. 使用 useCallback 优化 ✅
  const handleClick = useCallback(() => {
    console.log('Button clicked:', count)
  }, [count]) // 依赖数组保证最新值
 
  // 8. 使用 useMemo 优化计算 ✅
  const filteredList = useMemo(() => {
    // 模拟复杂计算
    return Array.from({ length: 10000 }, (_, i) => i).filter((num) => num % count === 0)
  }, [count])
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((c) => c + 1)}>Increment</button>
 
      <MemoizedChildComponent onClick={handleClick} />
    </div>
  )
}
 
// 配合 useCallback 的 memo 组件
const ChildComponent = ({ onClick }) => {
  return <button onClick={onClick}>Child Button</button>
}
const MemoizedChildComponent = memo(ChildComponent)
 
export default HookDemo

React Ref

作用

  • 存储信息:在函数组件中,ref 可以用来存储某些信息,而不会触发新的渲染。
  • 访问真实 DOM:用于直接访问 DOM 元素。
  • 获取子组件实例:父组件可以通过 ref 获取子组件的实例对象。

获取真实 DOM:三种创建方式

1、React.createRef() 和 useRef

// 类组件
class App extends React.Component {
  constructor(props) {
    super(props)
    this.ref = React.createRef()
  }
  render() {
    return <div ref={this.ref}></div>
  }
}
// 函数组件
 
const App = () => {
  const ref = React.useRef()
  return <div ref={ref}></div>
}

2、回调函数方式

// 类组件
class App extends React.Component {
  bindRef = (ele) => {
    this.bodyRef = ele
  }
  render() {
    return <div ref={this.bindRef}></div>
  }
}
// 函数组件
const App = () => {
  const bindRef = React.useCallback((ele) => {
    // 处理逻辑
  }, [])
  return <div ref={bindRef}></div>
}

3、字符串(仅限类组件)

class App extends React.Component {
  render() {
    return <div ref="bodyRef"></div>
  }
}

获取子组件实例

1、类组件:直接绑定 ref 即可获取子组件的实例。

class ChildComponent extends React.Component {}
 
const App = () => {
  const ref = React.useRef()
  return <ChildComponent ref={ref} />
}

2、函数组件:需要结合 forwardRef 和 useImperativeHandle。

import { forwardRef, useImperativeHandle } from 'react'
 
const ChildComponent = React.forwardRef((props, ref) => {
  useImperativeHandle(ref, () => {
    // 返回要暴露的接口
    return {
      customMethod: () => console.log('Custom method called'),
    }
  }, [])
  return <div>Child Component</div>
})
 
const App = () => {
  const ref = React.useRef()
  return <ChildComponent ref={ref} />
}

转发 ref

1、使用 React.forwardRef

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
))

2、类组件中转发 ref

class ChildComponent extends React.Component {
  render() {
    return <div ref={this.props.innerRef}>1</div>
  }
}
 
const App = () => {
  const bodyRef = React.useRef()
  return <ChildComponent innerRef={bodyRef} />
}

React 性能优化

  • 跳过不必要的组件更新
    • PureComponent:自动浅比较 props 和 state,避免不必要的渲染。
    • React.memo:对函数组件进行记忆化,避免父组件更新时不必要的子组件渲染。
    • shouldComponentUpdate:手动控制组件是否更新。
    • useMemo 和 useCallback:缓存计算结果和回调函数,避免频繁计算和函数重新创建。
  • 状态下放:缩小状态影响范围,将状态移动到只使用它的子树中,减少不必要的父组件更新。
  • 列表项使用 key 属性:优化列表渲染,确保列表项的 key 唯一且稳定,提高列表渲染性能。
  • useMemo:返回虚拟 DOM,利用 useMemo 缓存组件的虚拟 DOM,避免依赖不变时的重新渲染。
  • 跳过回调:避免不必要的 Render:通过 useCallback 缓存回调函数,避免因回调函数变化触发的重新渲染。
  • 自定义 Hooks:按需更新状态,自定义 Hooks 中暴露的状态按需更新,避免不相关的状态变化触发重新渲染。
  • 动画库直接修改 DOM 属性:动画库直接修改 DOM 属性,避免因状态变化触发的组件重新渲染。
  • 组件按需挂载
    • 懒加载:通过 React.lazy 和 Webpack 动态导入实现组件懒加载。
    • 懒渲染:组件进入或即将进入可视区域时才渲染,如 Modal、Drawer 等。
  • 虚拟列表:优化长列表渲染:只渲染可见区域的列表项,减少渲染的 DOM 节点数量。
  • 批量更新
    • 类组件:setState 自带批量更新。
    • 函数组件:合并相关状态更新,减少渲染次数。
    • 按优先级更新:将耗时任务放入宏任务队列,优先响应用户行为。
  • 缓存优化
  • useMemo 缓存计算结果:缓存耗时计算结果,避免重复计算。
  • 接口数据缓存:在系统闲暇时更新缓存数据,优化用户体验。
  • 优化频繁触发的回调函数:防抖(debounce)和节流(throttle):减少频繁触发的回调函数执行次数。

其他的Hook

Hooks 实现核心原理

  1. 核心机制
  • ‌链表结构‌:React 使用链表结构维护 Hooks 的调用顺序
  • ‌索引定位‌:依靠稳定的调用顺序索引(不能有条件判断)
  • ‌闭包存储‌:利用闭包特性存储状态和依赖关系
  1. 实现要点
  • ‌全局存储‌:用数组保存所有 Hook 的状态
  • ‌索引重置‌:每次渲染前重置索引指针
  • ‌依赖对比‌:使用浅比较判断依赖是否变化

通用基础架构

Hook 链表存储

所有 Hooks 在 Fiber 节点上以 ‌单向链表‌ 形式存储,每个 Hook 包含:

type Hook = {
  memoizedState: any // 当前状态值(useState的值、useEffect的依赖等)
  baseState: any // 基础状态(用于优先级中断恢复)
  baseQueue: Update<any> | null // 未处理的更新队列
  queue: UpdateQueue<any> | null // 更新队列(useState/useReducer专用)
  next: Hook | null // 指向下一个 Hook
}

‌双阶段处理‌

每个 Hook 在 ‌mount 阶段‌ 和 ‌update 阶段‌ 有独立处理逻辑:

// 伪代码示例:处理组件函数
function renderWithHooks(Component, props) {
  if (currentFiber.memoizedState === null) {
    // Mount 阶段
    dispatcher = HooksDispatcherOnMount
  } else {
    // Update 阶段
    dispatcher = HooksDispatcherOnUpdate
  }
 
  ReactCurrentDispatcher.current = dispatcher
  const children = Component(props)
  // ...处理副作用链表
  return children
}

useReducer

React官网-useReducer

原理

useReducer 是 useState 的一种扩展,它允许你使用 reducer(纯函数)来根据前一个状态和 action 计算下一个状态。实际上,useState 内部就是调用了 useReducer(使用一个简单的状态更新函数)。

实现机制

挂载阶段

  • React 为该 hook 分配一个 hook 对象,将初始状态存入其中。
  • 初始化一个更新队列,用于存储后续的更新操作。

调用 dispatch 时:

  • 将 action 插入到该 hook 的更新队列(通常以循环链表的形式)。

更新阶段:

  • React 会遍历队列,依次调用 reducer 函数计算出新的状态。
  • 最后更新 hook 对象中的状态值。

useEffect 和 useLayoutEffect

React官网-useEffect

React官网-useLayoutEffect

这两个 hook 用来处理副作用。它们不会直接存储业务状态,而是在 Fiber 上记录 effect 描述信息。

  • useEffect:回调会在浏览器绘制之后异步执行。
  • useLayoutEffect:回调会在所有 DOM 变更完成后、浏览器绘制之前同步执行。

实现机制

渲染阶段:

  • 每次调用会将 effect 信息(包括回调函数、依赖数组等)添加到一个 effect 列表中。

commit 阶段:

  • 根据 effect 的类型,React 会依次调用这些回调函数,同时处理卸载和更新逻辑。

实际应用

这使得它适用于大多数不需要立即反映到 UI 上的副作用场景。

在组件加载时获取数据

useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data')
    const data = await response.json()
    setData(data)
  }
  fetchData()
}, [])

订阅外部数据源,如事件或 WebSockets

useEffect(() => {
  const subscription = fetchData().subscribe((data) => {
    // 异步处理数据
  })
  return () => subscription.unsubscribe() // 清理副作用
}, [])

父子组件的 useEffect 执行顺序

1、默认执行顺序

  • 子组件先执行:React 在父组件渲染完成后,会继续渲染子组件。子组件的 useEffect 会在其渲染完成后执行,然后父组件的 useEffect 才会执行。
  • DOM 更新保证:React 保证每次运行 useEffect 时,DOM 已经更新完毕。
import React, { useEffect } from 'react'
 
function ParentComponent() {
  useEffect(() => {
    console.log('Parent useEffect')
  }, [])
 
  return <ChildComponent />
}
 
function ChildComponent() {
  useEffect(() => {
    console.log('Child useEffect')
  }, [])
 
  return <div>Child Component</div>
}
 
// 输出
// Child useEffect
// Parent useEffect

2、特殊情况:父组件需要先执行 如果某些场景需要父组件的 useEffect 在子组件之前执行,可以考虑使用 useLayoutEffect。

  • 同步执行:useLayoutEffect 在 DOM 更新后同步执行,确保在浏览器绘制之前完成。
  • 父组件先执行:通过 useLayoutEffect,父组件可以在子组件之前执行某些逻辑。
import React, { useEffect, useLayoutEffect } from 'react'
 
function ParentComponent() {
  useLayoutEffect(() => {
    console.log('Parent useLayoutEffect')
  }, [])
 
  useEffect(() => {
    console.log('Parent useEffect')
  }, [])
 
  return <ChildComponent />
}
 
function ChildComponent() {
  useEffect(() => {
    console.log('Child useEffect')
  }, [])
 
  return <div>Child Component</div>
}
 
// 输出
// Parent useLayoutEffect
// Child useEffect
// Parent useEffect

useMemo 和 useCallback

React 默认会在父组件重新渲染时重新调用子组件的渲染函数,即使子组件没有任何 props。这种行为可以通过以下方法优化,以避免不必要的子组件重新渲染:

  • 使用 React.memo 来记忆函数式组件的输出。
  • 使用 useCallback 来记忆传递给子组件的函数引用。
  • 使用 PureComponent 或 shouldComponentUpdate 来控制类组件的重新渲染逻辑。

React官网-useMemo

React官网-useCallback

原理

这两个 hook 都用于性能优化:

  • useMemo:用来记忆计算结果,避免在每次渲染时都执行高代价的计算。
  • useCallback:实际上是对 useMemo 的语法糖,用来记忆函数引用。

实现机制

  • 渲染阶段:React 会保存上一次计算的值和依赖数组到对应的 hook 对象中(存储在 Fiber 的 hook 链表上)。
  • 更新阶段:当组件重新渲染时,会对比当前依赖数组和上一次的依赖数组,只有在依赖发生变化时才会重新计算并存储新值,否则复用旧值。
import React, { useState, memo } from 'react'
 
const ChildComponent = memo(() => {
  console.log('ChildComponent is called')
  return <div>Child Component</div>
})
 
const ParentComponent = () => {
  const [parentState, setParentState] = useState(0)
 
  return (
    <div>
      <ChildComponent />
      <button onClick={() => setParentState(parentState + 1)}>Increment</button>
    </div>
  )
}
 
export default ParentComponent

每次点击按钮 Increment 导致 parentState 改变时,ParentComponent 会重新渲染,但 ChildComponent 不会重新渲染,因为 React.memo 记忆了 ChildComponent 的输出。

import React, { useState, useCallback } from 'react'
 
const ChildComponent = ({ onClick }) => {
  console.log('ChildComponent is called')
  return <button onClick={onClick}>Child Button</button>
}
 
const ParentComponent = () => {
  const [parentState, setParentState] = useState(0)
  const handleClick = useCallback(() => {
    console.log('Button clicked')
  }, [])
 
  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <button onClick={() => setParentState(parentState + 1)}>Increment</button>
    </div>
  )
}
 
export default ParentComponent

可以使用 shouldComponentUpdate 或 PureComponent 来控制组件的重新渲染逻辑。

import React, { useState, PureComponent } from 'react'
 
class ChildComponent extends PureComponent {
  render() {
    console.log('ChildComponent is called')
    return <div>Child Component</div>
  }
}
 
const ParentComponent = () => {
  const [parentState, setParentState] = useState(0)
 
  return (
    <div>
      <ChildComponent />
      <button onClick={() => setParentState(parentState + 1)}>Increment</button>
    </div>
  )
}
 
export default ParentComponent

memo自定义比较函数 如果需要自定义比较逻辑,可以通过第二个参数传递一个比较函数。 这个函数接收两个参数:上一次的 props 和当前的 props。只有在该函数返回 false 时,组件才会重新渲染。

 
import React, { memo } from 'react';
 
const MyComponent = (props) => {
  console.log('MyComponent is called');
  return <div>{props.value}</div>;
};
 
// areEqual 函数会比较 prevProps.value 和 nextProps.value,如果相等则不会重新渲染组件。
const areEqual = (prevProps, nextProps) => {
  return prevProps.value === nextProps.value;
};
 
export default memo(MyComponent, areEqual);
 

使用 React.memo 的注意事项

  • 浅比较:默认情况下,React.memo 进行的是浅比较。如果 props 包含复杂的嵌套对象,浅比较可能无法正确判断是否需要重新渲染,此时可以使用自定义比较函数。
  • 副作用:在 React.memo 包装的组件内部,不应包含会产生副作用的代码(如网络请求、订阅等),这些副作用应该放在适当的生命周期钩子(如 useEffect)中。
  • 频繁变化的 props:对于一些频繁变化的 props,使用 React.memo 可能不会带来明显的性能提升,甚至可能产生额外的性能开销。

完整的例子

import React, { useState, memo } from 'react';
 
const ChildComponent = memo(({ value }) => {
  console.log('ChildComponent is called');
  return <div>{value}</div>;
});
 
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
 
  return (
    <div>
      <ChildComponent value={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
};
 
export default ParentComponent;
 

ChildComponent 使用 React.memo 包装,只会在 count 改变时重新渲染,而不会在 text 改变时重新渲染,从而提升性能。

useRef

React官网-useRef

原理

useRef 用于保存一个可变对象(通常为 { current: ... }),该对象在组件的整个生命周期内保持不变。更新 ref 的值不会触发重新渲染。

实现机制

  • 挂载阶段:React 创建一个对象并存储在 hook 对象中。
  • 返回的对象:在后续渲染中保持同一引用,允许直接修改 .current 属性来存储数据。

最常见的场景是存储对 DOM 元素的引用:

// 函数组件
const inputRef = useRef(null);
const focusInput = () => {
    if (inputRef.current) {
        inputRef.current.focus();
    }
};
return (
    <div>
        <input ref={inputRef} type="text" />
        <button onClick={focusInput}>Focus Input</button>
    </div>
);
 

保留组件状态

可以存储不想触发重新渲染的值。例如,存储定时器的 ID,或者在组件的不同生命周期中保留某些状态

const countRef = useRef(0)
const handleIncrement = () => {
  countRef.current += 1
  console.log(countRef.current)
}

useContext

React官网-useContext

原理

useContext 用于订阅 React 上下文(Context),当 Context 的值发生变化时,依赖它的组件会重新渲染。

是 React Hooks 中的一个重要 Hook,用于在组件树中共享数据,而无需通过每一层手动传递 props。

作用:

  • 跨组件层级共享数据‌:允许组件直接访问全局共享数据,无需通过 props 逐层传递,解决深层嵌套组件的传值问题13。
  • ‌简化状态管理‌:结合 useReducer 或全局状态,可实现轻量级的状态管理逻辑(如主题、用户信息、多语言配置等)26。
  • ‌订阅动态数据变化‌:当 Context.Provider 的 value 值变化时,使用 useContext 的组件会自动重新渲染,实现数据响应式更新

实现机制

  • 渲染阶段:useContext 会读取当前 Context 的值(通常来自最近的 Provider)。
  • 内部机制:依赖于 React 的调度机制,当 Provider 更新时,依赖该 Context 的组件会被标记为需要更新。

使用步骤:

  1. 创建 Context‌:使用 createContext 创建一个 Context 对象,并为其设置默认值(可选)。
  2. 提供 Context‌:在父组件中使用 <Context.Provider> 包裹子组件,并通过 value 属性传递数据。
  3. 消费 Context‌:在子组件中使用 useContext 钩子获取 Context 的值。
import React, { createContext, useState, useContext } from 'react'
 
// 1. 创建 Context(可以单独抽离出 ctx.ts文件)
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
})
 
function App() {
  const [theme, setTheme] = useState('light')
 
  const toggleTheme = () => {
    setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
  }
 
  // 2. 提供 Context(可以在父组件引入 ctx.ts 使用)
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className="App">
        <ThemedButton />
        <ThemedText />
      </div>
    </ThemeContext.Provider>
  )
}
 
// 3. 子组件使用 Context(可以在子组件引入 ctx.ts 使用)
function ThemedButton() {
  const { toggleTheme } = useContext(ThemeContext)
  return <button onClick={toggleTheme}>切换主题</button>
}
 
function ThemedText() {
  const { theme } = useContext(ThemeContext)
  return <p style={{ color: theme === 'light' ? 'black' : 'white' }}>当前主题:{theme}</p>
}
 
export default App

实际应用代码

全局登录状态管理

useContext 常用于管理全局状态,如用户认证信息、语言设置等

import React, { createContext, useContext, useState } from 'react';
 
const AuthContext = createContext();
 
function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
 
  const handleLogin = () => {
    setIsLoggedIn(true);
  };
 
  const handleLogout = () => {
    setIsLoggedIn(false);
  };
 
  return (
    <AuthContext.Provider value={{ isLoggedIn, handleLogin, handleLogout }}>
      <LoginComponent />
      <DashboardComponent />
    </AuthContext.Provider>
  );
}
 
function LoginComponent() {
  const { handleLogin } = useContext(AuthContext);
  // 显示登录表单,登录后调用 handleLogin
}
 
function DashboardComponent() {
  const { isLoggedIn } = useContext(AuthContext);
  // 显示仪表板内容,如果用户未登录则显示登录提示
}
 

应用级别的配置

useContext 可以用于存储应用级别的配置,如 API 端点、API 密钥等

 
import React, { createContext, useContext } from 'react';
 
const AppConfigContext = createContext({
  apiUrl: 'https://api.example.com',
  apiKey: 'your-api-key',
});
 
function App() {
  return (
    <AppConfigContext.Provider value={{ apiUrl: 'https://api.example.com', apiKey: 'your-api-key' }}>
      <ApiComponent />
    </AppConfigContext.Provider>
  );
}
 
function ApiComponent() {
  const { apiUrl, apiKey } = useContext(AppConfigContext);
  // 使用 apiUrl 和 apiKey 进行 API 调用
}
 

useImperativeHandle

React官网-useImperativeHandle

原理

这个 hook 用来在使用 ref 时自定义暴露给父组件的实例值。它通常和 forwardRef 配合使用。

实现机制

  • 渲染阶段:根据传入的工厂函数和依赖数组计算出一个对象,并将这个对象赋值给对应的 ref。
  • 更新阶段:当依赖变化时,React 会更新该对象,确保父组件拿到的是最新的引用。

useDebugValue

React官网-useDebugValue

原理

useDebugValue 主要用于开发阶段,在 React DevTools 中显示自定义的调试信息,对组件逻辑没有实际影响。

实现机制

  • 开发环境下:React 会将传入的调试值记录在 hook 对象中,供调试工具读取。
  • 生产环境下:通常会被优化掉,不产生任何额外开销。

简化实现:

// ================ 全局存储容器 ================
let hookStates = [] // 存储所有 Hook 状态
let hookIndex = 0 // 当前 Hook 索引指针
let scheduleUpdate // 调度更新函数
 
// 初始化调度器(模拟 React 的渲染流程)
function initScheduler(updateCallback) {
  scheduleUpdate = () => {
    hookIndex = 0 // 重置索引(关键!保证调用顺序)
    updateCallback() // 触发重新渲染
  }
}
 
// ================== useState ==================
function useState(initialState) {
  // 获取当前 Hook 状态(首次渲染初始化)
  const currentIndex = hookIndex
  hookStates[currentIndex] =
    hookStates[currentIndex] ?? (typeof initialState === 'function' ? initialState() : initialState)
 
  // 状态更新函数
  function setState(newState) {
    const prevState = hookStates[currentIndex]
    // 计算新状态(支持函数式更新)
    const nextState = typeof newState === 'function' ? newState(prevState) : newState
 
    // 状态变化时才更新
    if (!Object.is(prevState, nextState)) {
      hookStates[currentIndex] = nextState
      scheduleUpdate() // 触发重新渲染
    }
  }
 
  // 返回状态并移动索引
  return [hookStates[hookIndex++], setState]
}
 
// ================== useEffect ==================
function useEffect(callback, deps) {
  const currentIndex = hookIndex++
  const [prevCleanup, prevDeps] = hookStates[currentIndex] || [null, null]
 
  // 依赖比较逻辑
  const hasChanged = !deps || !prevDeps || deps.some((dep, i) => !Object.is(dep, prevDeps[i]))
 
  // 存储当前依赖
  hookStates[currentIndex] = [prevCleanup, deps]
 
  if (hasChanged) {
    // 异步执行(模拟 React 的 effect 执行时机)
    queueMicrotask(() => {
      // 执行清理函数
      if (typeof prevCleanup === 'function') prevCleanup()
      // 执行新 effect 并存储清理函数
      const cleanup = callback()
      if (typeof cleanup === 'function') {
        hookStates[currentIndex] = cleanup
      }
    })
  }
}
 
// ================== useCallback ==================
function useCallback(callback, deps) {
  const currentIndex = hookIndex++
  const [prevCallback, prevDeps] = hookStates[currentIndex] || [null, null]
 
  // 依赖比较
  const hasChanged = !deps || !prevDeps || deps.some((dep, i) => !Object.is(dep, prevDeps[i]))
 
  // 更新缓存
  const memoizedCallback = hasChanged ? callback : prevCallback
  hookStates[currentIndex] = [memoizedCallback, deps]
 
  return memoizedCallback
}
 
// ================== useRef ==================
function useRef(initialValue) {
  const currentIndex = hookIndex++
  // 持久化存储 ref 对象
  hookStates[currentIndex] = hookStates[currentIndex] || {
    current: initialValue ?? null,
  }
  return hookStates[currentIndex++]
}
 
// ================== useContext ==================
function useContext(context) {
  // 实际实现需要接入 Provider 的更新机制
  return context._currentValue
}
 
// ================== useReducer ==================
function useReducer(reducer, initialState, initFunc) {
  const currentIndex = hookIndex++
  // 初始化状态
  hookStates[currentIndex] =
    hookStates[currentIndex] ?? (initFunc ? initFunc(initialState) : initialState)
 
  function dispatch(action) {
    const prevState = hookStates[currentIndex]
    const nextState = reducer(prevState, action)
 
    if (!Object.is(prevState, nextState)) {
      hookStates[currentIndex] = nextState
      scheduleUpdate()
    }
  }
 
  return [hookStates[hookIndex++], dispatch]
}
 
// ================ 模拟渲染流程 ================
function render(Component) {
  let isMounted = false
  const root = document.getElementById('root')
 
  function update() {
    if (!isMounted) {
      console.log('--- Mount ---')
      isMounted = true
    } else {
      console.log('--- Update ---')
    }
 
    hookIndex = 0 // 重置索引
    const vdom = Component()
    root.innerHTML = ''
    root.appendChild(vdom)
  }
 
  initScheduler(update)
  update()
}

使用:

function Counter() {
  const [count, setCount] = useState(0)
  const [step, setStep] = useState(1)
 
  useEffect(() => {
    console.log('Count changed:', count)
    return () => console.log('Cleanup count:', count)
  }, [count])
 
  const increment = useCallback(() => {
    setCount((c) => c + step)
  }, [step])
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onclick={increment}>Add {step}</button>
      <button onclick={() => setStep((s) => s + 1)}>Increment Step (current: {step})</button>
    </div>
  )
}
 
// 启动渲染
render(Counter)

高阶函数

forwardRef

forwardRef 是一个高级函数,用于将父组件的引用(ref)转发到子组件中的 DOM 元素或另一个组件实例。

forwardRef 的作用

  • ‌穿透组件传递 ref‌:让父组件直接访问子组件内部的 DOM 元素或组件实例
  • ‌高阶组件(HOC)集成‌:在包裹组件时保持 ref 的传递链
  • ‌第三方库组件封装‌:对未暴露 ref 的第三方组件进行二次封装

使用场景

访问子组件 DOM 元素

import React, { forwardRef, useRef, useState } from 'react'
 
// 自定义输入框组件,封装了原生的 input 元素
const CustomInput = forwardRef((props, ref) => {
  return (
    <div className="relative">
      <input {...props} ref={ref} className="w-full rounded border border-gray-300 p-2" />
      <span className="absolute right-2 top-1/2 -translate-y-1/2 transform text-gray-400">
        粘贴
      </span>
    </div>
  )
})
 
function App() {
  const inputRef = useRef(null)
  const [value, setValue] = useState('')
 
  const handleClick = () => {
    if (inputRef.current) {
      inputRef.current.focus()
    }
  }
 
  return (
    <div className="p-4">
      <CustomInput
        ref={inputRef}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="请输入内容"
      />
      <button className="mt-2 rounded bg-blue-500 px-4 py-2 text-white" onClick={handleClick}>
        点击聚焦输入框
      </button>
    </div>
  )
}
 
export default App
  1. forwardRef 的使用:forwardRef 函数接受一个组件作为参数,并返回一个新的组件。这个组件可以接收一个额外的 ref 参数,该参数将被转发到组件内部的 DOM 元素或另一个组件实例。
  2. 访问 DOM 元素:在 App 组件中,我们使用 useRef 创建了一个引用 inputRef,并通过 ref={inputRef} 将其传递给 CustomInput 组件。在 CustomInput 组件内部,我们将这个引用转发给了原生的 input 元素。这样,父组件 App 就可以通过 inputRef.current 访问到 input 元素,并调用其方法(如 focus)。
  3. 组件封装:CustomInput 组件封装了原生的 input 元素,并添加了一些额外的样式和功能(如右侧的“粘贴”提示)。同时,通过 forwardRef,它允许父组件获取到内部的 input 元素。

高阶组件封装:

高阶组件(HOC)的

import React, { forwardRef, useState } from 'react'
 
// 高阶组件,为子组件添加日志功能
const withLog = (WrappedComponent) => {
  const EnhancedComponent = forwardRef((props, ref) => {
    const [logs, setLogs] = useState([])
 
    const logAction = (action) => {
      setLogs((prevLogs) => [...prevLogs, action])
    }
 
    return (
      <div>
        <h3>日志:</h3>
        <ul>
          {logs.map((log, index) => (
            <li key={index}>{log}</li>
          ))}
        </ul>
        <WrappedComponent {...props} ref={ref} logAction={logAction} />
      </div>
    )
  })
 
  return EnhancedComponent
}
 
// 被包裹的组件
const InputWithLog = forwardRef((props, ref) => {
  const handleChange = (e) => {
    props.logAction(`值已更改: ${e.target.value}`)
  }
 
  return (
    <input
      {...props}
      ref={ref}
      onChange={handleChange}
      className="w-full rounded border border-gray-300 p-2"
    />
  )
})
 
// 使用 HOC 包裹组件
const EnhancedInput = withLog(InputWithLog)
 
function App() {
  const inputRef = useRef(null)
 
  return (
    <div className="p-4">
      <EnhancedInput ref={inputRef} placeholder="请输入内容" />
    </div>
  )
}
 
export default App
  • 高阶组件(HOC):withLog 是一个 HOC,它接收一个组件 WrappedComponent 作为参数,并返回一个新的组件 EnhancedComponent。
  • 转发引用:在 EnhancedComponent 中,使用 forwardRef 将父组件传递的引用转发给 WrappedComponent。这样,父组件可以通过引用访问到 WrappedComponent 的实例。
  • 日志功能:HOC 为 WrappedComponent 添加了日志功能,记录输入框值的变化。

综合示例:

import React, { forwardRef, useRef, useImperativeHandle } from 'react';
 
// 子组件(暴露DOM和方法)
interface ChildProps {}
interface ChildHandle {
  triggerAnimation: () => void;
}
 
const ChildComponent = forwardRef<ChildHandle, ChildProps>((props, ref) => {
  const divRef = useRef<HTMLDivElement>(null);
 
  // 暴露特定方法给父组件
  useImperativeHandle(ref, () => ({
    triggerAnimation: () => {
      if (divRef.current) {
        divRef.current.style.transform = 'scale(1.1)';
        setTimeout(() => {
          divRef.current!.style.transform = 'scale(1)';
        }, 500);
      }
    }
  }));
 
  return (
    <div ref={divRef} className="child-box">
      子组件内容
    </div>
  );
});
 
// 父组件
function Parent() {
  const childRef = useRef<ChildHandle>(null);
 
  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={() => childRef.current?.triggerAnimation()}>
        触发动画
      </button>
    </div>
  );
}
 
 
  • 避免在函数组件中直接使用 ref 属性(必须通过 forwardRef)
  • 优先通过 props 通信,仅在必须操作 DOM 或子组件实例时使用
  • 类组件可直接通过 createRef 获取实例方法
← Previous postJavaScript夯实基础
Next post →Vue夯实基础