React 常见问题

React 组件生命周期

  • 挂载阶段:组件首次被创建并插入到 DOM 中的阶段。
  • 更新阶段:当组件的 props 或 state 发生变化时,就会触发更新阶段。
  • 卸载阶段:组件从 DOM 中移除时进入卸载阶段。
    函数组件是没有明确的生命周期方法,但可以通过 useEffect 来模拟生命周期行为。
    模拟挂载阶段的生命周期方法:
  • 只需要在 useEffect 的依赖数组中传入一个空数组 []。这样,该副作用只会在组件挂载后运行一次。
js 复制代码
useEffect(() => {
  console.log('代码只会在组件挂载后执行一次')
}, [])
  • 通过将依赖项放入依赖数组中,useEffect 可以在依赖项更改时执行。如果你省略了依赖数组,副作用将在每次渲染后执行。
js 复制代码
// 注意这里没有提供依赖数组
useEffect(() => {
  console.log('代码会在组件挂载后以及每次更新后执行')
})
// 特定依赖更新时执行
useEffect(() => {
  console.log('代码会在 count 更新后执行')
}, [count])
  • useEffect 的函数中返回一个函数,该函数会在组件卸载前执行。
js 复制代码
useEffect(() => {
  return () => {
    console.log('代码会在组件卸载前执行')
  }
}, [])

正常行为 :当 useEffect 的依赖数组为空( [] )时,清理函数只会在 组件卸载时 执行一次

开发环境特殊行为 :在React严格模式下(通常在开发环境启用),React会故意执行两次副作用函数和清理函数:

  • 第一次:组件挂载 → 执行副作用 → 立即执行清理函数(模拟卸载)
  • 第二次:重新挂载 → 执行副作用 → 正常卸载时执行清理函数
    这种双重执行是为了帮助开发者检测副作用中的问题,生产环境不会有此行为

React 父子组件生命周期调用顺序

  • 挂载阶段
  1. 父组件:执行函数体(首次渲染)
  2. 子组件:执行函数体(首次渲染)
  3. 子组件:useEffect(挂载阶段)
  4. 父组件:useEffect(挂载阶段)

原因:React先递归渲染父组件→子组件,完成DOM更新后,再从子到父执行effect回调。

  • 更新阶段
  1. 父组件:执行函数体(重新渲染)
  2. 子组件:执行函数体(重新渲染)
  3. 子组件:useEffect清理函数(依赖变化时)
  4. 父组件:useEffect清理函数(依赖变化时)
  5. 子组件:useEffect(依赖变化时)
  6. 父组件:useEffect(依赖变化时)

原因:更新时先完成所有组件的重新渲染,再按「子→父」顺序执行清理函数,最后按同样顺序执行新的effect。

  • 卸载阶段
  1. 子组件:useEffect清理函数
  2. 父组件:useEffect清理函数
js 复制代码
import { useState, useEffect } from 'react';
import './App.css';

// 子组件
const ChildComponent = () => {
  console.log('子组件:执行函数体(渲染)');

  useEffect(() => {
    console.log('子组件:useEffect(挂载阶段)');
    return () => {
      console.log('子组件:useEffect清理函数(卸载阶段)');
    };
  }, []);

  useEffect(() => {
    console.log('子组件:useEffect(依赖变化 - 更新阶段)');
    return () => {
      console.log('子组件:useEffect清理函数(依赖变化 - 更新阶段)');
    };
  }, [/* 依赖项 */]);

  return <div>子组件</div>;
};

// 父组件
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [showChild, setShowChild] = useState(true);

  console.log('父组件:执行函数体(渲染)');

  useEffect(() => {
    console.log('父组件:useEffect(挂载阶段)');
    return () => {
      console.log('父组件:useEffect清理函数(卸载阶段)');
    };
  }, []);

  useEffect(() => {
    console.log('父组件:useEffect(依赖变化 - 更新阶段)');
    return () => {
      console.log('父组件:useEffect清理函数(依赖变化 - 更新阶段)');
    };
  }, [count]);

  return (
    <div className="parent">
      <h2>父组件</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>增加计数(触发更新)</button>
      <button onClick={() => setShowChild(false)}>卸载子组件</button>
      {showChild && <ChildComponent />}
    </div>
  );
};

function App() {
  return (
    <>
      <h1>React生命周期演示</h1>
      <ParentComponent />
    </>
  );
}

export default App;

执行结果:
挂载:
父组件:执行函数体(渲染)
子组件:执行函数体(渲染)
子组件:useEffect(挂载阶段)
子组件:useEffect(依赖变化 - 更新阶段)
父组件:useEffect(挂载阶段)
父组件:useEffect(依赖变化 - 更新阶段)
更新:
父组件:执行函数体(渲染)
子组件:执行函数体(渲染)
父组件:useEffect清理函数(依赖变化 - 更新阶段)
父组件:useEffect(依赖变化 - 更新阶段)
卸载:
父组件:执行函数体(渲染)
子组件:useEffect清理函数(卸载阶段)
子组件:useEffect清理函数(依赖变化 - 更新阶段)

React 组件通讯方式

  • 父组件传递给子组件:通过props向子组件传递数据
js 复制代码
//父组件
const Parent = () => {
  const message = 'Hello from Parent'
  return <Child message={message} />
}

// 子组件
const Child = ({ message }) => {
  return <div>{message}</div>
}
  • 子组件传递给父组件:父组件给子组件一个方法,子组件调用父组件的方法
js 复制代码
//父组件
const Parent = () => {
  const handleData = (data) => {
    console.log('Data from Child:', data)
  }
  return <Child onSendData={handleData} />
}

// 子组件
const Child = ({ onSendData }) => {
  return <button onClick={() => onSendData('Hello from Child')}>Send Data</button>
}
  • 使用refs调用子组件暴露的方法
js 复制代码
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
// 子组件
// 
const Child = forwardRef((props, ref) => {
  // 暴露方法给父组件
  useImperativeHandle(ref, () => ({
    sayHello() {
      alert('Hello from Child Component!')
    },
  }))
  return <div>Child Component</div>
})

// 父组件
function Parent() {
  const childRef = useRef(null)
  const handleClick = () => {
    if (childRef.current) {
      childRef.current.sayHello()
    }
  }
  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleClick}>Call Child Method</button>
    </div>
  )
}
export default Parent

### forwardRef 的作用
- 允许父组件将 ref 传递给子组件内部的 DOM 元素或组件实例
- 打破了默认情况下函数组件无法接收 ref 属性的限制
- 在代码中表现为: forwardRef((props, ref) => {...}) ,使子组件能接收第二个 ref 参数
### useImperativeHandle 的作用
- 自定义通过 ref 暴露给父组件的实例值
- 避免将子组件的完整实例暴露给父组件,只选择性暴露需要的方法
- 在代码中通过 useImperativeHandle(ref, () => ({...})) 定义了暴露给父组件的 sayHello 方法
  • 通过Context进行跨组件通信
js 复制代码
import React, { useState } from 'react'
// 创建一个 Context
const MyContext = React.createContext()
// 父组件
function Parent() {
  const [sharedData, setSharedData] = useState('Hello from Context')
  const updateData = () => {
    setSharedData('Updated Data from Context')
  }
  return (
    // 提供数据和更新函数
    <MyContext.Provider value={{ sharedData, updateData }}>
      <ChildA />
    </MyContext.Provider>
  )
}
// 子组件 A(引用子组件 B)
function ChildA() {
  return (
    <div>
      <ChildB />
    </div>
  )
}
// 子组件 B(使用 useContext)
function ChildB() {
  const { sharedData, updateData } = React.useContext(MyContext)
  return (
    <div>
      <div>ChildB: {sharedData}</div>
      <button onClick={updateData}>Update Data</button>
    </div>
  )
}
export default Parent

使用状态管理库进行通信

  • React Context + useReducer
js 复制代码
import React, { useReducer } from 'react'
const initialState = { count: 0 }
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}
const CounterContext = React.createContext()

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState)
  return <CounterContext.Provider value={{ state, dispatch }}>{children}</CounterContext.Provider>
}

function Counter() {
  const { state, dispatch } = React.useContext(CounterContext)
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  )
}
function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  )
}
export default App
  • Redux Toolkit
js 复制代码
import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
  },
})

const { increment, decrement } = counterSlice.actions

const store = configureStore({
  reducer: counterSlice.reducer
})
// 监听数据变化
store.subscribe(() => console.log(store.getState()))

store.dispatch(increment())
store.dispatch(decrement())
  • Event Bus
js 复制代码
import React from 'react'
import PubSub from 'pubsub-js'
const ParentComponent = () => {
  React.useEffect(() => {
    const token = PubSub.subscribe('childData', (msg, data) => {
      console.log('Received data from child:', data)
    })
    return () => {
      PubSub.unsubscribe(token)
    }
  }, [])
  return <ChildComponent />
}
const ChildComponent = () => {
  const sendData = () => {
    PubSub.publish('childData', { message: 'Hello from child' })
  }
  return <button onClick={sendData}>Send data from child</button>
}
export default ParentComponent

state 和 props 有什么区别?

在 React 中,props 和 state 都用于管理组件的数据和状态。
Props(属性):

props 是组件之间传递数据的一种方式,用于从父组件向子组件传递数据。 props 是只读的,即父组件传递给子组件的数据在子组件中不能被修改。 props 是在组件的声明中定义,通过组件的属性传递给子组件。 props 的值由父组件决定,子组件无法直接改变它的值。 当父组件的 props 发生变化时,子组件会重新渲染。
State(状态):

state 是组件内部的数据,用于管理组件的状态和变化。 state 是可变的,组件可以通过 setState 方法来更新和修改 state。 state 是在组件的构造函数中初始化的,通常被定义为组件的类属性。 state 的值可以由组件自身内部改变,通过调用 setState 方法触发组件的重新渲染。 当组件的 state 发生变化时,组件会重新渲染。

React 有哪些内置 Hooks ?

  1. 状态管理 Hooks
  • useState: 用于在函数组件中添加局部状态。
  • useReducer: 用于管理复杂的状态逻辑,类似于 Redux 的 reducer。
  1. 副作用 Hooks
  • useEffect: 用于在函数组件中执行副作用操作(如数据获取、订阅、手动 DOM 操作等)。
  • useLayoutEffect: 与 useEffect 类似,但在 DOM 更新后同步执行,适用于需要直接操作 DOM 的场景。
  1. 上下文 Hooks
  • useContext: 用于访问 React 的上下文(Context)。
  1. 引用 Hooks
  • useRef: 用于创建一个可变的引用对象,通常用于访问 DOM 元素或存储可变值。
  1. 性能优化 Hooks
  • useMemo: 用于缓存计算结果,避免在每次渲染时都重新计算。
    接收两个参数:计算函数和依赖数组,只有当依赖数组中的值发生变化时,才会重新执行计算函数并更新缓存,否则直接返回之前缓存的计算结果
js 复制代码
const memoizedValue = useMemo(() => {
  // 昂贵的计算逻辑
  return computeExpensiveValue(a, b);
}, [a, b]); // 只有 a 或 b 变化时才重新计算
  • useCallback: 缓存函数引用 ,避免在每次组件渲染时都创建新的函数实例。
js 复制代码
  const handleClick = useCallback(() => {
    setCount(count + 1)
  }, [count]) // 只有 count 变化,才会重新定义函数

useEffect 和 useLayoutEffect 的区别

1. 执行时机

  • useEffect :
    • 执行时机: 在浏览器完成绘制(即 DOM 更新并渲染到屏幕)之后异步执行。
    • 适用场景: 适用于大多数副作用操作,如数据获取、订阅、手动 DOM 操作等,因为这些操作通常不需要阻塞浏览器的渲染。
  • useLayoutEffect :
    • 执行时机: 在 DOM 更新之后,但在浏览器绘制之前同步执行。
    • 适用场景: 适用于需要在浏览器绘制之前同步执行的副作用操作,如测量 DOM 元素、同步更新 DOM 等。由于它是同步执行的,可能会阻塞浏览器的渲染,因此应谨慎使用。

2. 对渲染的影响

  • useEffect :
    • 由于是异步执行,不会阻塞浏览器的渲染过程,因此对用户体验的影响较小。
    • 如果副作用操作导致状态更新,React 会重新渲染组件,但用户不会看到中间的闪烁或不一致的状态。
  • useLayoutEffect :
    • 由于是同步执行,会阻塞浏览器的渲染过程,直到副作用操作完成。
    • 如果副作用操作导致状态更新,React 会立即重新渲染组件,用户可能会看到中间的闪烁或不一致的状态。

3. 总结

  • useEffect: 异步执行,不阻塞渲染,适合大多数副作用操作。
  • useLayoutEffect: 同步执行,阻塞渲染,适合需要在绘制前同步完成的副作用操作。

为何 dev 模式下 useEffect 执行两次

React的 <React.StrictMode> 组件在开发环境中会执行 双重渲染 (mount → unmount → remount),这会导致 useEffect 的回调函数和清理函数各执行一次。这种设计是为了:

  • 检测副作用泄漏 :例如忘记在清理函数中取消订阅、清除定时器或释放资源
  • 暴露不稳定的依赖项 :识别因依赖数组设置不当导致的bug
    促使开发者遵循React的最佳实践,确保副作用可预测且可清理

React 闭包陷阱

js 复制代码
function Counter() {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count) // 每次打印的都是初始值 0
    }, 1000)
    return () => clearInterval(timer)
  }, []) // 依赖数组为空,effect 只运行一次
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

在这个例子中:

  • useEffect 只在组件挂载时运行一次。
  • setInterval 的回调函数形成了一个闭包,捕获了初始的 count 值(即 0)。
  • 即使 count 状态更新了,setInterval 中的回调函数仍然访问的是旧的 count 值。
    解决方法:
js 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log(count) // 每次打印最新的 count 值
  }, 1000)

  return () => clearInterval(timer)
}, [count]) // 将 count 添加到依赖数组

React state 不可变数据

状态(state)的不可变性:指不能直接修改状态的值,而是需要创建一个新的值来替换旧的状态。 使用不可变数据可以带来如下好处:

  1. 性能优化
    React 使用浅比较(shallow comparison)来检测状态是否发生变化。如果状态是不可变的,React 只需要比较引用(即内存地址)是否变化,而不需要深度遍历整个对象或数组。
  2. 可预测性 每次状态更新都会生成一个新的对象或数组,这样可以更容易地调试和追踪状态的变化历史。
js 复制代码
// ❌ 错误:直接修改状态
state.name = 'new name'
setState(state)
// ✅ 正确:创建新对象
setState({
  ...state, // 复制旧状态
  name: 'new name', // 更新属性
})

// ❌ 错误:直接修改数组
state.items.push(newItem)
setState(state)
// ✅ 正确:创建新数组
setState({
  ...state,
  items: [...state.items, newItem], // 添加新元素
})

React state 异步更新!!!

在 React 18 之前,React 采用批处理策略来优化状态更新。在批处理策略下,React 将在事件处理函数结束后应用所有的状态更新,这样可以避免不必要的渲染和 DOM 操作。

然而,这个策略在异步操作中就无法工作了。因为 React 没有办法在适当的时机将更新合并起来,所以结果就是在异步操作中的每一个状态更新都会导致一个新的渲染。

例如,当你在一个 onClick 事件处理函数中连续调用两次 setState,React 会将这两个更新合并,然后在一次重新渲染中予以处理。

然而,在某些场景下,如果你在事件处理函数之外调用 setState,React 就无法进行批处理了。比如在 setTimeout 或者 Promise 的回调函数中。在这些场景中,每次调用 setState,React 都会触发一次重新渲染,无法达到批处理的效果。

React 18 引入了自动批处理更新机制,让 React 可以捕获所有的状态更新,并且无论在何处进行更新,都会对其进行批处理。这对一些异步的操作,如 Promise,setTimeout 之类的也同样有效。

这一新特性的实现,核心在于 React 18 对渲染优先级的管理。React 18 引入了一种新的协调器,被称为"React Scheduler"。它负责管理 React 的工作单元队列。每当有一个新的状态更新请求,React 会创建一个新的工作单元并放入这个队列。当 JavaScript 运行栈清空,Event Loop 即将开始新的一轮循环时,Scheduler 就会进入工作,处理队列中的所有工作单元,实现了批处理。

React 项目可做哪些性能优化

  1. useMemo: 用于缓存昂贵的计算结果,避免在每次渲染时重复计算。
js 复制代码
function ExpensiveComponent({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter((item) => item.includes(filter))
  }, [items, filter]) // 仅在 items 或 filter 变化时重新计算

  return (
    <ul>
      {filteredItems.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}
  1. useCallback: 用于缓存回调函数,避免在每次渲染时创建新的函数实例。 useCallback
js 复制代码
function ParentComponent() {
  const [count, setCount] = useState(0)

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1)
  }, []) // 空依赖数组,函数不会重新创建

  return (
    <div>
      <ChildComponent onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  )
}

const ChildComponent = React.memo(({ onClick }) => {
  console.log('ChildComponent rendered')
  return <button onClick={onClick}>Click me</button>
})
  1. React.memo: 是一个高阶组件,用于缓存组件的渲染结果,避免在 props 未变化时重新渲染
js 复制代码
const MyComponent = React.memo(({ value }) => {
  console.log('MyComponent rendered')
  return <div>{value}</div>
})

function ParentComponent() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <MyComponent value="Hello" /> {/* 不会因 count 变化而重新渲染 */}
    </div>
  )
}
  1. Suspense: 用于在异步加载数据或组件时显示加载状态,可以减少初始加载时间,提升用户体验
js 复制代码
import React, { Suspense, lazy } from 'react';
// 懒加载组件(代码分割)
const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 加载期间显示 fallback */}
      <LazyComponent />
    </Suspense>
  );
}
  1. 路由懒加载:通过动态导入(dynamic import)将路由组件拆分为单独的代码块,按需加载。可以减少初始加载的代码量,提升页面加载速度
js 复制代码
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import React, { Suspense } from 'react'

const Home = React.lazy(() => import('./Home'))
const About = React.lazy(() => import('./About'))

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  )
}

如何统一监听 React 组件报错

  1. Error Boundaries(错误边界)

React 中 JSX 的本质是什么

JSX 本质上是 JavaScript 的语法扩展,允许开发者在 JavaScript 代码中直接编写类似 HTML 的标记结构。 在 React 中,JSX 并非直接被浏览器解析执行,而是需要通过 Babel 等转译工具转换为标准的 JavaScript 函数调用。

js 复制代码
<div className="app">Hello JSX</div>
会被转换为:
React.createElement(
  'div',
  { className: 'app' },
  'Hello JSX'
)
最终目的是创建 React 元素(虚拟 DOM 节点)。

编译与工具链

通过 Babel 编译为浏览器可执行的 JavaScript

js 复制代码
// .babelrc
{
  "presets": ["@babel/preset-react"]
}

React Fiber

Fiber是一个JavaScript对象,代表React的一个工作单元,它包含了与组件相关的信息。

js 复制代码
{
  type: 'h1',  // 组件类型
  key: null,   // React key
  props: { ... }, // 输入的props
  state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
  child: Fiber | null,  // 第一个子元素的Fiber
  sibling: Fiber | null,  // 下一个兄弟元素的Fiber
  return: Fiber | null,  // 父元素的Fiber
  // ...其他属性
}

React开始工作时 -> 沿着Fiber树形结构进行 -> 完成每个Fiber的工作(比较新旧props,确定是否需要更新组件等)

如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。

因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。

  • 为什么需要 Fiber
    在React 16之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和,这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。
  • Fiber工作原理
    Fiber工作原理中最核心的点就是:可以中断恢复 ,这个特性增强了React的并发性和响应性。
    实现可中断和恢复的原因就在于:Fiber的数据结构里提供的信息让React可以追踪工作进度管理调度同步更新到DOM
    Fiber工作原理中的几个关键点:
  1. 单元工作 每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树,这种结构让React可以细粒度控制节点的行为。
  2. 链接属性 childsiblingreturn 字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。
  3. 双缓冲技术: React在更新时,会根据现有的Fiber树(Current Tree )创建一个新的临时树(Work-in-progress (WIP) Tree )WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备拥有优秀的渲染性能和UI的稳定性。
  4. State 和 Props
    React保存 memoizedProps(上一次的props值)pendingProps(新的props值) 进行浅比较, 当比较结果为不相等时,ChildComponent才会重新渲染.
    memoizedState(useState 钩子返回的状态) 每次调用setState时,React会将新状态与memoizedState中的旧状态进行比较如果状态发生变化,才会触发组件重新渲染。
  5. 副作用的追踪 flags(单个Fiber节点需要执行的副作用(如DOM更新、生命周期用))subtreeFlags(当前Fiber节点及其子树中存在需要处理的副作用)
js 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>计数器: {count}</h1>
      <Child count={count} />
      <button onClick={() => setCount(c => c + 1)}>加1</button>
    </div>
  );
}

function Child({ count }) {
  return <p>子组件显示: {count}</p>;
}
/* 
执行流程:
1. 点击按钮 → count 状态更新
2. 协调阶段:
- Parent组件Fiber标记 Update flag
- Parent的 subtreeFlags 标记为 HasEffect (因为子组件依赖count)
- Child组件Fiber也标记 Update flag
3. 提交阶段 :React一次性处理所有标记:
- 更新Parent的h1文本
- 更新Child的p文本
- 不会逐一遍历整个DOM树,而是直接处理标记的Fiber节点

先标记所有需要配送的包裹(协调阶段标记flags),再一次性配送(提交阶段处理副作用),比逐个处理效率更高。
*/

React在协调阶段(Reconciliation)会遍历组件树标记这些flags,然后在提交阶段(Commit)一次性处理所有标记的副作用,避免频繁DOM操作提升性能。

  • Fiber工作流程
    • 第一阶段:Reconciliation(调和)
  • 目标: 确定哪些部分的UI需要更新。
  • 原理: 这是React构建工作进度树的阶段,会比较新的props和旧的Fiber树来确定哪些部分需要更新。
  • 工作原理:
  • 构建workInProgress树 :当组件状态/属性变化时,React会创建新的Fiber树(workInProgress树)
  • 差异比较 :与当前DOM对应的current树进行对比
  • 标记副作用 :记录需要执行的DOM操作(插入/更新/删除等)
js 复制代码
class ClickCounter extends React.Component {
  state = { count: 0 };
  handleClick = () => this.setState({ count: this.state.count + 1 });
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Update counter</button>
        <span>{this.state.count}</span>
      </div>
    );
  }
}
Reconciliation阶段具体过程 :

1.初始渲染 :创建包含button和span的Fiber树
2.用户点击按钮 :setState触发更新
3.协调过程 :
   - 比较新旧Fiber树的根节点(div)- 类型相同,继续比较子节点
   - 比较button元素 - props未变(onClick引用相同),无需更新
   - 比较span元素 - 文本内容从"0"变为"1",标记为需要更新的副作用
4.准备提交 :将span的文本更新操作记录到副作用队列

为什么Fiber架构更快?

  1. 增量渲染:避免主线程阻塞 旧版Stack Reconciler采用递归同步渲染,一旦开始就无法中断。在计数器案例中,若同时存在其他复杂计算,会导致主线程被长时间占用,造成点击按钮后卡顿 。
    Fiber将协调阶段拆分为 小任务单元 (每个Fiber节点对应一个任务),通过 requestIdleCallback 在浏览器空闲时执行。例如更新计数器时,Fiber可以:
  • 先处理button和span的差异比较
  • 若时间片用尽,暂停并保存当前进度
  • 后续空闲时恢复执行,最终标记span的文本更新操作
  1. 优先级调度 Fiber为不同任务分配优先级, 用户输入(如点击按钮)被标记为高优先级 ,可中断低优先级任务(如列表渲染) 。
  • 点击按钮触发的状态更新属于高优先级任务
  • Fiber会优先完成该任务的协调过程(标记span的更新)
  • 确保用户点击后能立即看到计数器变化,避免延迟感
  1. 可中断的协调过程 Fiber通过 链表结构 (child/sibling/return指针)追踪任务进度,而非递归调用栈。当浏览器需要响应用户输入或执行动画时,Fiber可以:
  • 1.暂停当前协调任务

  • 2.保存当前Fiber节点的处理状态

  • 3.优先执行高优先级任务

  • 4.恢复协调过程并完成剩余工作

    • 第二阶段 Commit(提交) 在计数器案例中,当Reconciliation阶段标记完span元素需要更新后,React进入 不可中断的Commit阶段 ,通过三个子步骤将变更应用到DOM并执行副作用:
  1. BeforeMutation阶段:DOM更新前准备
  • 操作 :遍历副作用列表,执行DOM更新前的准备工作
  • 案例行为 :
    • 检查span元素的DOM状态(如当前文本内容)
    • 调用 getSnapshotBeforeUpdate 生命周期方法(若组件中定义)
  • 目的 :为后续DOM操作收集必要信息,如旧DOM属性值
  1. CommitMutation阶段:执行DOM更新
  • 操作 :遍历副作用列表,执行实际DOM操作
  • 案例行为 :
js 复制代码
// 副作用列表中的span元素更新任务
effect = {
  type: 'UPDATE_TEXT',
  target: spanDOMNode,
  payload: { textContent: '1' } // Reconciliation阶段计算的新值
}
// 执行DOM更新
spanDOMNode.textContent = '1'

关键特性 :此阶段 同步执行且不可中断 ,确保DOM状态一致性 3. commitLayout阶段:执行副作用与生命周期

  • 操作 :处理布局相关副作用,调用生命周期方法
  • 案例行为 :
    • 调用 componentDidUpdate 生命周期方法(若组件中定义)
    • 更新refs(若有相关定义)
    • 执行 useLayoutEffect 钩子(若使用)
  • 目的 :在DOM更新后但浏览器绘制前完成布局相关操作

与旧架构的核心差异

特性 Stack Reconciler(旧架构) Fiber架构
执行方式 递归同步执行整个更新流程 协调阶段可中断,提交阶段同步执行
副作用处理 边Diff边执行DOM操作 先收集副作用,再批量执行
用户体验 复杂场景下可能卡顿 确保DOM更新过程流畅无中断

总结 :在计数器案例中,Commit阶段通过批量执行副作用 (仅更新span元素)和同步不可中断的特性,确保用户点击按钮后能立即看到数字变化,同时避免了中间状态导致的视觉闪烁

为何 React 需要 Fiber 而 Vue 不需要

全树重渲染风险 React 组件更新默认触发 整个组件树的递归重渲染 (即使状态变化局限于深层子组件)。例如计数器案例中,父组件状态更新会导致所有子组件重新执行 render 方法 1 。

  • 旧版 Stack Reconciler 采用同步递归,若组件树深度过深(如 1000+ 层),会阻塞主线程超过 16ms,导致页面卡顿。
  • Fiber 将递归改为 可中断的链表遍历 ,支持时间切片(每处理一个 Fiber 节点检查剩余时间),避免长时间阻塞 。
维度 React (Fiber) Vue
更新触发 状态变化 → 组件重渲染 → 全树 Diff 数据变化 → 依赖组件精准更新
优化策略 运行时调度(时间切片、优先级) 编译时分析 + 响应式追踪
适用场景 大型复杂应用、并发交互 中小型应用、快速开发
核心目标 灵活性 + 可中断渲染 易用性 + 精准高效更新
  • React 需要 Fiber:因其函数式组件模型导致更新范围不可控,需通过 Fiber 实现复杂调度来保障性能。
  • Vue 无需 Fiber:通过响应式精准更新和编译时优化,已将更新成本降到最低,同步执行即可满足性能需求。

两者选择了不同的优化路径,但殊途同归------在各自的设计约束下实现高效渲染。Vue 若未来支持类似 Concurrent Mode 的特性,可能也会引入调度机制,但目前架构已足够高效。

useEffect 的底层是如何实现的(美团)

useEffect 的底层实现基于 React 的 Fiber 架构和 Hooks 链表系统,核心流程分为以下阶段:

1. Hook 节点创建与存储

  • 调用 useEffect 时,会创建包含以下信息的 Effect Hook 节点

    javascript 复制代码
    {
      tag: HookType.EFFECT, // 标记为副作用 Hook
      create: effectCallback, // 用户传入的副作用函数
      destroy: undefined, // 清理函数的占位符
      deps: depsArray, // 依赖数组
      next: null // 指向下一个 Hook 的指针
    }
  • 节点被添加到当前组件的 Hooks 链表中,由 的 memoizedState 管理。

2. 副作用标记与调度

  • React 会为 Effect Hook 打上 Passive 优先级标记 ,并将其加入 副作用队列
  • 在 Fiber 架构的 commit 阶段 (DOM 更新后),通过 commitPassiveEffects 统一执行副作用。

3. 依赖比较与执行逻辑

  • 首次挂载 :直接执行 create 回调,并将返回的清理函数存入 destroy 属性。
  • 更新阶段
    1. 对新旧依赖数组进行 浅比较Object.is
    2. 若依赖变化或无依赖数组,执行旧的 destroy 清理函数
    3. 执行新的 create 回调并更新 destroy

4. 清理机制

  • 清理函数的执行时机:
    • 组件卸载时通过 commitHookEffectListUnmount 执行所有 Effect 的清理
    • 组件更新时在新副作用执行前执行旧清理函数
  • 清理函数通过闭包捕获当前渲染周期的状态和属性

5. 与 Fiber 架构的协作

  • useEffect 的异步执行通过 调度优先级 实现,低优先级副作用可能被浏览器事件打断,通过 Scheduler 包的 scheduleCallback 延迟执行
  • useLayoutEffect 的核心区别:useEffect 属于 Passive Effect,在浏览器重绘后异步执行;useLayoutEffect 在 DOM 更新后同步执行

关键源码逻辑(简化版)

javascript 复制代码
function commitPassiveEffects(finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    // 遍历 Effect 链表
    for (let effect = lastEffect.next; effect !== lastEffect; effect = effect.next) {
      const destroy = effect.destroy;
      if (destroy !== undefined) {
        destroy(); // 执行清理
      }
      const create = effect.create;
      effect.destroy = create(); // 执行副作用并存储新清理函数
    }
  }
}

这种设计既保证了副作用与 DOM 更新的正确顺序,又通过依赖比较实现了性能优化,是 React 函数式组件管理副作用的核心机制。

相关推荐
程序视点43 分钟前
Escrcpy 3.0投屏控制软件使用教程:无线/有线连接+虚拟显示功能详解
前端·后端
silent_missile1 小时前
element-plus穿梭框transfer的调整
前端·javascript·vue.js
专注VB编程开发20年1 小时前
OpenXml、NPOI、EPPlus、Spire.Office组件对EXCEL ole对象附件的支持
前端·.net·excel·spire.office·npoi·openxml·spire.excel
古蓬莱掌管玉米的神1 小时前
coze娱乐ai换脸
前端
GIS之路1 小时前
GeoTools 开发合集(全)
前端
咖啡の猫1 小时前
Shell脚本-嵌套循环应用案例
前端·chrome
一点一木1 小时前
使用现代 <img> 元素实现完美图片效果(2025 深度实战版)
前端·css·html
萌萌哒草头将军2 小时前
🚀🚀🚀 告别复制粘贴,这个高效的 Vite 插件让我摸鱼🐟时间更充足了!
前端·vite·trae
布列瑟农的星空2 小时前
大话设计模式——关注点分离原则下的事件处理
前端·后端·架构
yvvvy2 小时前
前端必懂的 Cache 缓存机制详解
前端