React面试葵花宝典之二

news2025/3/4 20:10:16

36.Fiber的更新机制

React Fiber 更新机制详解

React Fiber 是 React 16 引入的核心架构重构,旨在解决可中断渲染优先级调度问题,提升复杂应用的流畅性。其核心思想是将渲染过程拆分为可控制的工作单元,实现更细粒度的任务管理。以下是其核心机制:


一、Fiber 架构的设计目标
  1. 可中断与恢复:允许渲染过程被高优先级任务(如用户输入)打断,后续恢复。
  2. 增量渲染:将渲染任务拆分为多个小任务(时间分片),避免阻塞主线程。
  3. 优先级调度:根据任务类型(如动画、数据加载)分配不同优先级。
  4. 并发模式支持:为 Suspense、Transition 等特性提供底层支持。

二、Fiber 节点:工作单元的基础

每个 Fiber 节点对应一个组件或 DOM 节点,构成链表树结构,包含以下关键信息:

  • 组件类型:函数/类组件、HTML 标签等。

  • 状态与 Propsstatepropscontext

  • 副作用标记:增/删/更新 DOM、调用生命周期等(通过 flags 字段标识)。

  • 链表指针

    • child:指向第一个子节点。
    • sibling:指向下一个兄弟节点。
    • return:指向父节点。
  • 优先级lane 模型标记任务优先级(如 SyncLane、InputContinuousLane)。


三、更新流程:从触发到提交
1. 触发更新
  • 来源setStateuseState、父组件重渲染、Context 变更等。
  • 创建更新对象:包含新状态、优先级等信息,添加到 Fiber 的更新队列。
2. 调度阶段(Scheduler)
  • 任务分片:将整个渲染流程拆分为多个 Fiber 节点的处理单元。
  • 优先级排序:使用 lane 模型分配优先级,高优先级任务可抢占低优先级。
  • 时间切片:通过 requestIdleCallbackMessageChannel 在浏览器空闲时段执行任务。
3. 协调阶段(Reconciler)
  • 构建 WorkInProgress 树:在内存中生成新 Fiber 树(双缓存机制)。
  • Diff 算法:对比新旧 Fiber 节点,标记变更(如 PlacementUpdateDeletion)。
  • 生命周期触发:执行函数组件的渲染、类组件的 render 方法。
4. 提交阶段(Commit)
  • 同步执行:不可中断,一次性将变更应用到 DOM。

  • 副作用处理

    • DOM 操作:增删改节点。
    • 生命周期:类组件的 componentDidMount/Update
    • HooksuseLayoutEffect 回调。
  • 切换当前树:将 WorkInProgress 树标记为 current 树。


四、优先级调度与中断机制
  • Lane 模型:用二进制位表示优先级(如 0b00010b0010 可合并为 0b0011)。
  • 高优先级抢占:用户交互触发的更新(如按钮点击)可中断正在进行的低优先级渲染(如大数据列表渲染)。
  • 饥饿问题处理:长时间未执行的低优先级任务会被提升优先级。

示例场景
用户输入搜索关键词时,输入框的即时响应(高优先级)会中断后台数据渲染(低优先级)。


五、双缓存技术
  • Current 树:当前屏幕上显示的 Fiber 树。
  • WorkInProgress 树:正在构建的新树,完成后替换 Current 树。
  • 优势:减少渲染过程中的页面闪烁,确保原子性更新。

六、并发模式下的更新
  • 过渡更新(Transition) :通过 startTransition 标记非紧急更新(如页面切换),可被用户交互打断。

    const [isPending, startTransition] = useTransition();
    startTransition(() => {
      setPage(newPage); // 低优先级更新
    });
    
  • Suspense:配合懒加载组件,在数据加载时显示 fallback UI。


七、性能优化启示
  1. 减少渲染粒度:使用 React.memouseMemo 避免无效渲染。
  2. 合理分配优先级:紧急操作使用高优先级,长任务用 startTransition 包裹。
  3. 优化 Fiber 树深度:扁平化组件结构,减少协调时间。

总结

React Fiber 通过可中断的异步渲染优先级调度,彻底改变了 React 的渲染机制。其核心价值在于:

  • 更流畅的交互:高优先级任务快速响应,避免界面卡顿。
  • 更高效的渲染:增量更新减少主线程阻塞。
  • 面向未来的扩展:为并发特性(如 Suspense、Server Components)奠定基础。

37.React18有哪些更新

React 18 主要更新详解

React 18 引入了多项重要改进和新特性,旨在提升性能、开发体验及扩展能力。以下是其核心更新内容:


1. 并发渲染(Concurrent Rendering)
  • 核心机制:通过可中断的渲染过程,实现任务优先级调度与时间分片。

    • 并发模式(Concurrent Mode) :现称为“并发特性”,无需全局开启,按需使用。
    • API支持startTransitionuseDeferredValue 等。
  • 优势

    • 高优先级任务(如用户输入)可中断低优先级渲染,提升交互流畅度。
    • 支持复杂场景下的无缝过渡(如页面切换、数据加载)。

示例

import { startTransition } from 'react';

// 标记非紧急更新
startTransition(() => {
  setSearchQuery(input); // 延迟渲染搜索结果,保持输入响应
});

2. 自动批处理(Automatic Batching)
  • 改进点:在更多场景下合并状态更新,减少渲染次数。

    • React 17及之前:仅在事件处理函数中批处理。
    • React 18:扩展至Promise、setTimeout等异步操作。
  • 效果:降低不必要的重渲染,优化性能。

示例

// React 18:两次setState合并为一次渲染
setTimeout(() => {
  setCount(1);
  setFlag(true);
}, 1000);

3. 新的根API(createRoot)
  • 替换旧API:使用 createRoot 替代 ReactDOM.render,启用并发特性。

  • 用法

    import { createRoot } from 'react-dom/client';
    
    const root = createRoot(document.getElementById('root'));
    root.render(<App />);
    

4. Suspense 增强
  • 服务端渲染(SSR)支持

    • 流式HTML传输:逐步发送HTML,加速首屏加载。
    • 选择性Hydration:优先为交互部分注水,提升可交互时间(TTI)。
  • 客户端扩展:支持在更多场景包裹异步组件或数据加载。

示例

<Suspense fallback={<Loading />}>
  <AsyncComponent />
</Suspense>

5. 新Hooks API
  • useId:生成唯一ID,解决SSR与客户端ID不一致问题。

    const id = useId(); // 生成如 ":r1:"
    
  • useSyncExternalStore:简化外部状态库(如Redux)集成。

    const state = useSyncExternalStore(store.subscribe, store.getState);
    
  • useInsertionEffect:适用于CSS-in-JS库动态插入样式。

    useInsertionEffect(() => {
      const style = document.createElement('style');
      style.innerHTML = `.css { color: red }`;
      document.head.appendChild(style);
    });
    

6. 过渡API(Transitions)
  • 区分紧急/非紧急更新:通过 startTransition 延迟非关键渲染。

  • UI反馈useTransition 提供 isPending 状态,显示加载指示。

    const [isPending, startTransition] = useTransition();
    
    startTransition(() => {
      setTab(newTab); // 非紧急导航
    });
    
    return isPending ? <Spinner /> : <Content />;
    

7. 严格模式增强
  • 开发环境行为

    • 双调用Effects:模拟组件卸载/挂载,暴露副作用问题。
    • 组件重复挂载:检查是否正确处理清理逻辑(如定时器、订阅)。

8. 服务端组件(实验性)
  • 核心能力

    • 服务端渲染组件:在服务端执行,减少客户端代码体积。
    • 无缝数据获取:直接访问后端API,传递序列化数据至客户端。
  • 使用场景:静态内容、SEO优化、性能敏感页面。

示例

// ServerComponent.server.js
export default function ServerComponent() {
  const data = fetchData(); // 服务端执行
  return <div>{data}</div>;
}

9. 其他改进
  • 性能优化:减少内存占用,提升大型应用渲染效率。
  • TypeScript支持:更严格的类型推断,减少显式类型声明。
  • 开发者工具:增强并发模式调试支持,可视化渲染优先级。

升级指南

  1. 兼容性:React 18 保持向后兼容,逐步采用新特性。

  2. 迁移步骤

    • 使用 createRoot 替换 ReactDOM.render
    • 按需引入并发API(如 startTransition)。
    • 测试严格模式下的副作用处理。

总结

React 18 通过并发渲染、自动批处理、Suspense增强等特性,显著提升了应用性能与用户体验。开发者可通过渐进式升级,利用新API优化交互流畅度与渲染效率,同时为未来特性(如服务端组件)奠定基础。

38.Rect19有哪些新特性

具体详见官网:
中文:React 19 新特性
英文:React 19 新特性

核心新特性

1. Actions

解决问题:简化数据变更和状态更新流程

  • 以前需要手动处理待定状态、错误、乐观更新和顺序请求
  • 需要维护多个状态变量(isPending, error 等)

新特性

function UpdateName() {
  const [state, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) return error;
      redirect("/path");
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input name="name" />
      <button disabled={isPending}>Update</button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

主要改进

  • 自动处理待定状态
  • 内置错误处理
  • 支持乐观更新
  • 简化表单处理

2. useFormStatus

解决问题:简化表单组件状态访问

  • 避免通过 props 传递表单状态
  • 提供统一的表单状态访问方式
function SubmitButton() {
  const { pending, data, method } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

3. useOptimistic

解决问题:提供更好的用户体验

  • 立即显示操作结果
  • 处理异步操作的状态更新
function LikeButton({ id }) {
  const [likes, setLikes] = useState(0);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, increment) => state + increment
  );

  async function handleLike() {
    addOptimisticLike(1); // 立即更新 UI
    await updateLikes(id); // 后台进行实际更新
  }
}

4. use() Hook

解决问题:统一资源使用方式

  • 简化 Promise 和 Context 的使用
  • 支持条件性使用
  • 提供更好的类型推断
function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // 自动处理 Suspense
  return comments.map(comment => <p>{comment}</p>);
}

架构改进

1. Document 流式渲染

解决问题:改善首次加载体验

  • 支持 HTML 流式传输
  • 优化资源加载顺序
function AsyncPage() {
  return (
    <Document>
      <Suspense fallback={<Loading />}>
        <AsyncContent />
      </Suspense>
    </Document>
  );
}

2. 资源处理优化

样式表支持

解决问题:简化样式管理

  • 自动处理样式表加载顺序
  • 支持组件级样式声明
function Component() {
  return (
    <>
      <link rel="stylesheet" href="styles.css" precedence="default" />
      <div className="styled-content">...</div>
    </>
  );
}
异步脚本支持

解决问题:优化脚本加载

  • 自动处理脚本去重
  • 优化加载优先级
function MyComponent() {
  return (
    <div>
      <script async={true} src="widget.js" />
      <div>Widget Content</div>
    </div>
  );
}

开发体验改进

1. 错误处理增强

解决问题:提供更清晰的错误信息

  • 消除重复错误日志
  • 提供更详细的错误上下文
createRoot(container, {
  onCaughtError: (error) => {
    // 错误边界捕获的错误
  },
  onUncaughtError: (error) => {
    // 未被捕获的错误
  },
  onRecoverableError: (error) => {
    // 可恢复的错误
  }
});

2. 自定义元素支持

解决问题:改善与 Web Components 的集成

  • 完整支持自定义元素
  • 正确处理属性和属性传递

最佳实践建议

  1. 渐进式采用

    • 优先使用新的表单处理方式
    • 在关键交互中使用乐观更新
    • 利用新的资源加载优化
  2. 性能优化

    • 使用流式渲染改善加载体验
    • 合理使用资源预加载
    • 优化并发更新
  3. 错误处理

    • 使用新的错误边界
    • 实现适当的降级策略
    • 监控错误模式

服务器组件

1. 服务器组件基础

解决问题:优化应用性能和开发体验

  • 减少客户端 bundle 大小
  • 直接访问后端资源
  • 改善数据获取模式
// 服务器组件
async function Notes() {
  // 直接访问数据库,无需 API 层
  const notes = await db.notes.getAll();
  
  return (
    <div>
      {notes.map(note => (
        <Expandable key={note.id}>
          <p>{note.content}</p>
        </Expandable>
      ))}
    </div>
  );
}

2. 服务器组件与客户端组件集成

解决问题:平滑处理服务器和客户端组件交互

  • 支持渐进式增强
  • 保持交互性
  • 优化数据流
// 服务器组件
import Expandable from './Expandable';  // 客户端组件

async function NotesContainer() {
  const notes = await db.notes.getAll();
  
  return (
    <div>
      {/* 服务器组件可以渲染客户端组件 */}
      <Expandable>
        <NotesList notes={notes} />
      </Expandable>
    </div>
  );
}

// 客户端组件
'use client'
function Expandable({ children }) {
  const [expanded, setExpanded] = useState(false);
  
  return (
    <div>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Collapse' : 'Expand'}
      </button>
      {expanded && children}
    </div>
  );
}

3. 异步组件

解决问题:简化异步数据处理

  • 支持 async/await 语法
  • 自动处理 Suspense 集成
  • 优化加载状态
// 服务器组件中的异步数据获取
async function Page({ id }) {
  const note = await db.notes.get(id);
  // 开始获取评论但不等待
  const commentsPromise = db.comments.get(id);
  
  return (
    <div>
      <h1>{note.title}</h1>
      <Suspense fallback={<Loading />}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </div>
  );
}

Refs 作为 Props

1. 将 ref 作为 prop

从 React 19 开始,你现在可以在函数组件中将 ref 作为 prop 进行访问:

function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

//...
<MyInput ref={ref} />

新的函数组件将不再需要 forwardRef,我们将发布一个 codemod 来自动更新你的组件以使用新的 ref prop。在未来的版本中,我们将弃用并移除 forwardRef。

2. Ref 稳定性改进

解决问题:优化 ref 更新和同步

  • 更可预测的 ref 更新时机
  • 更好的并发模式支持
  • 改进的性能特性
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  // ref 回调模式的改进
  const setRef = useCallback((element: HTMLInputElement | null) => {
    if (element) {
      element.focus();
    }
  }, []);
  
  return <input ref={setRef} />;
}

服务器组件最佳实践

  1. 数据获取策略

    • 在服务器组件中直接访问数据源
    • 使用流式传输处理大量数据
    • 实现适当的缓存策略
  2. 组件分割

    • 将有状态逻辑的组件标记为客户端组件
    • 保持服务器组件纯粹
    • 优化组件边界
  3. 性能优化

    • 使用服务器组件减少客户端 bundle
    • 实现渐进式加载
    • 优化数据预取
// 示例:优化的服务器组件结构
async function BlogPost({ id }) {
  const post = await db.posts.get(id);
  const authorPromise = db.authors.get(post.authorId);
  const commentsPromise = db.comments.get(id);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <Suspense fallback={<AuthorSkeleton />}>
        <Author authorPromise={authorPromise} />
      </Suspense>
      <Content>{post.content}</Content>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </article>
  );
}

注意事项

  1. 服务器组件限制

    • 不能使用浏览器 API
    • 不能使用状态和生命周期
    • 需要正确处理数据获取错误
  2. Ref 使用考虑

    • 注意 ref 的生命周期
    • 合理处理 ref 清理
    • 避免过度依赖 ref
  3. 性能注意事项

    • 平衡服务器和客户端渲染
    • 合理使用 Suspense 边界
    • 优化数据预加载策略
      详细请看链接

39.React为什么不直接使用this.state改变数据

在 React 中,不能直接通过 this.state 来改变数据,而必须使用 this.setState() 来更新状态。这背后有几个关键的原因,主要是为了保证 组件的可预测性、性能优化避免直接修改状态 所带来的潜在问题。接下来,我将详细解释这些原因。

1. 不可变数据和状态管理原则

React 提倡 不可变数据(immutable data)原则,即状态对象一旦创建,它的值就不应该直接修改。直接修改 this.state 会导致组件行为变得难以预测,难以追踪和调试。通过 this.setState(),React 可以保证每次状态更新时,状态对象都是全新的对象,而不是直接修改原有对象。

为什么要避免直接修改 this.state

  • 直接修改状态会破坏数据的不可变性,使得 React 无法检测到变化。
  • 状态不再是新的引用,这使得 React 无法有效地进行比较,进而影响渲染效率。

举个例子,如果直接修改 this.state

this.state.someValue = newValue;  // 不推荐

这样 React 就不会知道状态发生了变化,因此不会触发重新渲染,也就无法同步 UI 和状态。

而通过 this.setState()

this.setState({ someValue: newValue });  // 推荐

this.setState() 会创建一个新的状态对象,确保 React 能检测到状态变化,并触发 UI 更新。


2. 异步更新与批量更新

this.setState() 的更新是异步的,而直接修改 this.state 是同步的。React 内部有一种机制,用来批量更新状态,以减少不必要的重新渲染。这种机制不仅提高了性能,还避免了多次渲染的重复计算。

例如,假设你直接修改了 this.state,并且立即访问了 this.state 来获取新值。由于 React 的 setState() 是异步的,直接修改 this.state 可能会导致你获取到的状态值不是更新后的值。

this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 可能不会立即反映出最新的状态

React 会将多个 setState() 调用合并到一个批量更新中,以减少不必要的渲染和性能开销。通过使用 this.setState(),React 可以处理这些合并和异步更新的操作。


3. 性能优化

this.setState() 触发的更新过程与直接修改 this.state 的过程有所不同。当调用 setState() 时,React 会合并当前的状态和新的状态,只有发生了变化的部分会被更新。这对于性能优化至关重要。

如果你直接修改 this.state,React 就无法知道哪些部分发生了变化,也就无法进行智能的 diff 和批量更新。例如:

this.state.count = 10;  // 直接修改
this.setState({ count: 10 });  // 通过 setState 更新

setState() 中,React 会比较前后的状态,判断是否需要重新渲染组件,而直接修改 this.state 则无法触发这种比较。


4. 组件的生命周期和渲染

this.setState() 触发状态更新时,React 会在合适的生命周期方法中触发组件的重新渲染。例如,在状态更新时,shouldComponentUpdatecomponentDidUpdate 等生命周期方法会被调用,以便开发者可以在状态变化时执行一些操作。如果直接修改 this.state,React 不会知道组件状态发生变化,进而不会触发这些生命周期方法。

这会导致一些问题,比如无法对比新旧状态、无法做条件渲染等操作。


5. React 状态更新的队列机制

React 通过维护一个更新队列来优化状态的更新和渲染。当你调用 this.setState() 时,React 会把状态更新请求放入一个队列,批量处理这些更新。在队列中的多个状态更新可以合并,这避免了不必要的重新渲染,提高了性能。

直接修改 this.state 不会加入更新队列,React 不会触发它的重新渲染机制,因此无法享受 React 的批量更新和性能优化。


6. 追踪组件的变化

React 使用 this.setState() 来管理组件状态,并且通过对比旧的虚拟 DOM 和新的虚拟 DOM,来决定哪些部分需要重新渲染。这个过程需要 React 在底层追踪和比较状态,而直接修改 this.state 会使得这个追踪变得困难,影响性能优化。

通过 this.setState(),React 能够确保组件状态的变化得到跟踪,并且在状态变化时触发重新渲染,确保 UI 始终与状态一致。


7. React 16+ 的 Fiber 架构

在 React 16 及更高版本中,React 引入了 Fiber 架构,这使得 React 的渲染变得更加灵活和高效。通过 this.setState(),React 能够控制每一次渲染的优先级,并且通过增量更新来保持 UI 响应性。如果你直接修改 this.state,这种增量更新机制就无法发挥作用。


总结:

  • 数据不可变性this.setState() 遵循不可变数据的原则,避免直接修改原有状态。
  • 异步和批量更新this.setState() 支持异步更新,React 可以批量处理多个状态更新,提升性能。
  • 生命周期管理:通过 setState,React 能够触发生命周期钩子,确保组件正确更新。
  • 性能优化this.setState() 可以通过智能的差异化更新减少不必要的 DOM 操作,而直接修改 this.state 会跳过这一过程。

因此,直接修改 this.state 会破坏 React 的更新机制,导致状态和 UI 不一致,影响性能和可维护性。React 的设计理念和架构要求我们通过 this.setState() 来更新状态,从而确保更高效和可预测的渲染过程。

40.原来的react虚拟dom树遍历是深度优先遍历还是层序遍历?存储是链表存储还是栈存储,fiber机制下的是如何存储

React 虚拟DOM遍历与存储机制详解


一、传统虚拟DOM(React 15及之前)
  1. 遍历方式深度优先遍历(DFS)

    • 递归处理:从根组件开始,递归处理每个组件及其子组件,直到叶子节点,再回溯处理兄弟节点。

    • 顺序示例

      A → A.child B → B.child C → C.child D → 回溯到 B → B.sibling E → E.child F
      
  2. 存储结构隐式调用栈

    • 依赖调用栈:递归调用栈隐式管理遍历过程,无显式数据结构存储节点关系。

    • 缺点

      • 不可中断:递归一旦开始必须执行完毕,导致主线程阻塞。
      • 性能瓶颈:深层嵌套组件树易引发栈溢出或卡顿。

二、Fiber架构(React 16+)
  1. 遍历方式可中断的迭代式深度优先遍历

    • 顺序不变:仍按深度优先顺序处理节点(与之前一致)。

    • 实现变化:从递归改为循环+链表指针手动遍历,支持暂停与恢复。

    • 流程示例

      let fiber = rootFiber;
      while (fiber) {
        process(fiber);      // 处理当前节点
        if (fiber.child) {
          fiber = fiber.child; // 优先处理子节点
          continue;
        }
        while (fiber) {
          completeWork(fiber); // 完成当前节点
          if (fiber.sibling) {
            fiber = fiber.sibling; // 转向兄弟节点
            break;
          }
          fiber = fiber.return;    // 回溯父节点
        }
      }
      
  2. 存储结构显式链表树

    • Fiber节点结构

      interface Fiber {
        tag: ComponentType;      // 组件类型
        child: Fiber | null;     // 第一个子节点
        sibling: Fiber | null;   // 下一个兄弟节点
        return: Fiber | null;    // 父节点
        alternate: Fiber | null; // 指向另一棵树(双缓存)
        flags: number;           // 副作用标记(增/删/更新)
        lanes: Lanes;            // 优先级
        // ...其他字段(stateNode、props等)
      }
      
    • 双缓存机制

      • Current树:当前渲染的树(对应屏幕显示内容)。
      • WorkInProgress树:正在构建的新树,完成后替换Current树。
      • 优势:避免渲染中间状态导致的UI闪烁。

三、Fiber架构的核心改进
维度传统虚拟DOMFiber架构
遍历控制递归(不可中断)迭代(可中断 + 恢复)
数据结构隐式调用栈显式链表(child/sibling/return)
任务调度同步执行优先级调度 + 时间分片
性能优化易阻塞主线程增量渲染,避免卡顿
扩展能力有限支持并发模式(Suspense/Transition)

四、Fiber遍历流程示例

假设组件树结构为:

A
├─ B
│  ├─ C
│  └─ D
└─ E
   └─ F

遍历顺序

  1. 进入A → 处理A
  2. 进入A.child B → 处理B
  3. 进入B.child C → 处理C
  4. C无子节点 → 完成C,回溯到B
  5. 进入B.sibling D → 处理D
  6. D无子节点 → 完成D,回溯到B → 完成B,回溯到A
  7. 进入A.sibling E → 处理E
  8. 进入E.child F → 处理F
  9. F无子节点 → 完成F,回溯到E → 完成E,回溯到A → 完成A

五、Fiber架构的优势
  1. 可中断渲染:高优先级任务(如用户输入)可打断低优先级渲染。
  2. 增量更新:将渲染任务拆分为多个帧执行,避免主线程阻塞。
  3. 精准副作用提交:通过 flags 标记变更,一次性提交DOM操作。
  4. 并发模式支持:实现服务端渲染流式输出、Suspense等高级特性。

总结

  • 传统虚拟DOM:深度优先遍历 + 递归调用栈,简单但不可中断。
  • Fiber架构:深度优先遍历 + 显式链表结构,通过迭代实现可中断渲染,结合优先级调度与双缓存机制,为React带来革命性性能提升与扩展能力。
  • 核心价值:将同步渲染转化为异步可调度任务,使复杂应用保持流畅交互。

41.React如何创建工程环境(js,ts),eject的作用是什么?


一、创建React工程环境

1. JavaScript项目

使用 Create React App (CRA) 快速搭建React项目:

npx create-react-app my-app
  • 这会生成一个默认的JavaScript项目,包含基础配置(Webpack、Babel等),无需手动配置。
2. TypeScript项目

在创建时通过--template typescript指定TypeScript模板:

npx create-react-app my-app --template typescript
  • 或对已有JS项目添加TypeScript支持:

    npm install --save typescript @types/react @types/react-dom
    

    将文件后缀改为.tsx.ts,CRA会自动识别并配置TypeScript。


二、eject的作用

1. 功能
  • 暴露隐藏配置:运行npm run eject会将CRA封装的配置(如Webpack、Babel、ESLint)完全解压到项目目录,允许直接修改。
  • 不可逆操作:一旦执行,无法回退到CRA的默认封装状态。
2. 使用场景
  • 需要深度定制构建工具(如修改Webpack配置、添加插件)。
  • CRA默认配置无法满足项目需求(如自定义代码分割规则)。
3. 注意事项
  • 维护成本:需自行管理所有配置,增加复杂性。
  • 替代方案:优先考虑非侵入式工具(如react-app-rewired@craco/craco)覆盖配置,避免eject

三、总结

  • 创建项目:CRA是官方推荐工具,支持JS/TS开箱即用。
  • eject:谨慎使用,仅在必要时暴露配置,否则优先选择灵活配置方案。

42.React常见hooks有哪些


React常见Hooks及用途

React Hooks 是函数组件中管理状态、副作用和其他功能的工具。以下是常用Hooks及其核心作用:

1. useState
  • 用途:管理组件内部状态。

  • 示例

    const [count, setCount] = useState(0);
    
2. useEffect
  • 用途:处理副作用(如数据请求、订阅、DOM操作)。

  • 依赖控制:通过第二个参数(依赖数组)控制执行时机。

    useEffect(() => {
      fetchData();
      return () => { /* 清理逻辑 */ };
    }, [dependency]);
    
3. useContext
  • 用途:跨组件共享数据(如主题、用户信息),避免逐层传递props。

  • 示例

    const theme = useContext(ThemeContext);
    
4. useReducer
  • 用途:管理复杂状态逻辑,类似Redux的Reducer模式。

  • 适用场景:状态更新涉及多步骤或依赖之前状态。

    const [state, dispatch] = useReducer(reducer, initialState);
    
5. useRef
  • 用途

    • 访问DOM节点(如聚焦输入框)。
    • 保存可变值(如定时器ID),不触发重渲染。
  • 示例

    const inputRef = useRef(null);
    <input ref={inputRef} />
    
6. useMemo
  • 用途:缓存计算结果,避免重复计算。

  • 优化场景:依赖项未变化时复用缓存值。

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    
7. useCallback
  • 用途:缓存函数,避免子组件因函数引用变化而重渲染。

  • 示例

    const handleClick = useCallback(() => { doSomething(a) }, [a]);
    
8. useLayoutEffect
  • 用途:与useEffect类似,但同步执行(在DOM更新后、浏览器绘制前)。
  • 适用场景:需要直接操作DOM或避免视觉抖动。
其他Hooks
  • useImperativeHandle:自定义暴露给父组件的ref实例。
  • useDebugValue:在React开发者工具中显示自定义Hook的标签。

总结

  • 核心HooksuseStateuseEffectuseContextuseReduceruseRef
  • 性能优化useMemouseCallback
  • 高级场景useLayoutEffectuseImperativeHandle
  • 自定义Hook:封装可复用的逻辑(如useFetch)。

43.React中函数式组件如何模拟生命周期

类组件生命周期函数式组件实现方式
componentDidMountuseEffect + 空依赖数组
componentDidUpdateuseEffect + 监听依赖项
componentWillUnmountuseEffect 的清理函数
shouldComponentUpdateReact.memouseMemo
getDerivedStateFromPropsuseState + useEffect

44.React如何通过hooks通过数据接口,访问数据?


React通过Hooks访问数据接口的实现步骤

在React中,使用Hooks访问数据接口通常涉及以下步骤,结合状态管理、副作用处理及异步操作:


1. 使用useState管理数据状态

定义状态变量存储数据、加载状态及错误信息:

const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

2. 使用useEffect触发数据请求

useEffect中发起异步请求,处理数据获取逻辑:

useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      if (!response.ok) throw new Error('请求失败');
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  fetchData();
}, []); // 空依赖数组表示仅在组件挂载时执行

3. 处理组件卸载时的请求中断(可选)

使用AbortController取消未完成的请求,避免内存泄漏:

useEffect(() => {
  const abortController = new AbortController();

  const fetchData = async () => {
    try {
      const response = await fetch('https://api.example.com/data', {
        signal: abortController.signal,
      });
      // ...处理数据
    } catch (err) {
      if (err.name !== 'AbortError') setError(err.message);
    }
  };

  fetchData();
  return () => abortController.abort(); // 清理函数中中断请求
}, []);

4. 展示数据与状态

根据状态渲染UI:

return (
  <div>
    {isLoading ? (
      <div>加载中...</div>
    ) : error ? (
      <div>错误:{error}</div>
    ) : (
      <div>{JSON.stringify(data)}</div>
    )}
  </div>
);

5. 自定义Hook封装(进阶)

将数据获取逻辑抽象为可复用的自定义Hook:

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, isLoading, error };
};

// 在组件中使用
const MyComponent = () => {
  const { data, isLoading, error } = useFetch('https://api.example.com/data');
  // ...渲染逻辑
};

6. 使用TypeScript定义数据类型(可选)

为接口返回数据添加类型约束:

interface ApiData {
  id: number;
  name: string;
}

const useFetch = (url: string) => {
  const [data, setData] = useState<ApiData | null>(null);
  // ...其他逻辑
};

关键点总结

  • 状态管理useState管理数据、加载状态和错误。
  • 副作用控制useEffect处理异步请求,依赖项控制触发时机。
  • 请求中断AbortController避免组件卸载后更新状态导致的内存泄漏。
  • 自定义Hook:封装逻辑提升复用性。
  • 类型安全:TypeScript确保数据结构正确性。

45.React hook的使用,有哪些注意方式?为什么会有hooks?


React Hooks 使用注意事项

1. 遵守Hooks的调用规则
  • 只在顶层调用
    不可在条件、循环或嵌套函数中使用Hooks,确保每次渲染时Hooks的调用顺序一致

    // 错误示例:条件中使用Hook
    if (condition) {
      const [state, setState] = useState(); // 会导致后续Hooks顺序错乱
    }
    
  • 仅用于React函数组件或自定义Hooks
    不可在普通JavaScript函数中调用Hooks。

2. 正确处理依赖数组
  • useEffectuseMemouseCallback的依赖项
    明确列出所有外部依赖,避免闭包陷阱或过时数据。

    useEffect(() => {
      fetchData(id); // 若依赖id,需将其加入依赖数组
    }, [id]); 
    
  • 空依赖数组的用途
    仅在组件挂载时执行一次(模拟componentDidMount)。

3. 性能优化
  • 避免不必要的渲染
    使用React.memouseMemouseCallback减少子组件重复渲染。

    const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
    const handleClick = useCallback(() => action(a), [a]);
    
  • 避免滥用useState
    合并相关状态,减少渲染次数。

    // 合并为对象
    const [user, setUser] = useState({ name: 'Alice', age: 20 });
    
4. 清理副作用
  • useEffect的清理函数
    取消订阅、定时器或网络请求,防止内存泄漏。

    useEffect(() => {
      const timer = setInterval(() => {}, 1000);
      return () => clearInterval(timer);
    }, []);
    
5. 自定义Hooks规范
  • 命名以use开头
    便于React识别并应用Hooks规则,例如useFetchuseLocalStorage

为什么需要Hooks?

1. 解决类组件的痛点
  • 逻辑复用困难
    类组件中复用状态逻辑需通过高阶组件(HOC)或Render Props,导致“嵌套地狱”。
  • 生命周期方法分散逻辑
    相关代码分散在componentDidMountcomponentDidUpdate等生命周期中,难以维护。
  • this指向问题
    类组件中需要绑定this,增加代码复杂度。
2. 函数组件的增强
  • 赋予函数组件状态能力
    通过useStateuseEffect等Hooks,函数组件可管理状态和副作用,无需转换为类组件。
  • 逻辑聚合
    将相关逻辑集中到同一Hook中,提升代码可读性(如将数据请求与状态管理封装为useFetch)。
3. 更简洁的代码结构
  • 减少模板代码
    避免类组件的构造函数、生命周期方法等冗余代码。
  • 函数式编程优势
    更易编写纯函数,方便测试和调试。
4. 社区与未来趋势
  • 函数式编程普及
    Hooks推动React向函数式范式发展,与现代JavaScript生态更契合。
  • 渐进式迁移
    支持在现有类组件中逐步引入Hooks,降低重构成本。

总结

注意事项设计动机
调用顺序一致性解决类组件的逻辑复用与生命周期碎片化
依赖数组精确管理简化状态管理与副作用控制
性能优化与副作用清理提升代码可维护性与可读性
自定义Hooks规范推动函数式组件成为主流开发模式

46.React是如何获取组件对应的DOM元素?


React获取组件对应DOM元素的方法

在React中,通常不推荐直接操作DOM,但在需要访问特定元素(如管理焦点、集成第三方库)时,可通过以下方式获取:


1. 使用ref属性

核心方法:通过ref绑定到JSX元素,获取其对应的DOM节点。

类组件
  • React.createRef()
    创建ref对象并附加到元素:

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.myRef = React.createRef();
      }
    
      componentDidMount() {
        // 访问DOM节点
        console.log(this.myRef.current); // 输出对应的DOM元素
      }
    
      render() {
        return <div ref={this.myRef}>Hello</div>;
      }
    }
    
函数组件
  • useRef Hook
    创建可持久化的ref对象:

    import { useRef, useEffect } from 'react';
    
    function MyComponent() {
      const myRef = useRef(null);
    
      useEffect(() => {
        console.log(myRef.current); // 组件挂载后访问
      }, []);
    
      return <div ref={myRef}>Hello</div>;
    }
    

2. 回调Ref(动态绑定)

通过函数接收DOM元素,适用于动态绑定或类组件:

class MyComponent extends React.Component {
  setRef = (element) => {
    this.myRef = element; // 直接保存DOM元素
  };

  render() {
    return <div ref={this.setRef}>Hello</div>;
  }
}

3. 转发Ref(访问子组件DOM)

当需要获取子组件的DOM时,使用React.forwardRef

子组件(支持Ref转发)
const ChildComponent = React.forwardRef((props, ref) => {
  return <div ref={ref}>{props.children}</div>;
});
父组件
function ParentComponent() {
  const childRef = useRef(null);

  useEffect(() => {
    console.log(childRef.current); // 子组件的DOM元素
  }, []);

  return <ChildComponent ref={childRef}>Child</ChildComponent>;
}

4. 函数组件暴露方法(useImperativeHandle

控制子组件暴露给父组件的实例方法,而非直接暴露DOM:

const ChildComponent = React.forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
  }));

  return <input ref={inputRef} />;
});

// 父组件调用子组件的focus方法
function ParentComponent() {
  const childRef = useRef();

  return (
    <>
      <ChildComponent ref={childRef} />
      <button onClick={() => childRef.current.focus()}>聚焦输入框</button>
    </>
  );
}

注意事项

  1. 访问时机
    Ref的current属性在组件挂载后才有效,应在useEffectcomponentDidMount中访问。
  2. 避免滥用
    直接操作DOM可能破坏React的声明式特性,优先通过状态(State/Props)控制UI。
  3. 清理Ref
    若在Ref中保存了订阅或定时器,需在useEffect的清理函数或componentWillUnmount中释放资源。

总结

场景方法
类组件获取DOMReact.createRef()
函数组件获取DOMuseRef
动态绑定DOM回调Ref(函数形式)
访问子组件DOMReact.forwardRef
控制子组件暴露的实例useImperativeHandle + forwardRef

47.React什么是状态提升,子父组件如何通信?

在React中,状态提升(State Lifting)子父组件通信是组件间数据交互的核心机制


1. 状态提升(State Lifting)

概念

状态提升是指将多个子组件需要共享的状态(State)移动到它们最近的共同父组件中,通过props向下传递数据,再通过回调函数让子组件通知父组件更新状态。这种方式遵循单向数据流原则,确保状态管理的可预测性和一致性。

适用场景
  • 多个组件需要同步同一份数据(如温度转换器、表单联动输入)。
  • 需要集中管理状态以避免冗余或冲突。
实现步骤
  1. 提升状态:将共享状态定义在父组件中。
  2. 传递数据:通过props将状态传递给子组件。
  3. 传递回调:父组件将更新状态的函数通过props传给子组件,子组件触发回调以更新父组件状态。
示例
// 父组件
function TemperatureConverter() {
  const [celsius, setCelsius] = useState(0);

  return (
    <div>
      <CelsiusInput value={celsius} onChange={setCelsius} />
      <FahrenheitDisplay celsius={celsius} />
    </div>
  );
}

// 子组件1:输入摄氏度
function CelsiusInput({ value, onChange }) {
  return (
    <input
      type="number"
      value={value}
      onChange={(e) => onChange(Number(e.target.value))}
    />
  );
}

// 子组件2:显示华氏度
function FahrenheitDisplay({ celsius }) {
  const fahrenheit = celsius * 9 / 5 + 32;
  return <div>{fahrenheit}°F</div>;
}

2. 子父组件通信

核心机制

子组件通过调用父组件传递的回调函数(通过props)与父组件通信。父组件定义状态更新逻辑,子组件触发回调并传递数据。

实现步骤
  1. 父组件定义回调:在父组件中编写状态更新函数(如handleChange)。
  2. 传递回调给子组件:通过props将回调函数传递给子组件。
  3. 子组件触发回调:子组件在特定事件(如用户输入)中调用回调,并传递参数。
示例
// 父组件
function Parent() {
  const [data, setData] = useState("");

  const handleChildData = (childData) => {
    setData(childData);
  };

  return <Child onSendData={handleChildData} />;
}

// 子组件
function Child({ onSendData }) {
  const sendData = () => {
    onSendData("Data from Child");
  };

  return <button onClick={sendData}>Send Data to Parent</button>;
}

关键点总结

  • 状态提升:共享状态应置于共同父组件,通过props下发,保持单一数据源。
  • 子父通信:子组件通过父组件传递的回调函数通知状态变更。
  • 优势:遵循单向数据流,状态更易追踪,减少冗余和冲突。

通过这种方式,React应用能够实现高效、可维护的组件间通信,适用于复杂场景下的状态管理。

48.React 你用的react是什么版本,react版本有什么区别?


React 版本及区别

1. 我使用的 React 版本
目前使用的是 React 18,但如果项目需要最新特性,我也会关注 React 19 的更新和优化点。

2. 主要 React 版本的区别

版本主要区别
React 16 (2017)- 引入 Fiber 架构,优化渲染性能 - React.createRef() 方式创建 ref - 新的生命周期方法,如 getDerivedStateFromPropscomponentDidCatch (错误边界)
React 17 (2020)- 主要是渐进式升级,没有新特性 - 事件系统更新,支持事件委托至 root 而非 document
React 18 (2022)- 并发渲染(Concurrent Rendering) - useTransitionuseDeferredValue 以优化渲染体验 - React.startTransition 用于标记非紧急更新 - createRoot() 替代 ReactDOM.render()
React 19 (2024)- useOptimistic:用于优化状态更新,使 UI 响应更流畅 - useFormStatus & useFormState:改进表单状态管理 - React Compiler(实验性):更智能的代码优化 - React Server Components (RSC) 更稳定,可与 Next.js 结合优化渲染

3. React 版本对项目的影响

  • 如果是老项目(React 16/17),升级到 React 18 是较稳妥的选择,因为它带来了显著的性能优化,但不会影响现有代码。
  • 如果是新项目,可以考虑直接使用 React 19,尤其是在 Server Components 方面有更好的支持,并能提升用户体验。

这样能展现出对 React 版本的全面了解,并结合实际项目需求进行分析。

49.React中setState到底是同步还是异步?

如果我是前端面试者,我会这样回答:


React 中 setState 是同步还是异步?

React 的 setState 既不是完全同步,也不是完全异步,它的行为取决于执行环境

  1. 在 React 事件处理(如 onClick)或生命周期函数中setState 是异步的,React 会批量更新以提高性能。
  2. 在原生事件(如 setTimeoutPromise.then)或非 React 控制的回调中setState 是同步的,状态会立即更新。

示例 1:React 事件中 setState 体现异步性

import React, { useState } from "react";

const Example = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("Before setState:", count); // 0
    setCount(count + 1);
    console.log("After setState:", count);  // 仍然是 0(未立即更新)
  };

  return <button onClick={handleClick}>Count: {count}</button>;
};

export default Example;

解释

  • setState(count + 1) 触发更新,但不会立即修改 count 变量,因为 React 在事件处理中会批量更新以优化渲染。
  • console.log("After setState:", count); 仍然是旧值。

示例 2:在 setTimeoutPromisesetState 是同步的

import React, { useState, useEffect } from "react";

const Example = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      console.log("Before setState:", count); // 0
      setCount(count + 1);
      console.log("After setState:", count);  // 仍然是 0(闭包问题)
    }, 1000);
  }, []);

  return <p>Count: {count}</p>;
};

export default Example;

解释

  • setTimeout 里,setState 不会被批量处理,所以它的执行方式与 JavaScript 的普通函数类似。
  • 注意闭包问题countsetTimeout 内部保持初始值(即 0),导致 setCount(count + 1) 其实等于 setCount(1)
  • 解决方案:使用 setCount(prev => prev + 1),这样 prev 总是最新值。

React 18 及以上:批量更新

React 18 默认开启批处理,即使 setTimeoutPromisesetState 也会异步执行:

import React, { useState } from "react";

const Example = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    Promise.resolve().then(() => {
      setCount((prev) => prev + 1);
      setCount((prev) => prev + 1);
    });
  };

  return <button onClick={handleClick}>Count: {count}</button>;
};

export default Example;
  • React 18 之前:两次 setCount 会执行两次更新,最终 count 只加 1。
  • React 18 之后:React 自动批处理,最终 count 会加 2。

总结

场景setState 是同步还是异步?说明
React 事件处理异步React 会批量更新,优化性能
生命周期函数异步componentDidMountuseEffect
setTimeoutPromiseReact 17 及以前同步,React 18 及以上异步React 18 开启了自动批处理
useTransitionstartTransition异步标记低优先级更新,提高流畅度

最佳实践

  1. 要获取最新的 state,使用回调形式
setCount(prev => prev + 1);
  1. React 18 需要 flushSync() 强制同步
import { flushSync } from "react-dom";

flushSync(() => {
  setCount(count + 1);
});
console.log(count); // 立即获取新值

总结一句话

setState 在 React 事件和生命周期中是 异步的setTimeout 等原生异步任务中是 同步的(React 18 之后默认也是异步的)。

50.在React如何实现Vue中的expose的能力?


如何在 React 中实现 Vue 3 的 expose 能力?

在 Vue 3 中,expose 允许子组件显式暴露特定的属性或方法,避免父组件访问子组件整个实例。例如:

<script setup>
import { ref, defineExpose } from "vue";

const count = ref(0);
const increment = () => count.value++;

defineExpose({ count, increment }); // 仅暴露 count 和 increment
</script>

React 中如何实现类似能力?

React 没有 expose 这个 API,但可以通过 forwardRef + useImperativeHandle 组合实现类似的功能,使父组件只能访问子组件暴露的方法,而非整个子组件实例。

示例:使用 forwardRef + useImperativeHandle
import React, { useState, forwardRef, useImperativeHandle } from "react";

// 子组件
const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount((prev) => prev + 1);

  // 仅暴露 `increment` 方法
  useImperativeHandle(ref, () => ({
    increment,
  }));

  return <p>Count: {count}</p>;
});

// 父组件
const Parent = () => {
  const childRef = React.useRef(null);

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={() => childRef.current?.increment()}>增加</button>
    </div>
  );
};

export default Parent;

对比 Vue expose 和 React useImperativeHandle

框架API作用
Vue 3defineExpose仅暴露指定属性和方法给父组件
ReactuseImperativeHandle通过 ref 仅暴露特定方法,避免父组件访问不必要的状态

总结

在 React 中,useImperativeHandle + forwardRef 可以实现 Vue expose 的能力,确保父组件只访问必要的方法,而不是整个子组件实例。这在 封装组件库或希望控制组件暴露接口 时非常有用。

51.useLayoutEffect和useEffect 有什么区别呢?

1. useLayoutEffect 基本概念

useLayoutEffect 是 React 的一个 Hook,它的函数签名与 useEffect 完全相同,但它会在所有的 DOM 变更之后同步调用 effect。它可以用来读取 DOM 布局并同步触发重渲染。

2. useLayoutEffect vs useEffect

2.1 执行时机对比

Hook 名称执行时机执行方式使用场景
useEffectDOM 更新后且浏览器重新绘制屏幕之后异步执行 (组件渲染完成后)异步执行,不阻塞浏览器渲染大多数副作用,如数据获取、订阅
useLayoutEffectDOM 更新后且浏览器重新绘制屏幕之前同步执行(组件将要渲染时)同步执行,会阻塞浏览器渲染需要同步测量 DOM 或更新布局

2.2 执行顺序示例

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

  useEffect(() => {
    console.log('useEffect 执行'); // 后执行
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行'); // 先执行
  });

  return (
    <div onClick={() => setCount(c => c + 1)}>
      点击次数:{count}
    </div>
  );
}

3.1 何时使用 useLayoutEffect

  • 需要同步测量 DOM 元素
  • 需要在视觉更新前进行 DOM 修改
  • 需要避免闪烁或布局抖动
  • 处理依赖于 DOM 布局的动画

3.2 何时使用 useEffect

  • 数据获取
  • 订阅事件
  • 日志记录
  • 其他不需要同步 DOM 测量或修改的副作用

4. 最佳实践

  1. 优先使用 useEffect
// ✅ 大多数情况下使用 useEffect 即可
useEffect(() => {
  // 异步操作,不影响渲染
  fetchData();
}, []);
  1. 仅在必要时使用 useLayoutEffect
// ✅ 需要同步 DOM 测量和更新时使用 useLayoutEffect
useLayoutEffect(() => {
  // 同步操作,立即更新 DOM
  updateDOMPosition();
}, []);
  1. 注意性能影响
// ❌ 避免在 useLayoutEffect 中进行耗时操作
useLayoutEffect(() => {
  // 不要在这里进行大量计算或 API 调用
  heavyComputation();
}, []);

// ✅ 耗时操作应该放在 useEffect 中
useEffect(() => {
  heavyComputation();
}, []);

6. 注意事项

  1. useLayoutEffect 在服务器端渲染(SSR)中会收到警告,因为它只能在客户端执行
  2. 过度使用 useLayoutEffect 可能会导致性能问题
  3. 应该将耗时的操作放在 useEffect 中,只在 useLayoutEffect 中处理视觉相关的同步更新

52.React如何实现一个 withRouter?(React Router v6 之后,withRouter 被移除)选背

如果我是前端面试者,我会这样回答:


如何在 React 中实现 withRouter

在 React Router v5 及之前,withRouter 是一个高阶组件(HOC),用于向类组件提供 historylocationmatch 这三个路由对象。

但在 React Router v6 之后,withRouter 被移除,官方推荐使用 useNavigateuseLocation 等 Hook 直接在函数组件中使用。


1. 在 React Router v5 中手写 withRouter

实现一个 withRouter 高阶组件

import { withRouter } from "react-router-dom";

const MyComponent = ({ history, location, match }) => {
  return (
    <div>
      <p>Current Path: {location.pathname}</p>
      <button onClick={() => history.push("/home")}>Go Home</button>
    </div>
  );
};

export default withRouter(MyComponent);

手写实现 withRouter
其实就是写一个叫withRouter的HOC,传入组件component,将historylocationmatch用属性代理的方式传入然后返回新的component,本质是一个高阶组件的用法

import { useNavigate, useLocation, useParams } from "react-router-dom";

const withRouter = (Component) => {
  return (props) => {
    const navigate = useNavigate();
    const location = useLocation();
    const params = useParams();
    
    return <Component {...props} navigate={navigate} location={location} params={params} />;
  };
};

export default withRouter;

使用方式

const MyComponent = ({ navigate, location, params }) => {
  return (
    <div>
      <p>Current Path: {location.pathname}</p>
      <button onClick={() => navigate("/home")}>Go Home</button>
    </div>
  );
};

export default withRouter(MyComponent);

2. 在 React Router v6 中替代 withRouter

React Router v6 移除了 withRouter,推荐直接在函数组件中使用 useNavigateuseLocation

import { useNavigate, useLocation, useParams } from "react-router-dom";

const MyComponent = () => {
  const navigate = useNavigate();
  const location = useLocation();
  const params = useParams();

  return (
    <div>
      <p>Current Path: {location.pathname}</p>
      <button onClick={() => navigate("/home")}>Go Home</button>
    </div>
  );
};

export default MyComponent;

3. withRouter 适用场景

虽然在 React Router v6 中推荐使用 Hook,但如果项目中仍然使用 类组件,可以用 withRouter 来注入路由信息:

import React from "react";
import { withRouter } from "./withRouter"; // 之前实现的手写 HOC

class ClassComponent extends React.Component {
  render() {
    return (
      <div>
        <p>Current Path: {this.props.location.pathname}</p>
        <button onClick={() => this.props.navigate("/home")}>Go Home</button>
      </div>
    );
  }
}

export default withRouter(ClassComponent);

总结

  1. React Router v5 及之前withRouter 是官方提供的 HOC,可用于类组件。
  2. React Router v6 及以后:推荐使用 useNavigateuseLocation 等 Hook,直接在函数组件中获取路由信息。
  3. 如果必须支持类组件:可以自己手写 withRouter,使用 useNavigateuseLocation 封装 HOC。

53.react-router-dom v6 提供了哪些新的API?

总结

API作用替代的旧 API
createBrowserRouter新的路由创建方式<BrowserRouter>
useRoutes动态生成路由<Routes>
useNavigate进行页面跳转useHistory().push()
useSearchParams处理 URL 查询参数window.location.search
useLoaderData加载数据useEffect
Outlet处理嵌套路由children
useParams获取 URL 参数match.params
Navigate重定向Redirect
**

React Router v6 新 API 介绍

React Router v6 于 2021 年发布,相比 v5 进行了重大改进,包括 API 变化、更简洁的写法以及更强大的功能。


1. createBrowserRouter / createHashRouter

用于替代 <BrowserRouter>,支持数据加载和路由控制。

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "/about",
    element: <About />,
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

优势

  • 允许在路由定义时加载数据
  • 适用于 React 18 的流式渲染

2. useRoutes

动态创建路由,替代 <Switch>

import { useRoutes } from "react-router-dom";

const routes = [
  { path: "/", element: <Home /> },
  { path: "/about", element: <About /> },
];

const App = () => {
  const element = useRoutes(routes);
  return element;
};

优势

  • 更简洁,不需要手写 <Routes><Route>
  • 灵活可配置,适用于动态路由

3. useNavigate

替代 useHistory,进行导航跳转

import { useNavigate } from "react-router-dom";

const Home = () => {
  const navigate = useNavigate();
  
  return <button onClick={() => navigate("/about")}>Go to About</button>;
};

优势

  • 更清晰,不再需要 history.push()
  • 支持向前/向后跳转navigate(-1)

4. useSearchParams

用于管理 URL 查询参数

import { useSearchParams } from "react-router-dom";

const Users = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const userId = searchParams.get("id");

  return (
    <div>
      <p>User ID: {userId}</p>
      <button onClick={() => setSearchParams({ id: "123" })}>Set ID</button>
    </div>
  );
};

优势

  • 替代 query-string,更简洁
  • 直接修改 URL,无需手动拼接参数

5. useLoaderData

配合 loader 进行数据加载

import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router-dom";

const fetchUser = async () => {
  const res = await fetch("/api/user");
  return res.json();
};

const Profile = () => {
  const user = useLoaderData();
  return <p>{user.name}</p>;
};

const router = createBrowserRouter([
  {
    path: "/profile",
    element: <Profile />,
    loader: fetchUser,
  },
]);

const App = () => <RouterProvider router={router} />;

优势

  • SSR 友好,支持数据预加载
  • 简化数据获取,无需 useEffect

6. Outlet

用于嵌套路由

const Layout = () => (
  <div>
    <h1>Header</h1>
    <Outlet /> {/* 这里会渲染子路由 */}
  </div>
);

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      { path: "home", element: <Home /> },
      { path: "about", element: <About /> },
    ],
  },
]);

优势

  • 清晰管理嵌套路由
  • 更符合组件化设计

7. useParams

获取动态路由参数

import { useParams } from "react-router-dom";

const User = () => {
  const { id } = useParams();
  return <p>User ID: {id}</p>;
};

// 路由配置: path: "/user/:id"

优势

  • 更简单,无需 match.params
  • useNavigate 结合使用,体验更佳

8. Navigate 组件

替代 Redirect

import { Navigate } from "react-router-dom";

const PrivateRoute = ({ isAuth }) => {
  return isAuth ? <Dashboard /> : <Navigate to="/login" />;
};

优势

  • 更符合 JSX 语法
  • 清晰的重定向逻辑

54.useRoutes是如何使用的?如何使用useRoutes进行动态路由加载

如果我是前端面试者,我会这样回答:


1. useRoutes 介绍

useRoutesReact Router v6 提供的一个 Hook,用于基于配置动态生成路由,它替代了 v5 版本的 <Routes> + <Route> 组合,让路由更加声明式和清晰。


2. useRoutes 基本用法

替代 <Routes> 直接定义路由

传统 <Routes> 方式(v5/v6)

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";

const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  </BrowserRouter>
);

使用 useRoutes 方式(v6 推荐)

import { BrowserRouter, useRoutes } from "react-router-dom";
import Home from "./Home";
import About from "./About";

const routes = [
  { path: "/", element: <Home /> },
  { path: "/about", element: <About /> },
];

const AppRoutes = () => {
  return useRoutes(routes);
};

const App = () => (
  <BrowserRouter>
    <AppRoutes />
  </BrowserRouter>
);

优势

  • 让路由更加模块化,避免在 App.tsx 里堆积 <Route>
  • 适用于动态路由加载

3. useRoutes 处理嵌套路由

如果有子路由,可以使用 children 进行嵌套:

import { BrowserRouter, useRoutes, Outlet } from "react-router-dom";

const Layout = () => (
  <div>
    <h1>Header</h1>
    <Outlet /> {/* 子路由会渲染到这里 */}
  </div>
);

const routes = [
  {
    path: "/",
    element: <Layout />,
    children: [
      { path: "home", element: <Home /> },
      { path: "about", element: <About /> },
    ],
  },
];

const AppRoutes = () => useRoutes(routes);

const App = () => (
  <BrowserRouter>
    <AppRoutes />
  </BrowserRouter>
);

优势

  • Outlet 允许嵌套渲染子路由,替代 this.props.children
  • 更容易管理多级路由

4. useRoutes 进行动态路由加载

在实际项目中,通常需要按需加载路由组件,减少首屏加载时间。可以结合 React.lazy + Suspense 实现动态导入:

🚀 使用 React.lazy 进行按需加载

import { BrowserRouter, useRoutes } from "react-router-dom";
import { lazy, Suspense } from "react";

// 动态导入组件
const Home = lazy(() => import("./Home"));
const About = lazy(() => import("./About"));

const routes = [
  { path: "/", element: <Home /> },
  { path: "/about", element: <About /> },
];

const AppRoutes = () => useRoutes(routes);

const App = () => (
  <BrowserRouter>
    <Suspense fallback={<div>Loading...</div>}>
      <AppRoutes />
    </Suspense>
  </BrowserRouter>
);

优势

  • 仅在访问对应页面时才加载组件,提升首屏加载速度
  • 适用于大型项目,减少初始打包体积

5. useRoutes 结合后端动态生成路由

在某些场景下,前端的路由数据可能是由后端返回的。例如,后端返回用户可访问的菜单,我们可以根据 API 返回的路由表动态生成路由:

import { BrowserRouter, useRoutes } from "react-router-dom";
import { useEffect, useState } from "react";

const fetchRoutesFromAPI = async () => {
  return [
    { path: "/", element: <Home /> },
    { path: "/about", element: <About /> },
  ];
};

const AppRoutes = () => {
  const [routes, setRoutes] = useState([]);

  useEffect(() => {
    fetchRoutesFromAPI().then(setRoutes);
  }, []);

  return useRoutes(routes);
};

const App = () => (
  <BrowserRouter>
    <AppRoutes />
  </BrowserRouter>
);

优势

  • 适用于权限管理,根据用户权限动态生成可访问路由
  • 后端驱动路由,前端无需硬编码

6. useRoutes vs 传统 <Routes>

特性useRoutes<Routes>
代码简洁度✅ 更清晰,避免 JSX 嵌套❌ 需要多个 <Route>
动态加载✅ 支持 React.lazy❌ 需要手动 Suspense
动态路由✅ 适用于 API 获取路由❌ 需手动 map 生成 <Route>
嵌套路由children 方式✅ 但更麻烦
适用场景✅ 组件化、大型项目✅ 适合小项目

7. 总结

  1. useRoutes 让路由声明更加清晰,避免 JSX 里嵌套 <Route>
  2. 适用于动态路由,可以从后端 API 加载路由表并渲染。
  3. 结合 React.lazy 实现懒加载,提高首屏加载速度。
  4. 推荐在 React Router v6 中使用,特别是当路由结构较复杂时。

useRoutes 适用于大型项目,配合动态加载、权限管理等方案,能显著优化路由管理!

55.redux的中间件是如何实现的?

redux中间件是指在redux发出action后执行Reducer改变state之前劫持action做出其他开发者想操作的多余步骤如打印log,接受函数式dispatch,处理异步操作或者promise等然后再分发action到reducer改变 state。
实现原理是通过 **applyMiddleware**方法将中间件从右左形成中间件函数调用链,一层一层的调用中间件,最好执行真正的dispatch去出发Reducer。同时将store和 dispatch重写放入调用链中增强dispatch功能。

子问题:

applyMiddleware为什么要从右往左进行反向组装中间件链:从右到左依次包装 dispatch

Redux的applyMiddleware从右到左反向组装中间件链,是为了确保中间件的执行顺序与用户传入的顺序一致(从左到右) 。这种设计源于函数式编程的组合逻辑:每个中间件接收的next参数代表链中下一个中间件处理后的dispatch,通过从右到左依次包装,最终形成的调用链会按照中间件的声明顺序(从左到右)依次执行。例如,中间件[A, B, C]的组合顺序是C → B → A,但执行时A最先处理Action,调用next后触发B,再调用next触发C,最后到达原始dispatch,从而保证用户直观的中间件执行顺序。
具体的实现原理如下


Redux 中间件的作用

在 Redux 中,中间件(Middleware)用于扩展 dispatch 方法的功能,可以在派发(dispatch)ActionReducer 处理之前执行额外的逻辑,比如:

  • 处理 异步操作(如 redux-thunkredux-saga
  • 日志记录(如 redux-logger
  • 拦截/修改 action(如 redux-promise
  • 错误处理

Redux 中间件的核心在于增强 dispatch 方法,使其能够处理函数、Promise 等异步操作,而不仅仅是普通的对象。


Redux 中间件的实现原理

Redux 的中间件是基于函数式编程高阶函数(Higher-Order Function)
一个 Redux 中间件的基本结构如下:

const exampleMiddleware = (store) => (next) => (action) => {
  console.log("中间件触发:", action);
  return next(action); // 继续传递 action
};

解析:

  1. store —— Redux store 对象,包含 getState()dispatch() 方法。
  2. next —— 代表下一个中间件,或者最终到达 reducer
  3. action —— 被 dispatch 的 action。

核心逻辑

  • dispatch(action) 之后,中间件先拦截 action,可以对 action 进行修改、日志记录、异步处理等操作。
  • 调用 next(action) 将 action 传递给下一个中间件,最终到达 reducer。
  • 如果不调用 next(action),Redux 流程就会被中断(用于拦截某些 action)。

手写 Redux 中间件机制

1️⃣ Redux applyMiddleware 实现

Redux 提供 applyMiddleware 这个方法,它的核心逻辑如下:

const applyMiddleware = (...middlewares) => (createStore) => (reducer) => {
  const store = createStore(reducer); // 创建 Redux store
  let dispatch = store.dispatch; // 原始 dispatch 方法
  const middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action), // 让中间件能够调用 dispatch
  };
  
  // 依次执行每个中间件,得到增强后的 dispatch
  const chain = middlewares.map((middleware) => middleware(middlewareAPI));
  dispatch = chain.reduceRight((next, middleware) => middleware(next), store.dispatch);
  
  return { ...store, dispatch }; // 返回增强后的 store
};

核心流程

  1. 先创建 Redux store,获取原始 dispatch 方法。
  2. 给中间件传递 middlewareAPIgetStatedispatch)。
  3. 链式调用所有中间件,最终生成增强版 dispatch,替换 Redux 默认的 dispatch

2️⃣ 使用 applyMiddleware 来增强 Redux

import { createStore, applyMiddleware } from "redux";

// 自定义一个日志中间件
const loggerMiddleware = (store) => (next) => (action) => {
  console.log("当前状态:", store.getState());
  console.log("派发 action:", action);
  const result = next(action); // 继续执行 action
  console.log("更新后状态:", store.getState());
  return result;
};

// Reducer
const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    default:
      return state;
  }
};

// 创建 Redux store,使用 applyMiddleware 处理中间件
const store = createStore(reducer, applyMiddleware(loggerMiddleware));

// 触发 action
store.dispatch({ type: "INCREMENT" });

运行结果

当前状态: { count: 0 }
派发 action: { type: "INCREMENT" }
更新后状态: { count: 1 }

总结

  • applyMiddleware(loggerMiddleware) 增强了 dispatch,在每次 dispatch(action)打印日志
  • next(action) 负责传递 action,最终到达 reducer,否则 Redux 流程会被拦截。

3️⃣ Redux 异步中间件示例

Redux 默认不支持异步 action,我们需要用中间件来处理异步逻辑

✅ Redux-Thunk(处理 dispatch 函数)

redux-thunk 允许 dispatch 支持函数,而不仅仅是对象:

const thunkMiddleware = (store) => (next) => (action) => {
  if (typeof action === "function") {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

// 使用 thunk 中间件
const store = createStore(reducer, applyMiddleware(thunkMiddleware));

// 异步 action
const fetchData = () => {
  return (dispatch) => {
    setTimeout(() => {
      dispatch({ type: "INCREMENT" });
    }, 1000);
  };
};

// 触发异步 action
store.dispatch(fetchData());

thunkMiddleware 逻辑:

  1. 如果 action 是一个函数,则执行这个函数,并传入 dispatchgetState,支持异步操作
  2. 如果 action 是普通对象,直接传递给 next(action),进入 reducer 处理。

✅ Redux-Promise(处理 Promise action)

redux-promisedispatch 支持 Promise,比如:

const promiseMiddleware = (store) => (next) => (action) => {
  if (action instanceof Promise) {
    return action.then(next);
  }
  return next(action);
};

// 使用 promise 中间件
const store = createStore(reducer, applyMiddleware(promiseMiddleware));

// 触发 Promise action
store.dispatch(
  new Promise((resolve) => {
    setTimeout(() => resolve({ type: "INCREMENT" }), 1000);
  })
);

原理:

  • 如果 action 是一个 PromisePromise 解析后再执行 next(action),使 dispatch 能够直接处理异步请求

4️⃣ 总结

  1. Redux 中间件是 dispatch 的高阶增强,可以拦截、修改、异步处理 action

  2. 核心实现是

    • (store) => (next) => (action) => {} 这种 函数式组合
    • 通过 applyMiddleware 增强 dispatch,形成中间件链。
  3. 常见 Redux 中间件

    • redux-thunk → 让 dispatch 支持 function,用于异步操作。
    • redux-promise → 让 dispatch 支持 Promise,简化异步请求。
    • redux-logger → 记录 dispatch 过程中的 state 变化。
  4. 手写中间件的关键点

    • 拦截 dispatch(action)
    • 可以修改/处理 action
    • 调用 next(action) 传递给 reducer

总结一句话:Redux 中间件是一个 dispatch 的增强函数,它让 Redux 可以处理异步、日志、权限等功能,核心就是一个 store -> next -> action 结构的高阶函数! 🚀

56.React的render props是什么?

如果我是前端面试者,我会这样回答:


什么是 Render Props?

Render PropsReact 组件的一种模式,它指的是:一个组件接收一个函数作为 props,然后在组件内部调用这个函数并渲染其返回的内容

通常用于 组件复用,特别是共享组件逻辑(例如:状态管理、动画、数据获取等)。


Render Props 的基本用法

const MouseTracker = (props) => {
  const [position, setPosition] = React.useState({ x: 0, y: 0 });

  const handleMouseMove = (event) => {
    setPosition({ x: event.clientX, y: event.clientY });
  };

  return (
    <div style={{ height: "200px", border: "1px solid black" }} onMouseMove={handleMouseMove}>
      {props.render(position)}  {/* 调用 render props 函数 */}
    </div>
  );
};

const App = () => (
  <MouseTracker
    render={(position) => <h1>鼠标位置:{position.x}, {position.y}</h1>}
  />
);

工作原理:

  • MouseTracker 组件封装了鼠标位置的状态,并通过 render 传递给子组件
  • App 组件通过 render 传入函数,决定如何渲染 position 数据
  • 这避免了继承,允许灵活的 UI 复用。

Render Props vs 其他模式

模式适用场景缺点
Render Props组件复用、状态共享嵌套较深时可能影响可读性
HOC(高阶组件)逻辑复用,如权限控制可能导致 props 冲突,React DevTools 难以调试
Hooks现代 React 组件逻辑复用仅适用于函数组件

Render Props 的应用场景

  1. 共享组件状态(如 MouseTracker 示例)
  2. 数据获取(如封装 fetch 逻辑)
  3. 动画、过渡(如 react-motion

Render Props 现状

React 16.8+ 之后,Hooks(如 useState, useEffect, useContext)的出现减少了 Render Props 的使用,因为 Hooks 让组件逻辑复用变得更加简洁。
但在某些场景下,Render Props 仍然适用,特别是当你想要封装一个逻辑复用但 UI 仍然可自定义的组件时。


总结:Render Props 是 React 组件复用的一种模式,通过传递函数 props 让组件逻辑和 UI 解耦,适用于状态共享、数据获取等场景。 🚀

57.为什么之前react组件要写import react from ‘react’,现在又不用了?

在 React 17 之前,组件需要显式导入 React(如 import React from 'react'),是因为 JSX 语法会被 Babel 等工具转换为 React.createElement 调用,必须确保 React 在作用域内可用。而 React 17 引入了 新的 JSX 转换方式,通过自动从 react/jsx-runtime 注入 jsxjsxs 函数,不再依赖全局的 React 变量,因此无需手动导入。这一变化简化了代码,减少了冗余,同时为未来优化(如更高效的编译输出)奠定了基础。


详细解释

  1. React 17 之前

    • JSX 转换依赖 React.createElement
      当编写 <Button /> 时,Babel 会将其转换为 React.createElement(Button, null),因此必须导入 React 以访问 createElement 方法。

      // 代码
      import React from 'react';
      function App() { return <Button />; }
      
      // 转换后
      import React from 'react';
      function App() { return React.createElement(Button, null); }
      
  2. React 17+ 的新 JSX 转换

    • 自动引入运行时函数
      Babel 会将 JSX 转换为 _jsx_jsxs 函数,这些函数从 react/jsx-runtime 自动导入,不再依赖全局 React

      // 代码(无需导入 React)
      function App() { return <Button />; }
      
      // 转换后
      import { jsx as _jsx } from 'react/jsx-runtime';
      function App() { return _jsx(Button, {}); }
      
  3. 优势

    • 代码更简洁:省略不必要的 import React
    • 避免错误:消除因忘记导入 React 导致的 React is not defined 错误。
    • 性能优化:新的 JSX 运行时可能生成更高效的代码(如编译时优化)。
  4. 注意事项

    • 类组件仍需导入 React:若使用 class App extends React.Component,仍需导入 React

    • 直接使用 React API:如 useStateuseEffect 需从 'react' 导入,但无需引入整个 React 对象:

      import { useState } from 'react'; // ✅ 正确
      // import React from 'react';      // ❌ 不再需要
      

总结

  • 旧方式:JSX → React.createElement → 强制导入 React
  • 新方式:JSX → _jsx(自动注入)→ 无需导入 React
  • 升级条件:使用 React 17+ 和 Babel 7.9.0+(或相应工具链)。

59.说说 stack reconciler和fiber reconciler

在 React 中,Stack ReconcilerFiber Reconciler 是两种不同的调和(Reconciliation)算法,主要区别在于性能优化和更新方式。


1. Stack Reconciler(旧版调和器)

特点

  • React 15 及之前使用的是 Stack Reconciler
  • 采用 递归调用 组件树的方式进行协调(Reconciliation)。
  • 由于 JavaScript 引擎的调用栈大小有限,组件树过深时可能会导致 递归调用栈溢出
  • 同步更新:一旦开始调和,就必须一次性完成整个更新任务,无法中断。

缺点

  • 递归调用导致大任务无法拆分,阻塞主线程,影响用户体验(如页面卡顿)。
  • 无法进行任务优先级调度,所有更新一视同仁。

2. Fiber Reconciler(新版调和器)

特点

  • React 16 及之后引入 Fiber Reconciler,完全重写了协调算法。
  • 采用 Fiber 数据结构,将组件树转换为一个可操作的链表结构,使调和过程变成 可中断、可恢复 的。
  • 时间切片(Time Slicing) :更新可以被拆分成多个小任务,并在浏览器的空闲时间继续执行,从而提高页面响应速度。
  • 任务优先级调度:React 可以根据更新的重要性(如用户输入 vs. 动画 vs. 数据加载)分配不同的优先级。

Fiber 工作原理

  1. Render 阶段(可中断)

    • 深度优先遍历 方式遍历 Fiber 树,创建新的 Fiber 节点,并标记需要更新的部分。
    • 这一步可以被 分片执行,允许浏览器在空闲时继续计算,避免卡顿。
  2. Commit 阶段(不可中断)

    • 经过调度后,将最终的变更提交到 DOM 上,分为:

      • beforeMutation(调用 getSnapshotBeforeUpdate
      • Mutation(更新 DOM)
      • Layout(触发 componentDidMountcomponentDidUpdate

总结

对比项Stack ReconcilerFiber Reconciler
数据结构组件树(递归)Fiber 链表(双缓冲)
调和方式递归同步可中断 & 分片调度
性能任务无法拆分,容易阻塞任务可拆分,提升页面流畅度
任务优先级一视同仁可分配不同优先级
React 版本React 15 及之前React 16 及之后

Fiber Reconciler 解决了 Stack Reconciler 中的同步阻塞问题,使 React 可以更高效地渲染 UI,特别是在复杂应用和动画交互中带来了更好的用户体验。

你可以补充一些实际的 Fiber 调度 API(如 requestIdleCallbackscheduler 任务调度)来展示你的深入理解!

60.react中有哪几种数据结构,分别是干什么的?

在 React 源码中,主要涉及以下几种重要的数据结构,每种都有特定的用途:


1. Fiber 树(FiberNode)—— 组件调和

  • 作用:用于描述 React 组件树,并支持可中断的渲染更新。

  • 数据结构:双向链表(单个 Fiber 节点有 childsiblingreturn 指针)。

  • 关键字段

    • tag:表示当前 Fiber 节点的类型(如函数组件、类组件、DOM 元素等)。
    • stateNode:存放与 Fiber 关联的 DOM 节点或组件实例。
    • childsiblingreturn:指向子节点、兄弟节点、父节点,形成 Fiber 树。
    • alternate:指向前一次更新的 Fiber,形成 双缓存机制,用于 Diff 计算。

2. Update Queue(更新队列)—— 组件状态管理

  • 作用:存储组件的 state 更新任务。

  • 数据结构:链表,存放多个 update 对象(如 setState 触发的更新)。

  • 关键字段

    • shared.pending:指向等待处理的更新。
    • baseState:上一次计算的 state 值。
    • memoizedState:本次计算的 state 值。
    • effects:存放副作用(useEffect)的更新列表。

3. Effect List(副作用链表)—— 处理副作用

  • 作用:存储需要执行的副作用(useEffectcomponentDidMountcomponentDidUpdate 等)。

  • 数据结构:单向链表(所有需要执行副作用的 Fiber 形成链表)。

  • 关键字段

    • flags:标记当前 Fiber 节点的副作用类型,如 Placement(插入)、Update(更新)、Deletion(删除)。
    • nextEffect:指向下一个需要执行副作用的 Fiber 节点。

4. Lanes & Scheduler(优先级调度)—— 任务调度

  • 作用:控制 React 的并发更新,确保高优先级任务先执行。

  • 数据结构:位运算(bitmask),类似二进制位的调度系统。

  • 关键字段

    • lanes:存储当前任务的优先级,React 16+ 通过 bitmask 进行任务分配。
    • currentPriorityLevel:当前正在执行的任务优先级。
    • pendingLanes:所有等待执行的任务。

5. Hook 链表(useState、useEffect)—— 函数组件状态管理

  • 作用:管理 useStateuseReduceruseEffect 等 Hook 状态。

  • 数据结构:单向链表,每个 Hook 形成链表节点。

  • 关键字段

    • memoizedState:存储 Hook 的当前值。
    • next:指向下一个 Hook 节点。
    • queue:存储 setState 触发的更新队列。

总结

数据结构作用关键字段
Fiber 树组件调和(可中断更新)tagstateNodechildsibling
Update Queue管理 state 更新shared.pendingmemoizedState
Effect List处理副作用(useEffectflagsnextEffect
Lanes & Scheduler任务优先级调度lanespendingLanes
Hook 链表函数组件状态管理memoizedStatenext

61.说一下react的更新流程

React 的更新流程

React 的更新流程可以分为 触发更新、调和(Reconciliation)、提交(Commit) 三个阶段。React 采用 Fiber Reconciler 进行调和,使更新可以拆分并中断,提高渲染效率。


1. 触发更新(Trigger Update)

触发方式

更新可以由以下方式触发:

  • setState(类组件)
  • useState/useReducer(函数组件)
  • forceUpdate
  • Context 变化
  • props 变更
  • Suspense 触发回退 UI
  • 事件、定时器等外部触发

存入更新队列

  • 每个 Fiber 节点都有一个 更新队列(Update Queue) ,当 setState 等触发更新时,新的 update 被加入队列。
  • React 通过 Lanes 机制计算优先级,决定何时执行更新。

2. 调和(Reconciliation,Render 阶段)

  • 工作原理:使用 Fiber 架构 遍历组件树,计算需要更新的部分。
  • 特点:这个阶段是 可中断的(React 18 中的并发模式利用 requestIdleCallbackScheduler 进行时间切片)。

过程

  1. 生成新的 Fiber 树

    • 通过 workInProgress 指向当前正在构建的新 Fiber 树。
    • 通过 Diff 算法 对比新旧 Fiber 树,标记需要更新的节点。
    • 生成 Effect List 记录需要执行的副作用(如 useEffect)。
  2. 任务调度

    • React 使用 Scheduler(调度器) 分配更新优先级。
    • 高优先级(如用户输入)可以中断低优先级(如网络请求)。
  3. 计算更新

    • 组件的 render 方法或 函数组件 被执行,返回新的 VNode 结构。
    • 更新 memoizedState,存储新的 state 计算结果。

3. 提交(Commit 阶段,不可中断)

作用:将更新应用到 DOM,并执行副作用。

阶段

  1. Before Mutation(更新前)

    • 触发 getSnapshotBeforeUpdate
    • 记录旧 DOM 信息
  2. Mutation(更新 DOM)

    • 遍历 Effect List,执行 Placement(插入)、Update(更新)、Deletion(删除)
    • React 直接操作 DOM 进行渲染
  3. Layout(更新后)

    • 触发 componentDidMountcomponentDidUpdate
    • 执行 useEffectuseLayoutEffect
    • 组件状态更新完成

4. 重点总结

阶段作用关键点
触发更新组件 setStateprops 变化Update QueueLanes 机制
调和(Render)计算更新部分,构建新 Fiber 树Diff 算法Effect List
提交(Commit)更新 DOM,执行副作用MutationuseEffect生命周期

优化点

  1. 减少不必要的渲染

    • React.memo
    • useMemo / useCallback
    • shouldComponentUpdate
  2. 避免 Reconciliation

    • useRef 保存不变数据
    • PureComponent 避免无效更新
  3. 提高并发性能

    • startTransition(低优先级更新)
    • useDeferredValue(减少重新渲染)

理解 React 的更新流程优化策略,可以更高效地调试和优化应用,提高面试通过率!

62.什么是闭包陷阱?

闭包陷阱(Closure Trap)

闭包(Closure) 是 JavaScript 重要的特性之一,它允许函数“记住”其定义时的作用域。然而,使用不当时,会导致 性能问题、变量引用错误、意外的内存泄漏 等问题,这些问题被称为 闭包陷阱(Closure Trap)


常见闭包陷阱

1. 循环中的闭包(var 作用域问题)

📌 问题var 没有块级作用域,导致所有回调函数共享同一个变量,最终 i 变成了 3

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 3, 3, 3
  }, 1000);
}

解决方案

  • 使用 let 使 i 形成块级作用域
  • 使用 立即执行函数表达式(IIFE) 传递 i
// 方案1:使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 1000);
}

// 方案2:使用 IIFE
for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i); // 0, 1, 2
    }, 1000);
  })(i);
}

2. 闭包导致内存泄漏

📌 问题:闭包中的变量被长时间引用,导致无法被垃圾回收。

function createClosure() {
  let largeData = new Array(1000000); // 占用大量内存
  return function () {
    console.log(largeData.length);
  };
}

const closureFn = createClosure(); // `largeData` 无法被回收

解决方案

  • 显式置 null,释放引用
  • 仅在必要时使用闭包
function createClosure() {
  let largeData = new Array(1000000);
  return function () {
    console.log(largeData.length);
    largeData = null; // 释放内存
  };
}

3. 事件监听中的闭包

📌 问题:事件监听中使用闭包,导致 DOM 变量无法被回收。

function attachEvent() {
  const element = document.getElementById("btn");
  element.addEventListener("click", function () {
    console.log(element.id);
  });
}
attachEvent(); // `element` 仍然在闭包中,无法回收

解决方案

  • 解绑事件监听
  • 使用 this 代替闭包
function attachEvent() {
  const element = document.getElementById("btn");
  element.addEventListener("click", function () {
    console.log(this.id); // 这里 `this` 指向 `element`
  });
  element = null; // 释放引用
}

总结

闭包陷阱原因解决方案
循环中的闭包var 作用域导致变量共享使用 let 或 IIFE
内存泄漏变量被闭包长时间引用null 释放引用
事件监听泄漏DOM 节点被闭包引用解绑事件监听,使用 this

63.闭包陷阱的成因与解法?闭包陷阱的成因与解法?

闭包陷阱的成因与解法

闭包(Closure) 是 JavaScript 中的一个强大特性,它允许函数“记住”创建它时的作用域。然而,如果使用不当,闭包可能导致 变量引用错误、内存泄漏、性能问题,这些问题统称为 闭包陷阱(Closure Trap)


1. 闭包陷阱的成因

(1) 变量共享(作用域链问题)

📌 成因
for 循环中使用 var 时,var 变量在 函数作用域 内是共享的,因此闭包函数访问的是最终的 var 变量值,而不是循环时的 i 值。

🚨 问题示例

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出 3, 3, 3
  }, 1000);
}

所有 setTimeout 共享同一个 i,在 for 循环结束时 i = 3,所以 console.log(i) 输出 3, 3, 3

✅ 解法

  1. 使用 let(块级作用域)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 1000);
}

let 使 i 在每次循环时都有自己的作用域,不会共享。

  1. 使用 IIFE(立即执行函数表达式)
for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i); // 0, 1, 2
    }, 1000);
  })(i);
}

IIFE 创建了一个独立作用域,每次循环 i 值都会被“冻结”在该作用域中。


(2) 闭包导致的内存泄漏

📌 成因
闭包函数持有对 大对象或 DOM 元素 的引用,导致变量不会被垃圾回收(GC)。

🚨 问题示例

function createClosure() {
  let largeData = new Array(1000000); // 大量数据
  return function () {
    console.log(largeData.length);
  };
}
const closureFn = createClosure(); // `largeData` 仍然存活

largeDataclosureFn 持有,无法被垃圾回收,造成 内存泄漏

✅ 解法

  • 在不需要时手动释放引用
function createClosure() {
  let largeData = new Array(1000000);
  return function () {
    console.log(largeData.length);
    largeData = null; // 释放引用
  };
}

(3) 事件监听中的闭包陷阱

📌 成因
闭包函数引用了 DOM 元素,但未及时移除事件监听,导致 DOM 对象无法被回收。

🚨 问题示例

function attachEvent() {
  const element = document.getElementById("btn");
  element.addEventListener("click", function () {
    console.log(element.id); // `element` 仍然被引用
  });
}
attachEvent();

element 被闭包引用,即使 btn 被移除,内存仍然无法回收。

✅ 解法

  1. 解绑事件监听
function attachEvent() {
  const element = document.getElementById("btn");
  const handler = function () {
    console.log(element.id);
  };
  element.addEventListener("click", handler);
  element.removeEventListener("click", handler); // 解绑监听
}
  1. 使用 this 代替闭包
function attachEvent() {
  const element = document.getElementById("btn");
  element.addEventListener("click", function () {
    console.log(this.id); // `this` 指向 `element`
  });
}

这样不会在闭包内存储 element,减少引用链。


(4) 计时器 & 异步回调

📌 成因
setTimeoutsetInterval 可能导致闭包持有变量的引用,导致 变量长时间存活

🚨 问题示例

function startTimer() {
  let data = "重要数据";
  setTimeout(() => {
    console.log(data); // `data` 长时间存活
  }, 5000);
}
startTimer();

✅ 解法

  • 手动释放数据
function startTimer() {
  let data = "重要数据";
  let timer = setTimeout(() => {
    console.log(data);
    data = null; // 释放数据
  }, 5000);
}
startTimer();
  • 清除计时器
let timer = setTimeout(() => console.log("执行任务"), 5000);
clearTimeout(timer); // 及时清理

2. 闭包陷阱的总结

闭包陷阱成因解决方案
变量共享var 作用域导致变量被共享使用 let 或 IIFE
内存泄漏变量被闭包长时间引用null 释放引用
事件监听泄漏DOM 元素被闭包引用解绑事件监听,使用 this
计时器/回调定时器 & 回调函数持有变量手动释放数据,清除计时器

64.在react中,如何实现渲染控制?

React 中的渲染控制(Render Control)

在 React 中,渲染控制指的是 有选择性地渲染组件,以提高性能或优化用户体验。可以使用 条件渲染、列表渲染、优化渲染性能 等多种方式来实现。


1. 条件渲染(Conditional Rendering)

1.1 三元运算符

function Greeting({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? <h1>欢迎回来!</h1> : <h1>请登录</h1>}
    </div>
  );
}

1.2 && 短路运算

function ShowMessage({ hasMessage }) {
  return <div>{hasMessage && <p>您有新消息</p>}</div>;
}

⚠️ 注意:当 hasMessagefalse0 时,不会渲染 <p>

1.3 if-else 语句

function Greeting({ isLoggedIn }) {
  if (isLoggedIn) {
    return <h1>欢迎回来!</h1>;
  } else {
    return <h1>请登录</h1>;
  }
}

2. 列表渲染

使用 map() 动态生成 JSX 元素

function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

⚠️ 注意:必须给每个列表项加上唯一的 key,否则 React 可能会导致渲染错误或性能下降。


3. 避免不必要的渲染

3.1 React.memo()(浅比较 props)

适用于纯函数组件,当 props 没有变化时跳过重新渲染。

const MemoizedComponent = React.memo(function MyComponent({ name }) {
  console.log("渲染了!"); // 仅当 `name` 变化时触发
  return <p>你好,{name}!</p>;
});

3.2 shouldComponentUpdate()(类组件)

适用于类组件,可手动控制组件是否重新渲染。

class MyComponent extends React.PureComponent {
  shouldComponentUpdate(nextProps) {
    return nextProps.value !== this.props.value; // 仅当 props 变化时更新
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}

PureComponent 继承 shouldComponentUpdate(),默认对 props 进行浅比较

3.3 useMemo() & useCallback()(函数组件)

  • useMemo():缓存计算结果,避免重复计算
  • useCallback():缓存函数引用,避免组件重新渲染时创建新函数
const memoizedValue = useMemo(() => computeExpensiveValue(data), [data]);
const memoizedCallback = useCallback(() => doSomething(), []);

4. 组件卸载(移除不必要的组件)

4.1 通过 state 控制组件的挂载

function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(!show)}>切换组件</button>
      {show && <MyComponent />}
    </div>
  );
}

⚠️ 注意:当组件被卸载时,会触发 useEffect 的清理函数(return () => {...})。


5. 使用 Suspenselazy 实现懒加载

React 允许对 组件进行懒加载,提升首屏渲染性能。

const LazyComponent = React.lazy(() => import("./LazyComponent"));

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Suspense 提供 fallback 组件,在组件加载时展示占位内容


总结

渲染控制方法用途示例
条件渲染仅在满足条件时渲染if-else&&、三元运算符
列表渲染动态渲染数组数据map()
避免重复渲染仅当数据变更时重新渲染React.memo()useMemo()shouldComponentUpdate()
控制组件挂载组件按需显示/隐藏useState 控制组件是否渲染
懒加载按需加载组件,提高性能React.lazy() + Suspense

65.如何实现一个redux?

实现一个 Redux

Redux 是一个 状态管理库,核心思想是 单一数据源(Store)、纯函数(Reducer)和不可变状态。下面我们手写一个简化版 Redux,并解析其实现原理。


1. Redux 的核心概念

  • store(仓库) :存储应用的全局状态
  • state(状态) :存储的数据
  • action(动作) :描述对 state 进行的操作(必须是一个普通对象)
  • reducer(纯函数) :根据 action 计算新的 state
  • dispatch(派发) :触发 action,通知 reducer 更新 state
  • subscribe(订阅) :监听 state 变化,触发回调

2. 手写 Redux

📌 核心代码

function createStore(reducer) {
  let state; // 存储状态
  let listeners = []; // 存储订阅者

  // 获取当前状态
  function getState() {
    return state;
  }

  // 触发 action,更新 state
  function dispatch(action) {
    state = reducer(state, action); // 计算新 state
    listeners.forEach(listener => listener()); // 通知所有订阅者
  }

  // 订阅 state 变化
  function subscribe(listener) {
    listeners.push(listener); // 添加订阅者
    return () => {
      listeners = listeners.filter(l => l !== listener); // 取消订阅
    };
  }

  // 初始化 store,触发一次 reducer,获取初始值
  dispatch({ type: "@@INIT" });

  return { getState, dispatch, subscribe };
}

3. 使用 createStore

(1) 定义 reducer

function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
}

(2) 创建 store

const store = createStore(counterReducer);

(3) 订阅状态变化

store.subscribe(() => {
  console.log("State changed:", store.getState());
});

(4) 触发 action 更新 state

store.dispatch({ type: "INCREMENT" }); // State changed: { count: 1 }
store.dispatch({ type: "INCREMENT" }); // State changed: { count: 2 }
store.dispatch({ type: "DECREMENT" }); // State changed: { count: 1 }

4. 实现 Redux 中间件机制(类似 applyMiddleware

Middleware(中间件)用于增强 dispatch,例如:日志、异步操作等。

function applyMiddleware(store, middlewares) {
  let dispatch = store.dispatch;
  
  middlewares.forEach(middleware => {
    dispatch = middleware(store)(dispatch);
  });

  return { ...store, dispatch };
}

// 示例:日志中间件
const loggerMiddleware = store => next => action => {
  console.log("Action:", action);
  let result = next(action);
  console.log("New State:", store.getState());
  return result;
};

// 创建 store 并应用中间件
const storeWithMiddleware = applyMiddleware(store, [loggerMiddleware]);

storeWithMiddleware.dispatch({ type: "INCREMENT" });

5. Redux 的异步处理(Thunk 实现)

Redux 本身是同步的,要处理异步操作(如 API 请求),可以使用 redux-thunk

实现 thunkMiddleware

const thunkMiddleware = store => next => action => {
  if (typeof action === "function") {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

// 使用 thunkMiddleware
const storeWithThunk = applyMiddleware(store, [thunkMiddleware]);

// 异步 action
const asyncIncrement = () => (dispatch) => {
  setTimeout(() => {
    dispatch({ type: "INCREMENT" });
  }, 1000);
};

// 触发异步 action
storeWithThunk.dispatch(asyncIncrement());

6. 总结

功能核心 API作用
创建 StorecreateStore(reducer)创建全局状态存储
获取状态getState()获取当前 state
更新状态dispatch(action)触发 reducer,计算新 state
订阅变化subscribe(listener)监听状态变化
支持中间件applyMiddleware()增强 dispatch,添加日志、异步支持等

🔹 通过 createStore 创建 Redux,结合 middleware 处理异步,最终实现一个完整的 Redux! 🚀

66.useRoutes的原理是什么?

白话回答:useRoutes是一个hook,原理是将声明式路由配置(json的路由表)转化为动态的路由匹配与组件渲染,通过 React Router 的上下文和内部匹配算法,实现 URL 到组件的映射。其中使用到matchRoutes和
useLocation将从浏览器url中获取到的路由和json中进行match匹配,并将匹配到的路由对应的组件进行映射渲染
匹配结果通过 renderMatches(matches) 进行递归渲染:

  • 从最外层父路由开始,逐层渲染匹配的 element
  • 递归传递子路由 通过 Outlet 组件完成嵌套渲染。

useRoutes 是 React Router v6 中引入的一个核心钩子,用于在函数式组件中声明式地定义路由配置。它的原理可以拆解为以下几个关键点:


1. 基于配置的路由匹配

useRoutes 的核心是将路由配置转化为 React Router 内部的路由匹配逻辑。它接受一个路由配置对象数组(类似 Route 组件的结构化数据),例如:

const routes = [
  { path: '/', element: <Home /> },
  { path: 'users', element: <Users />, children: [
    { path: ':id', element: <UserProfile /> }
  ]}
];

useRoutes(routes) 会解析当前 URL,按配置的路径(path)和嵌套关系(children)进行匹配,最终确定需要渲染的组件。


2. 依赖 React Router 的上下文

useRoutes 必须工作在 React Router 的上下文(<BrowserRouter><HashRouter>)中,因为它依赖以下机制:

  • 路由状态:通过 useLocation() 获取当前 URL 的 location 对象。
  • 匹配算法:使用 matchPath() 等内部方法,将 location.pathname 与配置的 path 进行模式匹配(支持动态参数、通配符等)。
  • 嵌套路由:通过递归处理 children 配置,构建嵌套的路由层级结构。

3. 生成 React 元素

useRoutes 的返回值是一个 React 元素(或 null),代表当前 URL 匹配到的组件树。例如:

  • 当 URL 为 /users/123 时,useRoutes 会依次匹配 users:id,最终返回 <Users><UserProfile /></Users> 的嵌套结构。
  • 若未匹配到路由,返回 null(通常需要配置 path: '*' 作为兜底路由)。

4. 动态参数与状态传递

  • 参数解析:动态路径(如 :id)的参数会被提取,并通过 useParams() 传递给组件。
  • 状态传递useRoutes 自动处理路由状态(如 location.state),确保子组件能通过 useLocation() 获取。

5. 性能优化

useRoutes 内部通过 Memoization 优化路由匹配过程,避免不必要的重新渲染。只有当 location 或路由配置变化时,才会重新计算匹配结果。


源码简析(简化版)

useRoutes 的核心逻辑类似于以下伪代码:

function useRoutes(routes) {
  const location = useLocation();
  // 递归匹配路由,生成匹配结果
  const matches = matchRoutes(routes, location);
  // 将匹配结果转换为 React 元素(嵌套结构)
  return renderMatches(matches);
}

其中:

  • matchRoutes():遍历路由配置,找到与当前 location 匹配的路由。
  • renderMatches():将匹配的路由按嵌套关系渲染为 React 元素。

总结

可回答 useRoutes的原理应该是将路由配置对象转换为React元素,利用React Router的上下文和路由匹配机制,动态地根据当前URL决定渲染哪个组件。内部可能使用React的context API来传递路由状态,并通过匹配算法找到对应的路由配置,然后生成相应的组件结构。

useRoutes 的原理是将声明式路由配置转化为动态的路由匹配与组件渲染,通过 React Router 的上下文和内部匹配算法,实现 URL 到组件的映射。它简化了路由定义,同时保持了与 React 组件模型的深度集成。

67.react18中,为什么选择messagechannel来让出执行权?

React在更新时需要进行任务调度,确保高优先级的任务优先执行,同时不阻塞主线程,保持应用的响应性。 在React 18之前,可能使用的是requestIdleCallback或者setTimeout来进行任务调度。但requestIdleCallback的兼容性和触发时机可能不够可靠,而setTimeout有最小延迟时间4ms,可能不够及时。所以React需要一种更高效的方式来调度任务。 MessageChannel属于宏任务,它的回调会在当前事件循环的末尾执行,这样可以让出主线程,让浏览器有机会处理用户输入等紧急任务。相比setImmediate,MessageChannel的兼容性更好,而相比requestAnimationFrame,它更适合处理非渲染相关的任务。 另外,React可能希望更精细地控制任务的调度,使用MessageChannel可以更主动地安排任务的执行时机,避免被其他宏任务阻塞。同时,结合Scheduler包,React能够实现时间切片和任务的中断恢复,提升并发模式下的性能。
在 React 18 中,选择 MessageChannel 作为调度器(Scheduler)中让出主线程(Yield to Main Thread)的核心机制,是为了实现更精细的时间切片(Time Slicing)并发渲染(Concurrent Rendering) ,其背后的设计原理可以拆解如下:


1. 为什么需要让出执行权?

React 的并发模式需要将长任务拆分为可中断的微任务块,避免阻塞主线程导致页面卡顿(如点击事件、动画无法及时响应)。传统同步渲染模式下,组件树的渲染是连续不可中断的,而并发模式下需要主动让出主线程,让浏览器有机会处理更高优先级的任务。


2. MessageChannel 的核心优势

a. 宏任务(Macro Task)的特性

MessageChannel 的回调属于宏任务,其执行时机在当前事件循环的末尾,与微任务(如 Promise)相比:

  • 更可控的让出时机:宏任务会在浏览器完成当前任务(如渲染、事件处理)后执行,确保主线程有喘息机会。
  • 避免微任务的饥饿问题:微任务队列会一次性全部执行,若递归触发微任务可能导致主线程长期被占用。
b. 对比其他宏任务方案
  • setTimeout(fn, 0) :存在最小延迟(~4ms) ,且可能被浏览器节流(如后台标签页)。
  • requestAnimationFrame:与渲染帧强绑定,不适合非渲染相关的任务调度。
  • setImmediate:仅 IE/Node.js 支持,兼容性差。
  • MessageChannel:无延迟、无兼容性问题(主流浏览器均支持)、触发时机精准。

3. React 调度器的实现逻辑

React 的调度器(Scheduler)使用 MessageChannel 实现任务队列的调度,核心流程如下:

  1. 任务入队:将 React 的渲染任务(如组件更新)加入调度队列。
  2. 触发宏任务:通过 MessageChannel.port.postMessage 触发一个宏任务。
  3. 执行任务块:在宏任务回调中,执行一段任务(如 5ms 的时间切片)。
  4. 让出主线程:若任务超时,将剩余任务重新入队,等待下一个宏任务继续执行。
// 简化的调度逻辑
const channel = new MessageChannel();
channel.port2.onmessage = () => {
  // 执行任务块,若超时则让出主线程
  if (needsYield()) {
    channel.port1.postMessage(null); // 触发下一个宏任务
    return;
  }
  processNextTask();
};

4. 与浏览器事件循环的协同

通过 MessageChannel,React 的调度器能够:

  • 精准控制任务中断点:在每段任务执行后检查是否需要让出主线程。
  • 避免阻塞用户交互:即使渲染未完成,点击、滚动等事件也能在下一个事件循环中及时响应。
  • 兼容时间切片:实现类似“可中断渲染”的效果,为并发模式(Concurrent Mode)提供基础。

5. 性能与兼容性权衡

  • 性能:MessageChannel 的触发速度比 setTimeout 更快,且不受浏览器节流影响。
  • 兼容性:MessageChannel 在 IE11 及现代浏览器中均可使用,而 setImmediaterequestIdleCallback 存在兼容性问题。

6. 源码中的关键代码

在 React 源码的 Scheduler 模块中,以下代码体现了 MessageChannel 的使用:

// React Scheduler 中的调度逻辑
const performWorkUntilDeadline = () => {
  // 执行任务...
  if (hasMoreWork) {
    // 通过 MessageChannel 调度下一个任务块
    schedulePerformWorkUntilDeadline();
  }
};

const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;

// 触发任务调度
schedulePerformWorkUntilDeadline = () => {
  channel.port2.postMessage(null);
};

总结

React 18 选择 MessageChannel 作为让出执行权的机制,是因为它结合了宏任务的非阻塞特性精准的触发时机良好的兼容性,使得 React 能够在并发模式下实现高效的时间切片,平衡渲染性能与用户体验。这是 React 调度器设计中的关键决策,为复杂应用的流畅运行奠定了基础。
微任务和宏任务的区别,为什么选择宏任务而不是微任务。微任务会在当前任务结束后立即执行,可能导致长时间占用主线程,而宏任务可以让出主线程,让浏览器有机会处理其他事件。
React 18选择MessageChannel可能是因为它作为宏任务能够有效让出主线程,提供更可靠的调度时机,同时兼容性较好,适合实现时间切片和并发模式的需求。

5. MessageChannel vs. setTimeout vs. requestIdleCallback

API执行时间适用场景React 适配
setTimeout(fn, 0)4ms 延迟(最小值)低优先级任务❌ 太慢
requestAnimationFrame下一帧执行动画、布局计算❌ 不能保证及时执行
requestIdleCallback浏览器空闲时执行后台任务❌ 不适用于高优先级任务
MessageChannel微任务队列,尽快执行任务调度、React 并发渲染✅ 最优选择

69.RN和react在跨端架构上有什么区别?

React 和 React Native(RN)在跨端架构上的主要区别可以从以下几个方面来分析:


1. 渲染机制

React(Web 端)

  • React 主要运行在浏览器,通过 Virtual DOM (VDOM) 进行高效的 UI 更新。
  • 使用 ReactDOM.render() 渲染组件,最终通过 HTML + CSS 在浏览器中显示。
  • 依赖 CSS、HTML、JavaScript,主要针对 Web 平台。

示例

function App() {
  return <div>Hello, Web!</div>;
}

最终会被转换成:

<div>Hello, Web!</div>

React Native(移动端)

  • 不使用 DOM,而是使用 Native 组件(如 iOS 的 UIView、Android 的 View)。
  • 使用 React Native Bridge 将 JavaScript 调用转换为原生渲染指令。
  • 组件如 <View><Text> 会在 iOS 和 Android 上映射到原生组件。

示例

function App() {
  return <View><Text>Hello, Mobile!</Text></View>;
}

在 iOS 上会被转换为:

UIView -> UILabel("Hello, Mobile!")

在 Android 上会被转换为:

View -> TextView("Hello, Mobile!")

核心区别

  • React 依赖 DOM,使用 HTML/CSS
  • RN 直接调用原生组件,不使用 HTML,而是通过 桥接(Bridge) 与原生交互。

2. 样式与布局

React (Web)

  • 使用 CSS 进行样式布局。
  • 采用 Flexbox(但实现和 RN 有些不同)。
  • 依赖 px、rem、em、vh、vw 等单位。
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

React Native

  • 没有 CSS,只能用 StyleSheet 定义样式。
  • 所有样式基于 Flexbox(不支持 gridfloat)。
  • 单位默认为 dp(独立像素) ,不使用 px
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }
});

核心区别

  • React 用 CSS,支持多种布局方式
  • RN 只支持 Flexbox,并使用 StyleSheet 定义样式

3. 事件处理

React (Web)

  • 依赖 DOM 事件,比如 onClickonMouseMove
  • 事件是合成事件(Synthetic Event) ,在 document 级别进行事件代理。
<button onClick={() => alert('Clicked!')}>Click Me</button>

React Native

  • 没有 DOM,所以事件是直接绑定在原生组件上
  • 主要事件有 onPress(代替 onClick)、onTouchStartonScroll 等。
<TouchableOpacity onPress={() => alert('Clicked!')}>
  <Text>Click Me</Text>
</TouchableOpacity>

核心区别

  • React 使用 DOM 事件,React Native 直接绑定到原生事件
  • RN 没有 onClick,而是 onPress

4. 跨端运行方式

React(Web 端)

  • 只在浏览器中运行,不需要 Bridge
  • 使用 ReactDOM 渲染,最终依赖 HTML + CSS + JavaScript

React Native(移动端)

  • 需要一个 JavaScript 线程 + 原生线程

  • 通过 React Native Bridge 进行通信:

    1. JS 线程 运行 React 代码。
    2. Bridge 传输数据(JS <-> Native)。
    3. Native 线程 负责渲染 UI。

核心区别

  • React 直接在浏览器运行,React Native 需要 Bridge 连接 JS 和 Native 代码

5. 跨端兼容

React(Web 跨端)

  • 主要依赖 React Native Web,将 RN 组件转换为 HTML + CSS 组件。
  • Web 端用 react-router-dom 进行路由管理。
import { BrowserRouter as Router, Route } from 'react-router-dom';

React Native(移动端跨端)

  • 主要依赖 React Navigation,提供 StackNavigatorTabNavigator
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();

核心区别

  • React 依赖 react-router-dom 进行 Web 路由
  • RN 依赖 react-navigation 进行移动端导航

6. 代码复用性

React

  • 主要针对 Web,跨端需要借助 React Native WebElectron

React Native

  • 通过 Platform 进行 按平台渲染
import { Platform } from 'react-native';

const styles = StyleSheet.create({
  container: {
    padding: Platform.OS === 'ios' ? 20 : 10
  }
});
  • 可以使用 react-native-web 让 RN 代码在 Web 端运行。

核心区别

  • React 不能直接用于移动端,React Native 代码可以通过 react-native-web 兼容 Web

总结

维度React(Web)React Native(移动端)
渲染方式DOM + Virtual DOM原生组件 + Bridge
样式CSSStyleSheet(无 CSS)
事件onClickonMouseMoveonPressonTouchStart
运行环境浏览器iOS/Android
跨端能力需要 React Native Web直接支持 iOS 和 Android
代码复用主要针对 Web可通过 Platform 区分平台

核心结论

  1. React 面向 Web,依赖 HTML + CSS + JS,而 RN 面向移动端,依赖原生组件
  2. React Native 使用 MessageQueue 进行 JS-Native 通信,而 React 直接运行在浏览器
  3. React 代码可通过 react-native-web 兼容 RN,但 RN 代码比 Web 更通用

🚀 面试官可能会追问

  1. RN 如何优化 Bridge 速度?

    • 通过 Fabric(新架构)+ TurboModule 提高原生通信效率。
  2. RN 的 Hermes 引擎是什么?

    • Hermes 是一个优化的 JS 引擎,提高 RN 启动速度。
  3. Web 和 RN 组件能共享吗?

    • 可以,使用 react-native-web 实现跨端共享。

总结一句话:React 适用于 Web,React Native 适用于移动端,RN 通过 Bridge 实现 JS 和原生交互,二者在跨端架构上有本质区别! 🚀

70.常见的redux中间件有哪些

Redux 常见的中间件主要用于 异步处理、日志记录、调试工具、错误处理 等,

  1. redux-thunk(异步 Action 处理)
  2. redux-saga(更强大的异步流控制)
  3. redux-logger(日志中间件)
  4. redux-devtools-extension(开发者工具)
  5. redux-promise(Promise 处理)
    作用:基于 ES6 Generator 实现的异步流管理,比 redux-thunk 更适合 复杂的异步逻辑(如监听多个 action、并发请求、取消任务等)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2309642.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机毕业设计SpringBoot+Vue.js装饰工程管理系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

从波士顿动力到Figure AI:探寻人工智能驱动的机器人智能化

一、引言 1.1 研究背景与意义 在科技飞速发展的当下,机器人智能化已成为全球科技竞争的关键领域,深刻影响着人类社会的生产与生活方式。从工业制造到日常生活服务,从医疗保健到探索未知领域,机器人正逐步渗透进各个行业,展现出巨大的发展潜力与应用价值。其智能化水平的…

华为 VRP 系统简介配置SSH,TELNET远程登录

华为 VRP 系统简介&配置TELNET远程登录 1.华为 VRP 系统概述 1.1 什么是 VRP VRP&#xff08;Versatile Routing Platform 华为数通设备操作系统&#xff09;是华为公司数据通信产品的通用操作系统平台&#xff0c;从低端到核心的全系列路由器、以太网交换机、业务网关等…

【计算机网络入门】初学计算机网络(九)

目录 1.令牌传递协议 2. 局域网&IEEE802 2.1 局域网基本概念和体系结构 3. 以太网&IEEE802.3 3.1 MAC层标准 3.1.1 以太网V2标准 ​编辑 3.2 单播广播 3.3 冲突域广播域 4. 虚拟局域网VLAN 1.令牌传递协议 先回顾一下令牌环网技术&#xff0c;多个主机形成…

Lumoz Chain正式上线:AI 时代的新算力破局者

新的叙事和技术突破永远是推动行业前行的核心动力。当下&#xff0c;AI Agent无疑是最炙手可热的赛道之一。 当加密世界将目光投向AI领域时&#xff0c;大多数项目仍停留在以AI为工具或应用场景的层面&#xff0c;试图通过集成AI模型或优化链上功能来吸引用户。然而&#xff0…

sql调优:优化响应时间(优化sql) ; 优化吞吐量

Sql性能调优的目的 1.优化响应时间>>优化sql 经过调优后&#xff0c;执行查询、更新等操作的时候&#xff0c;数据库的反应速度更快&#xff0c;花费的时间更少。 2.优化吞吐量 即“并发”, 就是“同时处理请求”的能力。 优化sql 尽量将多条SQL语句压缩到一句>…

2020最新Java面试题

2020最新Java面试题 序Java基础JVMRedis 序 研发了这么多年&#xff0c;每天都在搬砖&#xff0c;斗转星移&#xff0c;从一个被面试者&#xff0c;成为了一个面试者。总感觉自己在面试的时候提出的问题没有一个由浅入深的感觉&#xff0c;总是东一个问题&#xff0c;西一个…

批量设置 Word 样式,如字体信息、段落距离、行距、页边距等信息

在 Word 文档中&#xff0c;我们可以做各种样式的处理。比如设置 Word 文档的字体样式、设置 Word 文档的段落样式以及设置 Word 文档的页面样式。我们通常可以在 Office 中完成这些操作&#xff0c;相信绝大部分场景我们也是这样完成的。但是如果我们手上有 1000 个 Word 文档…

win本地vscode通过代理远程链接linux服务器

时间&#xff1a;2025.2.28 1. win本地下载nmap.exe nmap官网 https://nmap.org/或者 https://nmap.org/download#windows下载win版本并安装。 2. vscode插件Remote-SSH 插件下载Remote-SSH 3. 配置 按照图中顺序配置ssh 1.点击左侧工具栏的“小电视”图标 2.点击ssh的…

EVOAGENT: Towards Automatic Multi-Agent Generation via Evolutionary Algorithms

题目 EVOAGENT:通过进化算法实现多智能体自动生成 论文地址&#xff1a;https://openreview.net/pdf?id05bBTmRj9s 项目地址&#xff1a;https://evo-agent.github.io/ 摘要 强大的大型语言模型(LLM)的出现激发了一种新的趋势&#xff0c;即构建基于LLM的自治代理来解决复杂的…

Linux虚拟机网络配置-桥接网络配置

简介 本文档旨在指导用户如何在虚拟环境中配置Linux系统的桥接网络&#xff0c;以实现虚拟机与物理主机以及外部网络的直接通信。桥接网络允许虚拟机如同一台独立的物理机一样直接连接到物理网络&#xff0c;从而可以被分配一个独立的IP地址&#xff0c;并能够与网络中的其他设…

【UI设计——视频播放界面分享】

视频播放界面设计分享 在本次设计分享中&#xff0c;带来一个视频播放界面的设计作品。 此界面采用了简洁直观的布局。顶部是导航栏&#xff0c;包含主页、播放、搜索框等常见功能&#xff0c;方便用户快速找到所需操作。搜索框旁输入 “萌宠成长记”&#xff0c;体现了对特定内…

SyntaxError: positional argument follows keyword argument

命令行里面日常练手爬虫不注意遇到的问题&#xff0c;报错说参数位置不正确 修改代码后&#xff0c;运行如下图&#xff1a; 结果&#xff1a; 希望各位也能顺利解决问题&#xff0c;祝你好运&#xff01;

使用vue3+element plus 的table自制的穿梭框(支持多列数据)

目录 一、效果图 二、介绍 三、代码区 一、效果图 话不多说&#xff0c;先上图 二、介绍 项目需要&#xff1a;通过穿梭框选择人员信息&#xff0c;可以根据部门、岗位进行筛选&#xff0c;需要显示多列&#xff08;不光显示姓名&#xff0c;还包括人员的一些基础信息&…

Java【多线程】(2)线程属性与线程安全

目录 1.前言 2.正文 2.1线程的进阶实现 2.2线程的核心属性 2.3线程安全 2.3.1线程安全问题的原因 2.3.2加锁和互斥 2.3.3可重入&#xff08;如何自己实现可重入锁&#xff09; 2.4.4死锁&#xff08;三种情况&#xff09; 2.4.4.1第一种情况 2.4.4.2第二种情况 2.4…

vue These dependencies were not found

These dependencies were not found: * vxe-table in ./src/main.js * vxe-table/lib/style.css in ./src/main.js To install them, you can run: npm install --save vxe-table vxe-table/lib/style.css 解决&#xff1a; nodejs执行以下语句 npm install --save vxe-t…

Yak 在 AI 浪潮中应该如何存活?

MCP 是 Claude 发起的一个协议&#xff0c;在2024年10月左右发布&#xff0c;在2025年2月开始逐步有大批量的 AI 应用体开始支持这个协议。这个协议目的是让 AI 同时可以感知有什么工具可以用&#xff0c;如果要调用这些工具的话&#xff0c;应该是用什么样的方式。 这个 MCP 协…

AI是否能真正理解人类情感?从语音助手到情感机器人

引言&#xff1a;AI与情感的交集 在过去的几十年里&#xff0c;人工智能&#xff08;AI&#xff09;的发展速度令人惊叹&#xff0c;从简单的语音识别到如今的深度学习和情感计算&#xff0c;AI已经深入到我们生活的方方面面。尤其是在语音助手和情感机器人领域&#xff0c;AI不…

大语言模型学习--本地部署DeepSeek

本地部署一个DeepSeek大语言模型 研究学习一下。 本地快速部署大模型的一个工具 先根据操作系统版本下载Ollama客户端 1.Ollama安装 ollama是一个开源的大型语言模型&#xff08;LLM&#xff09;本地化部署与管理工具&#xff0c;旨在简化在本地计算机上运行和管理大语言模型…

linux上面安装 向量数据库 mlivus和 可视化面板Attu

1. 确保docker(docker 19.0以上即可) 和 docker-compose&#xff08;V2.2.2以上&#xff09; 都已安装 2. 创建milvus工作目录 # 新建一个名为milvus的目录用于存放数据 目录名称可以自定义 mkdir milvus# 进入到新建的目录 cd milvus 3. 下载并编辑docker-compose.yml 在下载…