前端:最新 react 原理 对比 面试题!!! 2025

主要内容为react原理hooks,以及部分react 和 vue的对比,比较重要的内容会有写出代码,You can keep watching if you need to ,OR NO 出门右转,内容持续更新中,后续会收集或遇到好的问题...


react 中的不同优先级

javascript 复制代码
// 不同的优先级级别
const priorityLevels = {
  ImmediatePriority: 1,    // 最高优先级
  UserBlockingPriority: 2, // 用户阻塞级
  NormalPriority: 3,       // 普通优先级
  LowPriority: 4,         // 低优先级
  IdlePriority: 5         // 空闲优先级
};

// React 源码中的优先级定义
export const NoPriority = 0;          // 无优先级
export const ImmediatePriority = 1;   // 立即执行
export const UserBlockingPriority = 2; // 用户阻塞
export const NormalPriority = 3;      // 正常
export const LowPriority = 4;         // 低优先级
export const IdlePriority = 5;        // 空闲

类组件中 setState 发生了什么 是同步的还是异步的

  1. setState 获取到state
  2. enqueueState 将state 添加到更新队列中
  3. requestUpdateLane 获取当前执行更新的state 的优先级
  4. scheduleUpdateOnFiber 调度更新,区分更新模式,是同步更新(微任务异步 performSyncWorkOnRoot),还是并发更新(宏任务异步 performConcurrentWorkOnRoot postMessage实现的)

是异步的

如果是同步的话每次更新都会触发组件重新渲染浪费性能,异步还可以批量处理相同相似的更新

javascript 复制代码
// 1. setState 获取 state
function setState(newState) {
  // 获取当前组件实例的 state
  const instance = this;
  const currentState = instance.state;
}

// 2. enqueueState 将更新加入队列
function enqueueState(instance, newState) {
  // 创建更新对象
  const update = {
    payload: newState,
    next: null
  };
  
  // 将更新添加到更新队列
  const updateQueue = instance.updateQueue || (instance.updateQueue = []);
  updateQueue.push(update);
}

// 3. requestUpdateLane 确定优先级
function requestUpdateLane() {
  // 根据不同情况返回不同的优先级
  // 例如:用户交互 > 网络请求 > 普通更新
  if (isUserInteraction) {
    return UserBlockingPriority;
  }
  return NormalPriority;
}

// 4. scheduleUpdateOnFiber 调度更新
function scheduleUpdateOnFiber(fiber, lane) {
  if (lane === SyncLane) {
    // 同步更新:微任务
    queueMicrotask(() => {
      performSyncWorkOnRoot();
    });
  } else {
    // 并发更新:宏任务
    window.postMessage('update', '*');
  }
}

// 同步更新示例
function performSyncWorkOnRoot() {
  // 立即执行更新
}

// 并发更新示例
window.addEventListener('message', () => {
  performConcurrentWorkOnRoot();
});

什么是合成事件

React 的合成事件(SyntheticEvent)是 React 对原生 DOM 事件的封装。

// 执行顺序:

// 1. React 捕获阶段 (handleCapture)

// 2. 原生捕获阶段

// 3. 目标阶段

// 4. 原生冒泡阶段** --> 为什么有阻止冒泡了? 因为addEventListener第二个参数默认为false冒泡阶段 ,**冒泡阶段从内到外会触发父元素点击事件

// 5. React 冒泡阶段 (handleClick -> handleBubble)

javascript 复制代码
interface EventExamples {
  // 鼠标事件
  onClick: (e: React.MouseEvent) => void;
  onMouseEnter: (e: React.MouseEvent) => void;

  // 键盘事件
  onKeyPress: (e: React.KeyboardEvent) => void;

  // 表单事件
  onChange: (e: React.ChangeEvent) => void;
  onSubmit: (e: React.FormEvent) => void;

  // 焦点事件
  onFocus: (e: React.FocusEvent) => void;
}

所有事件都委托到 root 节点统一处理(React 17 之后)

原来的是委托到document元素上,事件冒泡需要冒泡到document 元素上更长了

javascript 复制代码
// React 16
// 事件冒泡到 document,路径更长
button -> div -> body -> document

// React 17
// 事件冒泡到 root,路径更短
button -> div -> root

跨浏览器兼容

事件委托减少事件监听器

当存在相同时间时,不需要创建多个事件,不需要多次绑定到root元素上,只需要绑定一次

javascript 复制代码
// 1. 跨浏览器兼容性
function handleChange(e) {
  // 在不同浏览器中都能正常工作
  const value = e.target.value;
}

// 2. 性能优化
<div>
  <button onClick={handleClick}>按钮1</button>
  <button onClick={handleClick}>按钮2</button>
  <button onClick={handleClick}>按钮3</button>
  {/* 只在 root 上绑定一个事件处理器 */}
</div>

// 3. 统一的事件处理
function handleEvent(e) {
  // 所有事件都有统一的接口
  e.preventDefault();
  e.stopPropagation();
  e.nativeEvent;
}

提高了性能和可维护性。

请描述一下react 的渲染流程是什么

首先可以分为两个阶段 || 三个阶段

  1. 两个阶段
  • reconciler 协调阶段(初始化挂载阶段 & 更新阶段)异步,可中断的
  • commit 提交阶段
    1. 初始化挂载阶段:从APP跟组件开始执行,根据组件的render函数层层递归创建dom元素(并不会挂载),再根据虚拟dom元素创建fiber树结构,使return sbling child三个属性进行关联,并对副作用比如hooks state进行标记
    2. 更新阶段:会对初始化挂载阶段创建的fiber对象进行深度优先遍历,然后对新老节点进行diff对比,如果有变化就会标记为副作用,并记录变化的props相关参数,在commit阶段进行更新。
  1. 三个阶段
  • reconciler协调阶段
  • render渲染阶段
  • comiit 提交阶段
    1. commit阶段:主要用于页面真正的渲染,这个阶段是同步的不可打断的,commit又分为三个阶段。 分别是commit之前,commit中,commit之后。
      1. commit之前: 执行组件渲染之前相关的hooks,比如useEffect。
      2. commit中,执行真正的渲染,这个时候dom会渲染到浏览器上。
      3. commit之后,处理一些渲染完成需要执行的事情,比如执行,useLayoutEffect。

让我们来详细查看一下内容:

1. Reconciliation 阶段(可中断)

  • Scheduler(调度器)
    • 调度任务优先级
    • 时间分片
    • 可中断渲染
javascript 复制代码
// 1. 优先级调度
function scheduler() {
    // 设置任务优先级
    const priorities = {
        ImmediatePriority: 1, // 最高优先级,需要同步执行
        UserBlockingPriority: 2, // 用户交互
        NormalPriority: 3, // 正常优先级
        LowPriority: 4, // 低优先级
        IdlePriority: 5 // 空闲优先级
    };

    // 任务时间分片
    requestIdleCallback(() => {
        // 在浏览器空闲时执行低优先级任务
    });
}
  • Reconciler(协调器)
    • 构建 Fiber 树
    • Diff 算法
    • 标记副作用 (hooks、state)
javascript 复制代码
// 1. 构建/更新 Fiber 树
function reconciler() {
    // a. 初始化挂载
    function mount() {
        // 根据组件创建 Fiber 节点
        // 通过 return、child、sibling 连接
        // 标记 Hooks、State 等副作用
    }

    // b. 更新处理
    function update() {
        // 深度优先遍历
        // Diff 算法对比
        // 标记需要更新的节点
    }
}

2. Commit 阶段(不可中断)

before commit:执行 pre-commit hooks

javascript 复制代码
function beforeCommit() {
    // 1. 处理 DOM 渲染前的准备工作
    // 2. 执行组件的 getSnapshotBeforeUpdate
    // 3. 执行 useEffect 的清理函数
}
markdown 复制代码
- **commit**:执行 DOM 操作
javascript 复制代码
function commit() {
    // 1. 处理 DOM 操作
    switch (effectTag) {
        case 'PLACEMENT':
            // 插入新节点
            break;
        case 'UPDATE':
            // 更新节点
            break;
        case 'DELETION':
            // 删除节点
            break;
    }

    // 2. 执行对应的生命周期
    // 3. 更新 ref
}

after commit:执行 useLayoutEffect 等

javascript 复制代码
function afterCommit() {
    // 1. 执行 useLayoutEffect
    // 2. 调用 componentDidMount/Update
    // 3. 执行 useEffect
    // 4. 清空副作用标记
}

这样就清晰多了:两个主要阶段,其中 Reconciliation 阶段包含调度和协调两个子过程。

React和React-Dom的区别?为什么有两个库?

  1. react:这是React库的核心。它定义了React组件的创建和生命周期方法,以及React元素的概念。你可以将其视为React的"引擎"。
  2. react-dom:这个库提供了在浏览器环境中使用React的方法,例如将React组件渲染到DOM中,或者在DOM中触发React组件的更新。你可以将其视为React的"驱动程序"。

为什么是两个包? 因为为了跨平台支持

javascript 复制代码
// Web 平台
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, container);

// Native 平台
import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('App', () => App);

// 测试环境
import TestRenderer from 'react-test-renderer';
const testInstance = TestRenderer.create(<App />);

React 严格模式的作用

  1. 不安全的生命周期方法:某些生命周期方法在未来的React版本中将被弃用。严格模式会警告这些不安全的方法的使用。
  2. 使用过时或遗留的API:严格模式会警告使用过时或遗留的API。
  3. 意外的副作用:严格模式可以帮助你发现组件中可能的意外副作用。
    防止你写一些有问题的代码
  4. 与旧版本React不兼容的代码:严格模式会警告你的代码中可能与未来版本的React不兼容的部分。

JSX的作用

可以在js 中编辑dom,结果babel编译成js认识的代码

1. JSX 的本质

JSX 本质上是 React.createElement() 的语法糖:

jsx 复制代码
// JSX 写法
const element = (
  <div className="greeting">
    <h1>Hello, world!</h1>
  </div>
);

// 编译后的 JavaScript
const element = React.createElement(
  'div',
  { className: 'greeting' },
  React.createElement('h1', null, 'Hello, world!')
);

2. JSX 的主要作用

  1. 声明式渲染
jsx 复制代码
// JSX 让界面描述更直观
function Welcome() {
  return (
    <div>
      <h1>Welcome</h1>
      <p>This is JSX</p>
    </div>
  );
}

// 不使用 JSX 则很难读
function Welcome() {
  return React.createElement('div', null,
    React.createElement('h1', null, 'Welcome'),
    React.createElement('p', null, 'This is JSX')
  );
}
  1. JavaScript 表达式支持
jsx 复制代码
const name = 'John';
const element = (
  <div>
    {/* 可以直接使用 JavaScript 表达式 */}
    <h1>{name}</h1>
    <p>{2 + 2}</p>
    {isLoggedIn && <UserGreeting />}
  </div>
);
  1. 属性传递
jsx 复制代码
// 动态属性
const buttonText = 'Click me';
const className = 'primary-button';

const button = (
  <button className={className} disabled={false}>
    {buttonText}
  </button>
);

3. JSX 的特性

  1. 防止 XSS 注入
jsx 复制代码
const userInput = '<script>alert("XSS!")</script>';
// JSX 会自动转义,确保安全
const element = <div>{userInput}</div>;
  1. 支持展开运算符
jsx 复制代码
const props = {
  firstName: 'John',
  lastName: 'Doe'
};

const greeting = <Greeting {...props} />;
  1. 条件渲染
jsx 复制代码
function Greeting({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <UserGreeting />
      ) : (
        <GuestGreeting />
      )}
    </div>
  );
}

5. JSX 的优势

  1. 开发效率
  • 更直观的代码结构
  • 更好的开发体验
  • IDE 支持(语法高亮、自动完成)
  1. 可维护性
  • 声明式代码更容易理解
  • 组件化开发
  • 更容易重构
  1. 性能优化
  • 编译时优化
  • 运行时优化
  • 虚拟 DOM 的高效更新

6. 实际应用示例

jsx 复制代码
// 一个完整的组件示例
function UserProfile({ user, onLogout }) {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <div className="user-profile">
      <header>
        <h2>{user.name}</h2>
        {isEditing ? (
          <button onClick={() => setIsEditing(false)}>
            Save
          </button>
        ) : (
          <button onClick={() => setIsEditing(true)}>
            Edit
          </button>
        )}
      </header>

      <main>
        {isEditing ? (
          <UserForm user={user} />
        ) : (
          <UserDetails user={user} />
        )}
      </main>

      <footer>
        <button onClick={onLogout}>
          Logout
        </button>
      </footer>
    </div>
  );
}

JSX 是 React 的一个重要特性,它让组件的编写更加直观和高效,同时保持了 JavaScript 的灵活性。

getDerivedStateFromProps 和 getSnapshotBeforeUpdate的作用

详情

getDerivedStateFromProps

  1. 代替老版本中的componentWillReceiveProps() 方法判断前后两个props是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做以来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。
  2. 新版本中通过 getDerivedStateFromProps 来更新值,并且在 其中不能访问this.props,在 componentDidUpdate 里处理回调事件,也可以在 getDerivedStateFromProps中使用 componentDidUpdate,相当于return 就是修改 state
  3. 它的返回值,会对state进行相同属性覆盖,如果没有和state一样的属性就什么都不做。
    执行时机: 挂载和更新都执行,在render之前。
    什么场景下可以用这个方法?
    1. 人民币转换大小写

1. 注意事项

1.1 避免无限循环:
js 复制代码
// 错误示例
class Wrong extends React.Component {
  state = { value: '' };

  static getDerivedStateFromProps(props) {
    // 错误:每次都返回新对象会导致无限更新
    return { value: props.value };
  }
}

// 正确示例
class Correct extends React.Component {
  state = { value: '' };

  static getDerivedStateFromProps(props, state) {
    // 只在 props.value 变化时更新
    if (props.value !== state.prevValue) {
      return {
        value: props.value,
        prevValue: props.value
      };
    }
    return null;
  }
}
1.2 替代方案:
js 复制代码
// 很多情况下可以用其他方式替代
function BetterComponent({ value }) {
  // 1. 直接使用 props
  return <div>{value}</div>;

  // 2. 使用 useMemo
  const computed = useMemo(() => {
    return expensiveComputation(value);
  }, [value]);
}

getSnapshotBeforeUpdate

  1. 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
  2. commit阶段 (提交) 包含 componentDIdMount | componetDidUpdate | componentWillUnmont
  3. getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
    此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。
  4. 当有 该生命周期有返回值时 componentDidUpdate 就存在第三个参数 (prevProps,prevState,snapshot)
  5. 执行时机:只会更新的时候执行,在render之后,浏览器页面更新之前

场景: 长列表,需要不断的往里面塞入内容。 更新之前,记住滚轮的位置,传递给componentDidUpdtate,保持滚轮的位置。 getSnapshotBeforeUpdate更新之前记录滚轮的原始位置,传递给componentDidUpdtate。 componentDidUpdtate就使用原始的位置更新dom,保证滚轮的正确性。

js 复制代码
class ScrollList extends React.Component {
  listRef = React.createRef();

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 如果列表有新增内容,返回当前滚动位置
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果有快照值,说明列表发生了变化
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef} style={{ height: '200px', overflow: 'auto' }}>
        {this.props.list.map(item => (
          <div key={item.id}>{item.content}</div>
        ))}
      </div>
    );
  }
}

useEffect的执行时机?useEffect 的返回函数执行时机?

  1. 回调函数执行时机
  • 初始化阶段
  • 依赖更新时
  1. 销毁函数
  • 依赖更新时
  • 组件卸载时
  1. 问: 为什么卸载函数在依赖更新时也会触发,举例聊天室重连的情况

这是因为 React 需要确保每次 effect 在重新执行前都能正确清理之前的副作用。让我解释一下具体原因:

1. Effect 的设计原理

jsx 复制代码
function Example({ count }) {
  useEffect(() => {
    // 这是一个新的 effect
    console.log(`订阅 count: ${count}`);
    
    return () => {
      // 清理前一个 effect
      console.log(`取消订阅 count: ${count}`);
    };
  }, [count]);
}

每次依赖更新时:

  1. React 需要清理旧的 effect(执行清理函数)
  2. 然后执行新的 effect

这样设计的原因是为了保证数据的一致性

2. 实际场景示例

  1. 事件订阅
jsx 复制代码
function UserStatus({ userId }) {
  useEffect(() => {
    // 订阅新用户的状态
    const subscription = subscribe(userId);
    
    return () => {
      // 在订阅新用户之前,需要取消旧用户的订阅
      subscription.unsubscribe();
    };
  }, [userId]);
}

// 执行顺序(当 userId 从 1 变为 2):
// 1. 取消订阅用户 1
// 2. 订阅用户 2
  1. WebSocket 连接
jsx 复制代码
function ChatRoom({ roomId }) {
  useEffect(() => {
    // 连接新房间
    const connection = createConnection(roomId);
    connection.connect();
    
    return () => {
      // 在连接新房间前,断开旧房间连接
      connection.disconnect();
    };
  }, [roomId]);
}

3. 为什么需要这样设计

  1. 避免资源泄露
jsx 复制代码
function DataFetcher({ id }) {
  useEffect(() => {
    const controller = new AbortController();
    
    fetch(`/api/data/${id}`, {
      signal: controller.signal
    });
    
    return () => {
      // 在发起新请求前取消旧请求
      controller.abort();
    };
  }, [id]);
}
  1. 保持数据一致性
jsx 复制代码
function Timer({ delay }) {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(`当前 delay: ${delay}`);
    }, delay);
    
    return () => {
      // 在设置新定时器前清除旧定时器
      clearInterval(timer);
    };
  }, [delay]);
}

4. 执行顺序演示

jsx 复制代码
function Counter({ count }) {
  useEffect(() => {
    console.log(`Effect: count is ${count}`);
    
    return () => {
      console.log(`Cleanup: count was ${count}`);
    };
  }, [count]);
  
  return <div>{count}</div>;
}

// 当 count 从 1 变为 2 时的日志:
// 1. "Cleanup: count was 1"  // 清理旧的 effect
// 2. "Effect: count is 2"    // 执行新的 effect

5. 注意事项

  1. 闭包陷阱
jsx 复制代码
function Example({ count }) {
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的 count 是创建 effect 时的值
      console.log(count);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []); // 空依赖数组!
}
  1. 正确的依赖处理
jsx 复制代码
function Example({ count }) {
  useEffect(() => {
    const timer = setInterval(() => {
      // 现在会正确响应 count 的变化
      console.log(count);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [count]); // 添加正确的依赖
}

这种设计确保了:

  1. 副作用的正确清理
  2. 数据的一致性
  3. 避免资源泄露
  4. 更可预测的行为

虽然有时可能看起来有额外的开销,但这种设计对于维护应用的状态一致性是必要的。

为什么需要在依赖更新时清理:

因为 React 需要确保每次 effect 执行前都是"干净"的状态

  1. 防止副作用(会影响组件外部的操作 fetch 订阅 修改dom)重叠
javascript 复制代码
useEffect(() => {
    // 设置定时器
    const timer = setInterval(() => {
        console.log('当前count:', count);
    }, 1000);

    return () => clearInterval(timer); // 清理旧的定时器
}, [count]); // count 更新时

如果不清理,每次 count 变化都会创建新定时器,导致多个定时器同时运行

  1. 防止数据混乱
javascript 复制代码
useEffect(() => {
    // 订阅数据
    const subscription = api.subscribe(userId, data => {
        setUserData(data);
    });

    return () => subscription.unsubscribe(); // 取消旧的订阅
}, [userId]); // userId 更新时

如果不清理,切换用户时可能收到旧用户的数据

这就像:在重新装修房间前,需要先清理掉旧的装修,否则会造成混乱。

Hooks钩子为什么必须在顶层作用域?

因为在react中,组件的hooks是用链表这种数据结构进行连接的,通过next属性保持执行顺序的,如果从中间断开,或者执行的顺序不同,会导致找不到后面的钩子,使组件的渲染达到不可预期的情况。

它的底层就是在fiber 树中的memoizedState中存储的以链的形式 hook1=> hook2=> hook3的链表结构

hooks它解决了什么问题:

  • hooks状态复用、逻辑复用。 class组件UI复用,比较臃肿。
  • React官方的理念,并不想把hooks和生命周期混为一谈。你会发现hooks并不能完全和class组件对号入座?

Hooks 在 Fiber 中的存储结构:

javascript 复制代码
// Fiber 节点结构(简化)
{
    memoizedState: {
        // hook1
        memoizedState: value1,
        next: {
            // hook2
            memoizedState: value2,
            next: {
                // hook3
                memoizedState: value3,
                next: null
            }
        }
    }
}

fiber对象的颗粒度到底长什么样?

要注意,关于fiber的面试,会问fiber怎么体现组件间的关联关系的? 这里实际上就是3个属性,child,sibling,return 将整个项目所有fiber对象关联起来。 fiber对象的遍历方式是使用深度优先遍历。 深度优先遍历,这里面试官可能会追问,怎么遍历的? 有兴趣同学可以了解下,深度和广度遍历两种算法。 blog.csdn.net/qq_44918331...

javascript 复制代码
// Fiber 节点的基本结构
type Fiber = {
  // 标记 Fiber 类型
  tag: WorkTag,           // 例如:函数组件、类组件、原生DOM等
  
  // 元素的标识
  key: null | string,
  elementType: any,       // React 元素类型
  type: any,             // 组件类型
  
  // DOM 相关
  stateNode: any,        // 实际 DOM 节点或组件实例
  
  // Fiber 树结构
  return: Fiber | null,  // 父节点
  child: Fiber | null,   // 第一个子节点
  sibling: Fiber | null, // 下一个兄弟节点
  
  firstEffect: null, // 第一个副作用​
  lastEffect: null, // 最后一个副作用​
  nextEffect: null, // 下一个副作用
  
  // 更新相关
  pendingProps: any,     // 新的 props
  memoizedProps: any,    // 当前 props
  memoizedState: any,    // 当前 state
};

// JSX 结构 实例
function App() {
  return (
    <div>              // Fiber 节点
      <Header />       // Fiber 节点
      <Main>           // Fiber 节点
        <Article />    // Fiber 节点
        <Sidebar />    // Fiber 节点
      </Main>
      <Footer />       // Fiber 节点
    </div>
  );
}

// 对应的 Fiber 树结构
const fiber = {
  tag: HostComponent,
  type: 'div',
  child: headerFiber,
  sibling: null,
  return: parentFiber,
  // ...其他属性
};

useEffect和useLayoutEffect的区别?

执行时机不同:

  1. useEffect
javascript 复制代码
// 异步执行,在浏览器渲染完成后执行
useEffect(() => {
    // 不会阻塞浏览器渲染
    console.log('浏览器渲染后执行');
});
  1. useLayoutEffect
javascript 复制代码
// 同步执行,在浏览器渲染前执行
useLayoutEffect(() => {
    // 会阻塞浏览器渲染
    console.log('浏览器渲染前执行');
});

应用场景:

加载时间过长导致添加的dom 元素 或者内容跳动 就可以使用他 案例

案例 :useEffect 是异步执行的,当浏览器渲染成功后才会执行,随后计算元素的位置,因为存在一个默认位置,此时如果计算元素位置这个步骤耗时很长就会出现元素抖动的情况,所有使用useLayoutEffec**t 同步执行**,当js修改dom之后再执行的useLayoutEffect,就可以在浏览器开始渲染执行计算元素的位置,之后再显示元素这样就可以解决抖动的问题, 意思就是说useLayoutEfect是同步操作,但是一般不适用于**耗时**的业务,因为会阻塞页面的渲染

使用场景:

  1. useEffect
  • 大多数副作用(网络请求、订阅等)
  • 不需要同步更新的场景
  • 性能更好,是默认推荐的选择
  1. useLayoutEffect
  • 需要操作 DOM 并且立即看到变化
  • 防止页面闪烁
  • 需要同步计算布局的场景

简单记忆:

  • useEffect:渲染后执行,像"事后处理"
  • useLayoutEffect:渲染前执行,像"同步处理"

useMemo useCallback React.memo的应用场景?

useMemo:缓存变量

useMemo 是用来解决react产生的无用渲染性能问题,就想生命周期中的**shouldComponentUpdate**异曲同工之妙 , 缓存的是变量案例

:::color1 适用于 父组件需要更新 子组件不需要更新的情况, 因为react 的执行机制是至上而下的,当父组件更新子组件也会跟着更新 就可以用到useMemo, 前提是 **子组件的方法很耗时才需要使用到 **否则不建议使用,

问:在vue中有相应的方法吗?

computed

:::

javascript 复制代码
// 缓存计算结果或值
const memoizedValue = useMemo(() => {
    return expensiveComputation(a, b);
}, [a, b]);


// 该案例是在子组件内部做的缓存 也可以在父组件中做子组件prop 的缓存
// 下面的案例就是结合 React.Memo 做的
import React, { useState, useEffect ,useMemo } from 'react';


function Memo(){
    const [state1,setState1] = useState('状态一')
    const [state2,setState2] = useState('状态二')

    return(
        <div>
            <button onClick={()=>setState1(Date.now())}>状态一</button>
            <button onClick={()=>setState2(Date.now())}>状态二</button>
            <MemoChildrens name={state1}>{state2}</MemoChildrens>
        </div>
    )
}

function MemoChildrens({name,children}){
    // console.log(name,children)
    function handleClick(name){
        console.log('点击执行')
        return name 
    }
    //! 控制当name状态一发生变化 ,该函数才知晓,因为状态二和该函数没有关系
		//! 第二个参数为空时,只会在组件创建时只需第一个参数
		//! 它和useCallBack不同的地方是返回的是一个变量
    const actionState1 = useMemo(()=>handleClick(name),[name])
    return(
        <>
            <div>{actionState1} 状态一</div>
            <div>{children} 状态二 </div>
        </>
    )
}

export default Memo;

使用场景:

  • 复杂计算的结果
  • 防止对象频繁创建

useCallback:缓存函数

是用来**缓存方法**的,当组件创建时,第二个参数发生变化时该方法会被重新创建,否则使用缓存中的方法, 和 useEffect类似 案例, 为什么会使用他,因为每次组件被创建或更新的时候函数都是创建一个新的个引用,前后不一样,所有需要使用到useCallback

使用场景:

传递React.memo的子组件的回调使用

作为其他hooks 的依赖时使用

复杂的回调逻辑使用

javascript 复制代码
1. // 缓存函数
const memoizedFunction = useCallback(() => {
    doSomething(a, b);
}, [a, b]);

2. useCallback作为其他hooks 的依赖时使用
function SearchComponent() {
  const [query, setQuery] = useState('');
  
  // ✅ 好的使用场景:函数会被用作其他 Hook 的依赖
  const searchApi = useCallback(async (searchQuery) => {
    const response = await fetch(`/api/search?q=${searchQuery}`);
    return response.json();
  }, []);

  // searchApi 作为 useEffect 的依赖项
  useEffect(() => {
    searchApi(query);
  }, [query, searchApi]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

3. userCallback+ react.memo
//该案例使用 
 //应用场景是 在父组件中对子组件的props 进行缓存用户就需要结合 React.memo
  // 当然也可以在子组件内 做缓存 就不需要 React.memo ,上面一个案例就是子组件内部做缓存

function Bpp(props){
  console.log('bpp 执行')
  const  {click} = props
  return(
    <div>Bpp</div>
  )
}
// 将组件进行渲染优化 //此时父组件更新 data子组件不会更新
const BppMemo = React.memo(Bpp)

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

  console.log('app 执行')
  const click = useCallback(()=>{
    alert(1)
  },[])
  let data = useMemo(()=>{
    return {}
  },[])
  return(
    <div>
      <button onClick={()=>setCount(count +1)}>加</button>
      {count}
      <BppMemo data={data} click={click}/>
    </div> 
  )
}

使用场景:

  • 传递给子组件的函数
  • 防止函数重新创建

React.memo:缓存组件

多用于计算密集型组件,使用React.memo来避免不必要的重复渲染,是用于组件级别的缓存,而useMemo是值级别的缓存

实例有 userTransiution 中for 的多次循环显示子组件,还有userCallback 中对子组件的 缓存函数案例

问:和 useMemo的区别是什么

React.memo 是缓存组件级的一般是配置useCallback 一起使用,useMemo是缓存变量的

javascript 复制代码
// 缓存整个组件
const MemoizedComponent = React.memo(function MyComponent(props) {
    return <div>{props.value}</div>;
});


// 父组件
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    console.log("按钮点击");
  }, []);

  console.log("父组件渲染");

  return (
    <div>
      <input 
        value={text} 
        onChange={e => setText(e.target.value)} 
      />
      <button onClick={() => setCount(count + 1)}>
        count: {count}
      </button>

      {/* 没有使用 React.memo,即使 props 没变,也会重新渲染 */}
      <ChildComponent onClick={handleClick} name="test" />
    </div>
  );
}

// 普通子组件(没有使用 React.memo)
function ChildComponent({ onClick, name }) {
  console.log("子组件渲染");
  return (
    <div>
      <h3>名字: {name}</h3>
      <button onClick={onClick}>点击</button>
    </div>
  );
}

//////对比使用和不使用 React.memo 的情况:

// 1. 不使用 React.memo(会重新渲染)
function ChildComponent({ onClick, name }) {
  console.log("普通子组件渲染");
  return <div>{name}</div>;
}

// 2. 使用 React.memo(不会重新渲染)
const MemoChildComponent = React.memo(function ChildComponent({ onClick, name }) {
  console.log("memo子组件渲染");
  return <div>{name}</div>;
});

// 父组件
function Parent() {
  const [count, setCount] = useState(0);
  
  // 当点击按钮时,即使子组件的 props 没变
  // ChildComponent 也会重新渲染
  // 而 MemoChildComponent 不会重新渲染
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        count: {count}
      </button>
      
      <ChildComponent name="test" />
      <MemoChildComponent name="test" />
    </div>
  );
}

使用场景:

  • 纯展示组件
  • 接收简单 props 的组件
  • 重复渲染的组件

简单记忆:

  • useMemo:记住值
  • useCallback:记住函数
  • React.memo:记住组件

forwardRef useRef的作用? 在vue 中中的Ref的区别

React 中的 Ref:

  1. useRef:用于保存可变值或获取 DOM 元素
javascript 复制代码
function Component() {
    // 1. 保存值(不会触发重渲染)
    const countRef = useRef(0);
    countRef.current++; // 修改值

    // 2. 获取 DOM 元素
    const inputRef = useRef(null);
    useEffect(() => {
        inputRef.current.focus();
    }, []);

    return <input ref={inputRef} />;
}
  1. forwardRef:转发 ref 到子组件
javascript 复制代码
// 子组件
const ChildInput = forwardRef((props, ref) => {
    return <input ref={ref} />;
});

// 父组件
function Parent() {
    const inputRef = useRef(null);
    return <ChildInput ref={inputRef} />;
}

Vue 中的 Ref:

  1. 模板引用
vue 复制代码
<template>
    <input ref="inputRef">
</template>

<script setup>
const inputRef = ref(null);
onMounted(() => {
    inputRef.value.focus();
});
</script>
  1. 响应式数据
vue 复制代码
<script setup>
const count = ref(0); // 响应式数据
count.value++; // 修改会触发视图更新
</script>

主要区别:

  1. React 的 useRef:
    • 不是响应式的
    • 修改不会触发重渲染
    • 主要用于存储 DOM 或值
  2. Vue 的 ref:
    • 是响应式的
    • 修改会触发视图更新
    • 既可以引用 DOM 也是响应式数据

讲一下react 的并发更新

并发更新是基于fiber数据结构,react将任务进行细粒度的拆分,在源码中将任务分为5个等级,并发任务是在时间切片中执行,并且规定时间切片时间为5ms(超出时间片则让出主线程),当切片时间有空闲时间就会用来执行并发任务(保证主线程不被阻塞), 这个浏览器空闲时间 是requestIdLeCallback 的思想, 现在是通过messageChennel 中的postmassage 来模拟实现的,

  • 高优先级任务可以打断低优先级任务
  • 被打断的任务会在后续时间片中继续执行
  • 确保重要的更新(如用户输入)能够及时响应

useTransition 和 useDeferredValue 区别?

都是用于处理性能优化的 Hook,但有不同的使用场景:

useTransition:控制状态更新优先级

javascript 复制代码
function SearchComponent() {
    const [isPending, startTransition] = useTransition();
    const [searchQuery, setSearchQuery] = useState('');

    const handleChange = (e) => {
        // 输入框立即更新(高优先级)
        setSearchQuery(e.target.value);
        
        // 搜索结果延迟更新(低优先级)
        startTransition(() => {
            setSearchResults(search(e.target.value));
        });
    };

    return (
        <div>
            <input onChange={handleChange} />
            {isPending && <div>Loading...</div>}
            <SearchResults results={searchResults} />
        </div>

    );
}

useDeferredValue:延迟值的更新

javascript 复制代码
function SearchList({ query }) {
    // 延迟处理搜索词
    const deferredQuery = useDeferredValue(query);
    
    // 使用延迟的值来渲染列表
    return <ExpensiveList query={deferredQuery} />;
}

主要区别:

  1. useTransition:
    • 主动控制更新优先级
    • 可以显示 pending 状态
    • 用于控制状态更新
  2. useDeferredValue:
    • 被动接收需要延迟的值
    • 类似防抖效果
    • 用于延迟展示

简单理解:

  • useTransition 像是"主动控制交通灯"
  • useDeferredValue 像是"被动等待高峰期过去"

useInsertionEffect

主要是服务于 css -in- js 库的作者特意打造的

useSyncExternalStore的作用?

解决的问题是状态撕裂的问题,

这个问题是由react并发更新的机制引起的,比如我们正在执行更新任务,redux 将状态改为A,结果此时有个更高优先级的任务插入进来,旧的任务就会先暂停,但是这个时候新任务把redux 状态该为了B,然后react就不知道到到底依赖的是A 还是B

redux 中的useSelector的作用和原理

作用:读取store的某一个数据

原理:首先执行的 useContext(ReactReduxContext) 获取到store, 再通过 selector 获取到当前所需要的数据,最后通过store.subscribe注册回调订阅store的变化,当state发生变化时,会重新调用selectore函数获取到当前state,与原来的state进行对比,发送变化就强制更新组件

javascript 复制代码
function useSelector<State, Selected>(
  selector: (state: State) => Selected,
  equalityFn = Object.is  // 默认比较函数
) {
  // 1. 获取 Redux store
  const store = useContext(ReactReduxContext);
  
  // 2. 获取当前选择的值
  const selectedState = selector(store.getState());
  
  // 3. 强制更新组件的函数
  const [, forceRender] = useReducer(s => s + 1, 0);
  
  // 4. 订阅 store 变化
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      // 获取最新状态
      const newSelectedState = selector(store.getState());
      
      // 比较新旧值
      if (!equalityFn(selectedState, newSelectedState)) {
        // 值变化时强制更新组件
        forceRender();
      }
    });
    
    // 清理订阅
    return unsubscribe;
  }, [store, selector, equalityFn]);
  
  return selectedState;
}

useEffect 钩子,如果A组件嵌套B组件,执行顺序是什么

节点层级越深,越靠前 effect的回调收集过程是倒着收集的。 从最底层节点,往上收集。

Redux解决了什么问题?

实际上redux的模型并不是组件通信,redux的核心原理实际上是action修改store,store变化引起组件的重新渲染。

  • Store 就是一个公共的数据仓库
    • 可以理解为一个大对象,存放着所有需要共享的数据
  • 修改数据的唯一方式是派发 Action
    • Action 就像是一个命令,告诉 Redux "我要改什么数据"
    • 比如:dispatch({ type: 'ADD_TODO', payload: '写作业' })
  • 数据变化后,相关组件自动更新
    • 只要组件订阅了 Store 中的数据
    • Store 中的数据改变时,组件就会重新渲染

Redux 核心模型

javascript 复制代码
// 1. Store: 存储状态
const store = createStore(reducer, initialState);

// 2. Action: 描述状态变化
const addTodo = (text) => ({
  type: 'ADD_TODO',
  payload: text
});

// 3. Reducer: 处理状态更新
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    default:
      return state;
  }
};

数据流动过程

javascript 复制代码
// 1. 派发 Action
function TodoComponent() {
  const dispatch = useDispatch();
  
  const handleAdd = () => {
    // 触发 action
    dispatch(addTodo('新任务'));
  };
}

// 2. Reducer 处理更新
// 3. Store 状态更新
// 4. 组件重新渲染
function TodoList() {
  // 订阅 store 变化
  const todos = useSelector(state => state.todos);
  
  // store 变化导致组件重新渲染
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>

      ))}
    </ul>

  );
}

单向数据流

javascript 复制代码
// Action -> Reducer -> Store -> UI
const dataFlow = {
  // 1. Action 触发更新
  dispatch(action) ----> 

  // 2. Reducer 处理状态
  reducer(state, action) ----> 

  // 3. Store 更新状态
  store.setState(newState) ----> 

  // 4. UI 重新渲染
  component.render()
};

redux 有什么中间件

immer 是什么?有什么用?

首先:react 规定不能直接修改数据,因为react 使用浅比较来判断状态是否改变,从而决定是否更新组件

javascript 复制代码
// 1. 直接修改原始数据的情况
const user = { name: 'John' };
user.name = 'Mike';  // 修改后,user 还是原来的对象引用

// React 在比较时
prevState === currentState  // true,因为是同一个对象引用
// 结果:React 认为状态没有变化,不会触发重新渲染


// 2. 创建新对象的情况
const prevUser = { name: 'John' };
const newUser = { ...prevUser, name: 'Mike' };  // 创建新对象

// React 在比较时
prevState === currentState  // false,因为是不同的对象引用
// 结果:React 检测到变化,会触发重新渲染

如果使用传统的方式修改state的话会很繁琐,所以immer 就出现了,它简化了对数据的修改

javascript 复制代码
### 2. 传统解决方案:创建新对象
// ✅ 正确方式:但很繁琐
const updateCity = () => {
  setUser({
    ...user,               // 复制第一层
    address: {
      ...user.address,     // 复制第二层
      city: 'New York'     // 修改目标属性
    }
  });
};


### 3. Immer 解决方案:看起来像直接修改,但实际上是安全的
// ✅ 使用 Immer:简单直观
const updateCity = () => {
  setUser(draft => {
    draft.address.city = 'New York'; // 看起来是直接修改
    // 但 Immer 在背后帮我们处理了所有复制工作
  });
};

当我们对一个对象既需要拷贝,拷贝完又要对拷贝完的对象修改时,如果使用浅拷贝,会有一个修改对象的深层属性同时修改原对象深层属性的问题,如果使用深拷贝,当对象的数据层级过深结构过复杂时,但是我们只需要修改一两个属性,却要对整个对象结构进行深拷贝,那么对象结构的很多部分进行的深拷贝,是一种对计算机资源的严重的浪费!所以我们就想要一种按需拷贝的方案,就是需要修改的一小部分结构进行深拷贝,不需要修改的大部分结构进行浅拷贝,immer就实现了这种方案!immutablejs也解决了同样的问题,

什么是受控组件?

将UI状态完全交给state或者props管理,不允许直接修改value。

错误边界组件的作用是什么?vue 中的类似的API 嘛?

react 的错误边界组件

在react 中出现错误的时候就会把整个页面删除,错误边界要做的就是 将错误的组件报错就行

在vite 项目中自带 react-error-boundary 插件,无需安装 案例

javascript 复制代码
import React,{ useState } from "react";
import { ErrorBoundary,useErrorBoundary } from "react-error-boundary";
const MyComponent = () => {
    const [value, setValue] = useState('');
    const { showBoundary } = useErrorBoundary();
    try{
      const handleClick = () => {
        // 这里故意抛出一个错误
        if (value === '') {
          // 这个是自动收集的方式
          throw new Error('Value cannot be empty');
        }
      }
        useEffect(() => {
            fetchGreeting(name).then(
                response => {
                    // Set data in state and re-render
                },
                error => {
                    // Show error boundary 这是手动的方式将错误传递给边界函数ErrorBoundary
                    showBoundary(error);
                }
            );
        });
      return (
          <div>
              Error
              <button onClick={handleClick}>Click me 手动触发错误</button>
          </div>
      );
    }catch(error){
        showBoundary(error);
    }
};
const MyComponent2 = () => {
    return (
      <div>另外一个组价</div>
    )
}
const ErrorFallback = ({error, resetErrorBoundary }) => {
  return (
      <div>
        <h1>Something went wrong. 显示错误消息的组件</h1>
        <p>{error.message}</p>
        <button onClick={resetErrorBoundary}>Try again</button>
      </div>
  );
};

const Errors = () => {
  return (
      <>
          <MyComponent2 />
          <ErrorBoundary FallbackComponent={ErrorFallback} >
              <MyComponent />
          </ErrorBoundary>
      </>
  );
};

export default Errors;

vue 中错误API

Vue 提供了几种错误处理方式:

  1. 全局错误处理
javascript 复制代码
// main.ts
app.config.errorHandler = (err, instance, info) => {
  // 处理错误
  console.error('全局错误:', err)
}
  1. 组件级错误处理
typescript 复制代码
// 使用 onErrorCaptured 生命周期钩子
onErrorCaptured((err, instance, info) => {
  // 处理错误
  return false // 阻止错误继续传播
})

react 的diff 算法 和 vue 的diff算法对比

Vue 的处理:

  • 通过双端对比,可以快速识别节点移动
  • 最终只需要移动节点位置

React 的处理:

  • 会依次比对,发现不同就创建新节点
  • 可能会有更多的 DOM 操作

Tree Diff(树比较)

jsx 复制代码
// 旧的结构
<div>
  <p>Hello</p>

</div>

// 新的结构
<span>
  <p>Hello</p>

</span>

// 因为根节点 div -> span 类型不同
// React 会直接删除旧树,创建新树

Component Diff(组件比较)

jsx 复制代码
// 旧的结构
<div>
  <Counter />
</div>

// 新的结构
<div>
  <Timer />
</div>

// Counter 和 Timer 是不同的组件
// React 会删除 Counter,插入 Timer

Element Diff(元素比较)

jsx 复制代码
// 旧的列表
<ul>
  <li key="1">First</li>

  <li key="2">Second</li>

</ul>

// 新的列表
<ul>
  <li key="2">Second</li>

  <li key="1">First</li>

</ul>

// 通过 key,React 知道只需要调整顺序
// 而不是删除后重建

简单说:

  1. dom 元素比较两棵树时,先看根节点类型
  2. 比较组件时,看组件类型
  3. 比较列表时,用 key 来判断

这样 React 就能高效地找出需要更新的部分。

Vue 的处理

为什么 React 不用双端对比?

  • 设计理念不同
    • React 更注重简单性和可预测性
    • Vue 更注重性能优化和减少 DOM 操作
  • Fiber 架构的影响
    • React 的 Fiber 架构需要支持中断和恢复
    • 双端对比算法难以在中断后恢复状态
  • 权衡取舍
    • React 团队认为复杂的 Diff 算法收益有限
    • 单端对比虽然可能多一些 DOM 操作,但代码更简单、可维护
相关推荐
晚安日记wanna31 分钟前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna33 分钟前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
晚安日记wanna41 分钟前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna1 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna1 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
晚安日记wanna1 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构
SamDeepThinking1 小时前
Java 17内存屏障:为什么需要,怎么用
java·后端·面试
CC数分1 小时前
2026年金融数据分析岗位需要哪些工具?2027届秋招备考指南
面试·数据分析
ocean21034 小时前
2025-2026年C语言专题面试高频知识点洞察
c语言·面试·面试经验
letisgo55 小时前
JAVA 高级进阶18篇《生产级RAG:切分、检索、评估与Graph RAG全链路实战》
java·面试·检索增强·rag·graph rag