常见React题目 (2024)

102 阅读17分钟

1.虚拟 DOM 的原理是什么?

React 用 JS 对象来模拟 DOM 节点,然后将其渲染成真实的 DOM 节点,这个JS对象就是虚拟DOM。
用 JSX 语法写出来的『标签』就是JS对象的一种形式,后面会转换为JS对象。
如果数据发生变化,并不会直接render整个组件。而是通过对比JS对象得到真正发生变化的部分,仅对变化的部分调用DOM API进行修改。

优点:
结合diff算法,减少不必要的dom修改,保证性能。
为 React 带来了跨平台能力,因为虚拟节点除了渲染为真实节点,还可以渲染为其他东西。

React中的类组件和函数组件之间有什么区别?

函数组件无状态,没有state,没有生命周期。可以传入参数,类似于类组件的props。
在react16.8版本中添加了hooks,可以在函数组件中使用useState钩子去管理state,使用useEffect钩子去使用生命周期函数。

什么是高阶组件(HOC)?

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
高阶组件是 React 中用于复用组件逻辑的一种技巧。
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
例如:React Router 的 withRouter,参数和返回值都是组件,用于给组件传入一些路由的API。

constructor中super与props参数一起使用的目的是什么?

ES6类:
如果在子类中不使用super关键字,会报错(报错内容:必须在访问this之前,或从constructor返回之前调用super())。
报错的原因是子类是没有自己的 this 对象的,它只能继承父类的 this 对象,然后对其进行加工。而 super() 就是将父类中的 this 对象继承给子类的,没有 super() 子类就得不到 this 对象。
类组件:
在调用 super() 的时候,我们一般都需要传入 props 作为参数,如果不传进去,React 内部也会将其定义在组件实例中。
但是也不建议使用 super() 代替 super(props),因为在 React 会在类组件构造函数生成实例后再给 this.props 赋值,所以在不传递 props 给 super 的情况下,constructor 中调用 this.props 为 undefined
总结:
在 React 中,类组件基于 ES6,所以在 constructor 中必须使用 super
在调用 super 过程,无论是否传入 propsReact 内部都会将 porps 赋值给组件实例 porps 属性中。
如果只调用了 super(),那么 this.props 在 constructor 中是 undefined
vue3js.cn/interview/R…

什么是受控组件?

受控组件是指由React组件负责管理其表单元素的状态的组件。在受控组件中,表单元素的值或状态由React组件的state来控制,而不是由DOM自身来管理。当用户与表单元素交互时,React组件会更新其state,从而更新表单元素的值或状态,并且通过事件处理程序来处理用户输入。

6.说说React的事件机制?

  • React 的合成事件是 React 提供的一种事件处理系统,用于封装原生 DOM 事件,提供了跨浏览器兼容性的解决方案。
  • 合成事件的优点在于它抽象了底层浏览器实现的差异,使得开发者不需要直接操作底层的事件对象,而是使用 React 提供的统一事件对象来处理事件。
  • React 的合成事件还具有事件冒泡、事件代理、事件委托等功能。
  • 减少了内存消耗,提升了性能。
  • React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM,这减少了内存开销。
    juejin.cn/spost/73432…

React中为什么要给组件设置 key?

可以复用一些只是位置发生变化而内容没变的dom元素。

旧虚拟DOM中找到了与新虚拟DOM相同的key:
①.若虚拟DOM中内容没变,直接使用之前的真实DOM。
②.若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

React Hooks带来了什么便利?

  • 使得函数组件能使用state和生命周期方法。
  • 更容易复用组件代码。
    1.通过自定义Hooks,可以将一些共享的状态逻辑、副作用处理逻辑等提取出来,简化组件代码,提高代码复用性。
    2.采用Hooks编写的组件更接近纯函数,使得逻辑更容易复用和测试。
    3.更符合函数式编程的思想,可以更容易地进行逻辑的拆分和组合。
  • 更易于测试:由于 Hooks 是纯函数,因此更易于对逻辑进行单元测试,提高代码质量和可靠性。

为什么不能在 循环/条件判断/嵌套函数 中调用 hooks?

组件第一次执行多个useState时,赋值。后面再次执行时,能够记住并返回对应值,依赖Hook的调用顺序。
React Hook 只能在 React 函数组件的顶层调用,而不能在循环、条件或嵌套函数中调用。这是因为 React Hook 的设计和实现依赖于 React 的组件渲染过程。在函数组件中,React Hook 的调用顺序必须保持稳定。React 依赖于 Hook 的调用顺序来正确地管理组件的状态和效果。

useState的大致实现:
image.png

列举几个常见的 Hook?

  • useState:类似类式组件中 this.state、setState 的功能。
  • useEffect:它的本意是监听一些数据,当数据更新的时候做一些事情。可以模拟生命周期钩子componentDidMount、componentDidUpdate和componentWillUnmount。
  • useRef:获取组件的真实节点。
import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  const handleButtonClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleButtonClick}>Focus Input</button>
    </div>
  );
}

export default MyComponent;
  • useContext:获取 context 对象,用来向组件树深层传递数据时的一个环节。
import React, { useContext } from 'react';

// 创建一个上下文对象
const MyContext = React.createContext();

// 在组件中使用上下文提供者
const MyComponent = () => {
  return (
    <MyContext.Provider value="Hello">
      <ChildComponent />
    </MyContext.Provider>
  );
};

// 在子组件中使用 useContext() 访问上下文
const ChildComponent = () => {
  const value = useContext(MyContext); // 这里的参数是 MyContext
  return <div>{value}</div>; // 这里的 value 是来自 MyContext 的值
};
  • useMemo:用于对计算昂贵的值进行记忆化处理,以提高性能。它接受一个函数和一个依赖项数组作为参数,并返回由该函数计算得出的值。在组件重新渲染时,useMemo 会缓存上一次计算的值,只有在依赖项数组发生变化时才会重新计算。
import React, { useMemo } from 'react';

function MyComponent({ a, b }) {
  // 使用 useMemo 缓存计算结果
  const result = useMemo(() => {
    console.log('Calculating result...');
    return a * b;
  }, [a, b]); // 依赖项数组包含 a 和 b

  return <div>Result: {result}</div>;
}

export default MyComponent;
  • useCallback:用于记忆化函数,以避免在每次渲染时创建新的函数实例。从而减少不必要的重复渲染。它接受一个函数和一个依赖项数组作为参数,并返回一个记忆化后的函数。当依赖项数组中的任何一个值发生变化时,useCallback 返回的函数实例才会重新计算,否则将会复用上一次的函数实例。
import React, { useState, useCallback } from 'react';

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

  // 使用 useCallback 缓存回调函数
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 空依赖项数组表示不依赖任何外部值

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

export default MyComponent;

11.useRef()有什么用?

在 React 中,用于在函数组件中创建可变的引用。它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。useRef 返回的 ref 对象在整个组件的生命周期内保持不变,即使组件重新渲染,也不会改变。

获取 DOM 元素的引用:您可以使用 useRef 来获取函数组件中的 DOM 元素的引用。通过将 ref 属性赋值为 useRef 创建的 ref 对象,您可以在组件中访问到该 DOM 元素。

import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

export default MyComponent;

存储任意可变值:除了 DOM 元素的引用之外,useRef 还可以用于存储任意可变值。通过修改 useRef 返回的 ref 对象的 .current 属性,您可以在组件的重新渲染中保存和更新数据,而无需触发重新渲染

这段代码中,times记录渲染次数,每次渲染次数加一,但是加一的操作又会导致重新渲染,从而陷入死循环。

let App = function App() {
  const [name, setName] = useState('');
  const [times, setTimes] = useState(0);

  useEffect(() => {
    setTimes(times => times + 1);
  });

  return (
    <>
      <input onChange={e => setName(e.target.value)} />
      <div>my name is {name}</div>
      <div>i rendered {times} times</div>
    </>
  );
}

简单介绍下React中的 diff 算法

  1. 首先对比两棵树的根节点
    a. 如果根节点的类型改变了,比如 div 变成了 p,那么直接认为整棵树都变了,不再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树。
    b. 如果根节点的类型没变,就看看属性变了没有
    ⅰ. 如果没变,就保留对应的真实节点
    ⅱ. 如果变了,就只更新该节点的属性,不重新创建节点。
  2. 然后同时遍历两棵树的子节点,每个节点的对比过程同上。

为什么不能用数组下标来作为react组件中的key?

1.key用来标记相同的条目,如果只是变换位置,让React可以识别,能够复用。如果用下标作为key,同一个条目的下标是变化的,起不到复用的作用。
2.而且可能导致两次渲染中不同的条目key相同,导致React重用不同的条目,从而出现意料之外的效果。(输入框内已输入内容,第二次渲染重用了此输入框)

React Fiber是什么?(可优化)

React Fiber

www.google.com.hk/search?q=wh…
blog.logrocket.com/deep-dive-r…

React中的路由懒加载是什么?原理是什么?

是什么:按需加载页面,而不是一次性请求所有页面。所有JS代码被打包到一起,会导致首次加载速度较慢,按需加载时性能和加载速度都更好。

如何使用:在 React 中,通常使用 import()、React.lazy() 和 Suspense 组件来实现路由懒加载。
引入组件方式的变化:

// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

如上代码中,通过 import()、React.lazy 和 Suspense 共同一起实现了 React 的懒加载。

原理:关于import() 原理,在浏览器宿主环境中一个import()的参考实现如下:

function import(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };

    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
}

结合上面的代码来看,webpack 通过创建 script 标签来实现动态加载的。
关于 Suspense 组件,Suspense 内部主要通过捕获组件的状态去判断如何加载, React.lazy 创建的动态加载组件具有 Pending、Resolved、Rejected 三种状态,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。

juejin.cn/spost/73449…
fe.ecool.fun/topic/de046…

16.React有哪些性能优化的方法?

juejin.cn/post/734762…

减少渲染的节点/降低渲染计算量(复杂度)

  • 不要在渲染函数都进行不必要的计算,比如不要在渲染函数(render)中进行数组排序、数据转换、订阅事件等等。
  • 虚拟列表。只渲染当前视口可见元素。
  • 惰性渲染。初衷本质上和虚表一样,也就是说只在必要时才去渲染对应的节点。

避免重新渲染

  • 不变的事件处理器。避免使用箭头函数形式的事件处理器, 例如:
<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />
  • 简化 state。不是所有状态都应该放在组件的 state 中。如果需要组件响应它的变动, 才应该放到 state 中。这样可以避免不必要的数据变动导致组件重新渲染。

避免不必要的渲染:通过正确使用 shouldComponentUpdate(对于类组件)或 React.memo(对于函数组件)来避免不必要的组件重新渲染。

fe.ecool.fun/topic/060a3…

React 组件间怎么进行通信?

父=>子:props。
子=>父:父组件向子组件传递函数,由子组件调用,调用时传入值。
兄弟之间:以父组件作为中介,A传给父,修改了父的state值,父通过props传给B。
父组件=>后代组件:通过 useContext。
其他情况:使用全局资源管理工具,如redux等。

Redux 中异步的请求怎么处理?(待完成)

Redux 状态管理器和变量挂载到 window 中有什么区别?
Redux中的connect有什么作用?

useEffect 与 useLayoutEffect 有什么区别?(待完成)

下面函数组件的输出分别是什么?

先点击“alert”按钮,再点击一次“add”按钮,那么弹窗框中的值和页面中展示value分别是什么?

const FunctionComponent = () => {
  const [value, setValue] = useState(1)

  const log = () => {
    setTimeout(() => {
      alert(value)
    }, 3000);
  }

  return (
    <div>
      <p>FunctionComponent</p>
      <div>value: {value}</div>
      <button onClick={log}>alert</button>
      <button onClick={() => setValue(value + 1)}>add</button>
    </div>
  )
}

弹出的值是 1,页面显示的值是 2。原因:setValue会导致整个组件重新执行,每次都生成新的log函数,每次也都重新声明了变量value,所以每个log函数对应的value变量都是不同的,并不对应最新的value值。这种现象被称为“闭包陷阱”或者“Capture Value”。

使用 useRef 解决:

const FunctionComponent = () => {
  const [value, setValue] = useState(1)
  const countRef = useRef(value)

  const log = () => {
    setTimeout(() => {
      alert(countRef.current)
    }, 3000);
  }

  useEffect(() => {
    countRef.current = value
  }, [value])

  return (
    <div>
      <p>FunctionComponent</p>
      <div>value: {value}</div>
      <button onClick={log}>alert</button>
      <button onClick={() => setValue(value + 1)}>add</button>
    </div>
  )
}

useRef 每次 render 时都会返回同一个引用类型的对象
overreacted.io/how-are-fun…

21.React中,父子组件的生命周期执行顺序是怎么样的?

总结:

  • 在挂载阶段,首先会调用父组件的生命周期方法,然后是子组件的生命周期方法。
  • 在更新阶段,也是先调用父组件的生命周期方法,然后是子组件的生命周期方法。
  • 在卸载阶段,先调用子组件的 componentWillUnmount 方法,然后才是父组件的。

  • 挂载阶段:父组件:constructor -> render -> componentDidMount,子组件:constructor -> render -> componentDidMount。
  • 更新阶段:父组件:render -> componentDidUpdate,子组件:render -> componentDidUpdate。
  • 卸载阶段:子组件:componentWillUnmount,父组件:componentWillUnmount。

React.memo() 和 useMemo() 的用法是什么?

React.memo 是 React 提供的一个高阶组件,用于优化函数组件的性能。它类似于类组件中的 shouldComponentUpdate 生命周期钩子,用于避免不必要的重新渲染。
当你将一个函数组件包裹在 React.memo 中时,React 会将该组件的 props 与前一次渲染时的 props 进行比较。只有在 props 发生变化时,才会重新渲染该组件。如果 props 没有发生变化,则会直接返回上一次渲染的结果,从而减少不必要的重复渲染。

import React from 'react';

const MyComponent = React.memo(function MyComponent({ prop1, prop2 }) {
  // 渲染组件的逻辑
});

export default MyComponent;

useMemo:用于对计算昂贵的值进行记忆化处理,以提高性能。它接受一个函数和一个依赖项数组作为参数,并返回由该函数计算得出的值。在组件重新渲染时,useMemo 会缓存上一次计算的值,只有在依赖项数组发生变化时才会重新计算。

import React, { useMemo } from 'react';

function MyComponent({ a, b }) {
  // 使用 useMemo 缓存计算结果
  const result = useMemo(() => {
    console.log('Calculating result...');
    return a * b;
  }, [a, b]); // 依赖项数组包含 a 和 b

  return <div>Result: {result}</div>;
}

export default MyComponent;

总结:React.memo() 缓存组件,useMemo() 缓存函数的计算结果。

使用 React hooks 怎么实现类里面的所有生命周期?

class 组件Hooks 组件
constructoruseState
render函数本身
componentDidMountuseEffect 第二个参数为[]
componentDidUpdateuseEffect 第二个参数为需要监视的 props 或 state
componentWillUnmountuseEffect 里面返回的函数
shouldComponentUpdateReact.memo
juejin.cn/post/684490…

实现一个 useTimeout Hook??

如果直接在函数式组件中使用 setTimeout ,会遇到以下问题。
多次调用setTimeout。我们原本的目的是在页面渲染完3s后修改一下state,但是你会发现当state+1后,触发了页面的重新渲染,就会重新有一个3s的定时器出现来给state+1,既而变成了每3秒+1。

 function App() {  
    const [state, setState] = useState(1);  
    setTimeout(() => {  
        setState(state + 1);  
    }, 3000);  
    return (    
        <div> {state} </div>  
    );  
  }; 

useTimeout 实现:

function useTimeout(callback, delay) {
  const memorizeCallback = useRef();

  useEffect(() => {
    memorizeCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const timer = setTimeout(() => {
        memorizeCallback.current();
      }, delay);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [delay]);
};

说说React Router有几种模式,以及实现原理?

共同点:改变 url 且不让浏览器向服务器发送请求。

hash 模式:改变hash值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页面。hash 值改变,触发全局 window 对象上的 hashchange 事件。所以 hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转。

history模式:Html5的History可以监听到路径变化,还可以直接修改路径同时让页面不跳转。(需要后端将所有对页面的请求都返回index.html)

示例代码:

import React from 'react';
import { BrowserRouter as Router, Route } from "react-router-dom";
import Home from './pages/Home';
import Login from './pages/Login';
import Admin from './pages/Admin';

function App() {
  return (
    <Router>
      <Route path="/login" component={Login}/>
      <Route path="/admin" component={Admin}/>
      <Route path="/" component={Home}/>
    </Router>
  );
}
export default App;

fe.ecool.fun/topic/2b68a…

26.说说你对React Router的理解?常用的Router组件有哪些?

Router的功能使得前端能够全面控制页面的切换,实现了页面代码的缓存和复用。这种方式可以提供更快的加载速度和更流畅的用户体验。
状态管理:在某些情况下,页面之间可能需要共享状态或数据。Router 功能提供了一种在不同页面之间共享状态和数据的方式,以便在整个应用程序中保持一致的用户体验。
路由参数和动态路由:有时需要将参数传递给页面,以便根据参数加载不同的内容。Router 功能允许您定义路由参数和动态路由,从而根据不同的 URL 加载不同的页面或组件。

常用的Router组件:
BrowserRouter、HashRouter:Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件。BrowserRouter是history模式,HashRouter模式。使用两者作为最顶层组件包裹其他组件。 Route:用于路径的匹配,然后进行组件的渲染。path 属性:用于设置匹配到的路径。component 属性:设置匹配到路径后,渲染的组件。
Link、NavLink:通常路径的跳转是使用Link组件,最终会被渲染成a元素。
Redirect:用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中。
Switch:作用是当匹配到第一个组件的时候,后面的组件就不应该继续匹配。

react-router还提供一些hooks:
useHistory:useHistory可以让组件内部直接访问history,无须通过props获取。
useParams:用于在函数组件中访问 URL 中的动态路由参数。
useLocation:返回当前 URL的 location 对象。

react-router 里的 <Link> 标签和 <a> 标签有什么区别?

使用 <Link> 标签可以避免浏览器的页面刷新,因为它会使用 React Router 提供的路由机制进行导航,而不会发送新的 HTTP 请求。(背后是用history api修改url,通过react router切换页面组件)
使用 <a> 标签时,浏览器会向服务器发送新的 HTTP 请求,导致页面刷新。(相当于在地址栏输入地址并回车)