前端:最新 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 操作,但代码更简单、可维护
相关推荐
m0_674031438 小时前
React - useContext和深层传递参数
前端·javascript·react.js
傻小胖8 小时前
React Diffing 算法完整指南
开发语言·react.js
刺客-Andy8 小时前
React 第二十节 useRef 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
漂流瓶jz8 小时前
如何使用React,透传各类组件能力/属性?
前端·javascript·react.js
fengxingzhe0088 小时前
OSPFv2协议状态切换(状态机)基本原理-RFC2328
运维·网络·网络协议·面试·ospfv2·ospfv2协议状态机·ospfv2报文交互实例
泡泡茶壶_lemon12 小时前
React-Scheduler机制
react.js
LoongProxy12 小时前
提升Scrapy爬虫效率的动态代理IP设置指南
面试
DogDaoDao12 小时前
leetcode 面试经典 150 题:删除有序数组中的重复项
算法·leetcode·面试·数组·双指针·数据结构与算法·重复数组
周星星日记12 小时前
vue3中ref到底在干什么
前端·vue.js·面试
Abelard_14 小时前
Redis--缓存穿透、击穿、雪崩以及预热问题(面试高频问题!)
java·数据库·redis·缓存·面试