2025年了,再被面试官问到这些问题就该自信吟唱了 —— React 篇

2,691 阅读11分钟

写这篇文章主要记录一些自己在面试过程中被问到的高频问题以及自己的一些理解,因为是面试题,难免会写得比较无聊,在努力改进,本文结合目录食用更佳。

(2年多React工作经验,经验丰富的掘友们可以划走啦 :p

如果有写的不准确的地方,欢迎评论区指出~


Q:React 相关问题

useEffect 和 useLayoutEffect 的区别

执行顺序不同

  • useEffect 在渲染完成后被调用,其副作用操作是在组件渲染完成后的“提交阶段”执行的。
  • useLayoutEffect 在dom 更新前调用,其副作用操作是在组件渲染完成后的“布局阶段”执行的。

性能影响不同

  • useEffect 为异步调用,不会阻塞UI的渲染。
  • useLayoutEffect 为同步调用,如果在副作用中执行的操作非常耗时,可能导致页面响应变慢。此外,由于它的副作用会在DOM 操作之后同步执行,如果涉及到DOM 的更改,可能会因为触发两次页面的绘制引发页面抖动。

在React 中ref 有什么作用?如果要实现父组件调用子组件方法,使用ref 和forwardRef 分别应该怎么实现?

在React中,ref 提供了访问DOM节点或React元素的方式。forwardRef 完成对 ref 的转发

类组件中的ref

首先我们需要了解,

React 默认支持类组件ref,直接传递给类组件,会绑定到类组件的实例上。

React 默认支持类组件ref,直接传递给类组件,会绑定到类组件的实例上。

React 默认支持类组件ref,直接传递给类组件,会绑定到类组件的实例上。

重要的事情说三遍!!!

我们来看一段代码示例,可以发现下图中子组件方法中并没有对ref做什么特殊处理

// 子组件 ChildComponent  
class ChildComponent extends React.Component {  
  myMethod() {  
    console.log('myMethod called from ChildComponent');  
  }  
  
  render() {  
    return <div>Child Component</div>;  
  }  
}  
  
// 父组件 ParentComponent  
class ParentComponent extends React.Component {  
  childRef = React.createRef();  
  
  callChildMethod = () => {  
    this.childRef.current.myMethod();  
  }  
  
  render() {  
    return (  
      <div>  
        <ChildComponent ref={this.childRef} />  
        <button onClick={this.callChildMethod}>Call Child Method</button>  
      </div>  
    );  
  }  
}

代码解析:

  1. React 的 ref 绑定类组件:

    • 当一个类组件绑定了 ref,React 会将该组件实例赋值给 ref.current
    • this.childRef.current 就是子组件 ChildComponent 的实例。
  2. 调用子组件方法:

    • this.childRef.current.myMethod() 调用了子组件 ChildComponent 上的方法 myMethod,输出 'myMethod called from ChildComponent'

函数式组件中的forwardRef

那如果是函数式组件呢?

函数式组件需要使用 React.forwardRef 来支持 ref

函数式组件需要使用 React.forwardRef 来支持 ref

函数式组件需要使用 React.forwardRef 来支持 ref

重要的事情再说三遍!!!

所以,如果子组件是函数式组件,则必须在其定义时包裹 forwardRef

我们结合代码来看一下

// 子组件 ChildComponent  
const ChildComponent = React.forwardRef((props, ref) => {  
  const myMethod = () => {  
    console.log('myMethod called from ChildComponent');  
  }  
  
  React.useImperativeHandle(ref, () => ({  
    myMethod  
  }));  
  
  return <div>Child Component</div>;  
});  
  
// 父组件 ParentComponent  
function ParentComponent() {  
  const childRef = React.createRef();  
  
  const callChildMethod = () => {  
    childRef.current.myMethod();  
  }  
  
  return (  
    <div>  
      <ChildComponent ref={childRef} />  
      <button onClick={callChildMethod}>Call Child Method</button>  
    </div>  
  );  
}

我们发现上面子组件用到了一个钩子函数 useImperativeHandle, 那它是做什么的呢?

useImperativeHandle

useImperativeHandle 是 React 提供的一个 Hook,用于在使用 React.forwardRef 的函数式组件中,自定义暴露给父组件的 实例方法或属性。

通常情况下,ref 默认指向子组件内部的 DOM 节点或实例,而 useImperativeHandle 可以让我们自定义 ref 暴露的内容,从而更灵活地控制子组件的行为。

useImperativeHandle(ref, createHandle, [deps])
//参数介绍
ref: 父组件传递下来的 ref。
createHandle: 一个返回对象的函数,用于定义暴露给父组件的实例方法或属性。
[deps]: 可选的依赖数组,当依赖变化时重新计算暴露的对象。

除上面例子中的简单应用外,我们还可以使用它实现自定义方法 比如A:B的形式

const ParentProfileContent = ({ personId }) => {
    const childRef = React.useRef(null)
    const handleContentUpdate = () => {
        childRef.current.refresh()
    }
    return (
        <Space direction="vertical" style={{ width: '100%' }} size={32}>
            <ChildInfo ref={childRef} personId={personId} />
            <ChildContent personId={personId} onUpdate={handleContentUpdate} /> //另一个子组件
        </Space>
    )
}

const ChildInfo = React.forwardRef(({ personId }, ref) => {
    React.useImperativeHandle(ref, () => ({
        refresh: handleRefresh
    }))

    const handleRefresh = () => {
        setLoading(true)
        fetchDetail(personId).finally(() => { //fetchDetail为提前写好的调用API的方法
            setLoading(false)
        })
    }
    
    return (
        //ChildInfo return 相关代码
    )
}

为什么函数式组件不能直接使用 ref ?

  1. 函数式组件没有实例

    • 类组件有实例(this 指向组件实例),绑定 ref 时 React 会将实例对象赋值给 ref.current
    • 函数式组件本质上是一个无状态的普通 JavaScript 函数,没有实例,因此无法直接绑定 ref
  2. React 的 ref 行为

    • 如果将 ref 直接传递给函数式组件,React 会将其作为普通的 props 传递,而不是绑定到组件本身。
    • React 默认只对 DOM 元素或类组件支持 ref,函数式组件除非用 React.forwardRef,否则不支持 ref

什么是虚拟DOM 和真实DOM? 它们是如何搭建的? 有节点删改的时候会不会进行重绘和重排?

真实DOM

真实DOM:是浏览器对页面元素的抽象表示

假设有以下HTML:

<div>
  <h1>Hello, Real DOM</h1>
  <p>This is a paragraph.</p>
</div>

对应的真实DOM 树结构

HTML
└── BODY
    └── DIV
        ├── H1
        └── P

真实DOM的搭建: 通过HTML和CSS的解析,浏览器构建DOM树和CSSOM,然后合并成渲染树(render tree),最后进行布局和绘制。

虚拟DOM

虚拟DOM:是React 对DOM 的抽象表示,它是一个轻量化的Js对象,描述了UI结构与真实DOM对应。

虚拟DOM的搭建: 通过JSX React.createElement在内存中创建JavaScript对象树,作为真实DOM的抽象表示。

二者区别

特性虚拟 DOM真实 DOM
存储位置内存中,JS 对象浏览器环境,HTML 结构
性能快速构建、对比和更新频繁操作会影响性能
修改方式比较差异后,批量更新到真实 DOM直接修改 DOM 节点
直接交互不可直接显示或操作可直接交互或展示

真实DOM 会进行重绘和重排,虚拟DOM 不会(React通过其高效的Diff算法和批量更新策略来最小化重绘和重排的次数。)

虚拟DOM 存在的意义是为了弥补真实DOM 操作的性能问题。

简单介绍下Diff算法

Diff 算法 是 React 虚拟 DOM 的核心部分,用于高效地找出新旧虚拟 DOM 树之间的差异,并将最小更新应用到真实 DOM。

React 的 Diff 算法基于以下两点优化:

  • 最小化对真实 DOM 的操作:频繁操作真实 DOM 会导致性能问题,因此 React 通过 Diff 算法尽量减少不必要的 DOM 操作。
  • 局部更新:只对有变动的部分进行更新,而非重新渲染整个 UI。
Diff 算法的原理

React Diff 算法有以下重要特性:

  1. 分层对比

    React 假设 DOM 的跨层级操作很少发生。如果发现节点层级改变(比如一个节点被移到了完全不同的层级),React 会直接删除原来的节点并重新创建,而不会尝试优化。

  2. 同层比较

    在同一层中,React 按顺序比较子节点。如果节点的顺序发生变化,React 会将节点移除并重新插入。

  3. Key 属性的优化

    • React 使用 key 属性来标识元素的唯一性。
    • 在子节点的顺序发生变化时,key 能帮助 React 快速定位变化的节点,避免不必要的重新创建。
  4. 三种变更类型

    • 节点替换:当新旧节点类型不同时,直接替换整个节点。
    • 属性更新:当节点类型相同时,只更新属性而不替换节点。
    • 文本更新:如果一个文本节点发生变化,直接修改文本内容。

React 是如何通过虚拟DOM 把改变的部分传递给真实DOM的?

  1. 构建虚拟 DOM

    当组件的 state(状态)或 props(属性)发生变化时,React 会重新调用组件的 render 方法,生成一个新的虚拟 DOM 树。这棵虚拟 DOM 树是组件在当前状态或属性下的理想结构的抽象表示。

  2. 比较虚拟 DOM

    React 使用高效的 Diff 算法(由 React Fiber 实现)比较新旧两棵虚拟 DOM 树之间的差异(diff)。这一过程旨在识别哪些部分发生了变化、哪些部分保持不变,从而避免不必要的更新。

  3. 计算最小变更集

    基于虚拟 DOM 的对比结果,React 计算出从旧虚拟 DOM 更新为新虚拟 DOM 所需的最小变更集。这些变更包括:

    • 新增节点:新增的虚拟 DOM 节点会被映射到真实 DOM 中并插入相应位置。
    • 删除节点:从虚拟 DOM 中删除的节点会被从真实 DOM 中移除。
    • 更新节点:已存在的节点如果属性或内容发生变化,React 会直接修改对应的真实 DOM 节点。
  4. 更新真实 DOM

    React 将计算出的最小变更集应用到真实 DOM。由于只更新变化部分,避免了整棵 DOM 树的重绘,大大提高了性能。这个过程通常使用 document.createElementappendChildremoveChild 等 DOM 操作来实现。

  5. 优化和复用

    为了进一步提升性能,React 会尝试复用和重排现有的 DOM 节点。

    例如:如果节点只是更改了属性(如 classNamestyle),React 会直接更新属性,而不会重新创建节点。 如果列表元素的顺序变化,React 会通过 key 属性来标识和移动节点,而不是删除后重新插入。

React 渲染机制是什么

React 的渲染机制以 虚拟 DOMReconciliation (协调算法)为核心,通过将应用状态与UI映射的过程高效化,实现快速、直观的UI构建和更新

为什么hooks能重用和共享组件状态?

React 提供的 Hooks(特别是自定义 Hooks)让开发者可以抽象出可复用的状态逻辑。通过自定义 Hooks,开发者能够将组件中的特定逻辑(如状态管理、数据获取、事件处理等)封装为独立的函数,并在多个组件中共享这些逻辑。

自定义 Hooks 的设计与特点

  1. 逻辑封装与复用

    自定义 Hooks 是以 use 开头的普通函数,可以接收参数,返回状态或逻辑。它们允许开发者将复杂的逻辑抽象出来,从而在多个组件中共享。例如,一个管理表单状态的自定义 Hook,可以在不同的表单组件中重用,而无需重复实现状态管理逻辑。

// 一个自定义 Hook 示例
const useForm = (initialValues) => {
    const [values, setValues] = useState(initialValues);
    const handleChange = (e) => setValues({ ...values, [e.target.name]: e.target.value });
    return [values, handleChange];
};

// 在多个组件中重用
const FormComponent1 = () => {
    const [formValues, handleChange] = useForm({ name: '', email: '' });
    return <input name="name" value={formValues.name} onChange={handleChange} />;
};
const FormComponent2 = () => {
    const [formValues, handleChange] = useForm({ username: '', password: '' });
    return <input name="username" value={formValues.username} onChange={handleChange} />;
};
  1. 逻辑解耦: Hooks 将逻辑从组件的渲染逻辑中分离出来,使得代码更模块化和可读。同时,状态和副作用的管理不再依赖类组件的生命周期方法,组件逻辑更加集中且易于维护。

  2. 与函数式组件的融合: 自定义 Hooks 可以充分利用函数式组件的特性,比如函数的参数传递、返回值复用等,使逻辑复用和组合更加自然。Hooks 的函数式特性还避免了类组件中的复杂继承和 this 绑定问题。

状态共享

通过 useContext Hook 配合 React 的 Context API,可以实现跨组件的状态共享。例如,定义一个全局状态管理的 Context,并在任意组件中通过自定义 Hook 来消费:

const GlobalContext = React.createContext();

const useGlobalState = () => {
    const context = useContext(GlobalContext);
    if (!context) throw new Error('useGlobalState must be used within a GlobalProvider');
    return context;
};

const GlobalProvider = ({ children }) => {
    const [state, setState] = useState({ user: null });
    const value = { state, setState };
    return <GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>;
};

// 在组件中共享状态
const ComponentA = () => {
    const { state, setState } = useGlobalState();
    return <button onClick={() => setState({ user: 'John' })}>Login</button>;
};
const ComponentB = () => {
    const { state } = useGlobalState();
    return <div>User: {state.user}</div>;
};

优势总结

  1. 模块化与可复用性: Hooks 让开发者可以将组件逻辑提取到独立的函数中,避免重复代码,提高开发效率。

  2. 可维护性与可读性: 自定义 Hooks 将组件的逻辑组织得更加集中,降低了耦合度,使代码易于理解和维护。

  3. 支持更灵活的共享方式:

    • useContext 实现全局状态共享;
    • Redux 或其他状态管理工具结合 Hooks(如 useSelectoruseDispatch)实现复杂场景的状态共享。
  4. 没有类组件的限制: Hooks 是函数式编程的体现,无需类组件的继承、生命周期方法和 this 的复杂绑定,更加灵活。

更多自定义Hooks 的例子可以移步博主的这篇博客。