React 面试题

142 阅读15分钟

React

React是一个由Facebook创建的JavaScript库,用于构建用户界面。它是一个用于构建UI组件的工具。

React是一个前端框架,它允许开发人员使用组件化的方式来构建复杂的用户界面。React组件是独立的、可复用的代码块,它们可以接收输入并返回React元素来描述应该在页面上显示什么。

React的核心思想是声明式编程。这意味着开发人员只需要描述应用程序应该呈现什么样子,而不需要关心如何实现它。React会负责计算出如何高效地更新用户界面,以便它始终与最新的状态保持一致。

优点:

  • 高性能:React使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,React会根据新的状态创建一个新的虚拟DOM树。然后,React会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。
  • 组件化架构:React采用了组件化的架构来构建复杂的用户界面。在React中,一个组件是一个独立的、可复用的代码块,它可以接收输入并返回React元素来描述应该在页面上显示什么。开发人员可以使用组件来封装各种UI功能,并将它们组合起来构建复杂的用户界面。
  • 易于学习:相比其他前端框架,React相对容易学习。它有着丰富的文档和教程,并且有着庞大的社区支持。

缺点:

  • 开发速度:由于React不断更新和改进,开发人员需要不断学习新知识才能跟上React的发展步伐。
  • 文档不足:尽管React有着丰富的文档和教程,但由于其快速发展,有时文档可能不够完整或过时。
  • 学习曲线陡峭:尽管React相对容易学习,但要真正掌握它并开发复杂的应用程序仍然需要一定的时间和精力。

底层实现原理

React是一个JavaScript库,用于构建用户界面。它的底层实现原理包括虚拟DOM组件化架构响应式更新等。

  • 虚拟DOM:React使用虚拟DOM来提高应用的性能和渲染效率。虚拟DOM是一个轻量级的JavaScript对象,它是真实DOM的抽象表示。当组件的状态发生变化时,React会根据新的状态创建一个新的虚拟DOM树。然后,React会使用一个高效的算法来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM。这个过程被称为“reconciliation”。
  • 组件化架构:React采用了组件化的架构来构建复杂的用户界面。在React中,一个组件是一个独立的、可复用的代码块,它可以接收输入并返回React元素来描述应该在页面上显示什么。开发人员可以使用组件来封装各种UI功能,并将它们组合起来构建复杂的用户界面。
  • 响应式更新:React采用了响应式的方式来更新用户界面。当组件的状态发生变化时,React会自动计算出需要更新的部分,并高效地更新真实DOM。这样,开发人员只需要关心如何描述应用程序应该呈现什么样子,而不需要关心如何实现它。

生命周期

React组件的生命周期可分成三个状态:Mounting(挂载)、Updating(更新)和Unmounting(卸载)。

u=278311522,1651330364&fm=253&fmt=auto&app=138&f=PNG.webp

Mounting(挂载): 当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  1. constructor(): 在 React 组件挂载之前,会调用它的构造函数。
  2. getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
  3. render(): render() 方法是 class 组件中唯一必须实现的方法。
  4. componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用。

Updating(更新): 每当组件的 state 或 props 发生变化时,组件就会更新。当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  1. getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
  2. shouldComponentUpdate(): 当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。
  3. render(): render() 方法是 class 组件中唯一必须实现的方法。
  4. getSnapshotBeforeUpdate(): 在最近一次渲染输出(提交到 DOM 节点)之前调用。
  5. componentDidUpdate(): 在更新后会被立即调用。

Unmounting(卸载): 当组件从 DOM 中移除时会调用如下方法:

  1. componentWillUnmount(): 在组件卸载及销毁之前直接调用。

React-fiber

React-fiber是对React核心算法的一次重新实现。它能让React中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。Fiber能够将渲染工作分割成块并将其分散到多个帧中。同时加入了在新的更新进入时暂停,中止或重复工作的能力和为不同类型的更新分配优先级的能力。

在Fiber诞生之前,React处理一次setState(首次渲染)时会有两个阶段:调度阶段(Reconciler)和渲染阶段(Renderer)。调度阶段React用新数据生成新的Virtual DOM,遍历Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去。渲染阶段React根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是更新对应的DOM元素。

这种设计看似合理,但是对于复杂组件,需要大量的diff计算,会严重影响到页面的交互性。例如,假设更新一个组件需要1ms,如果有500个组件要更新,那就需要500ms,在这500ms的更新过程中,浏览器唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。这就是所谓的界面卡顿。

React-fiber就是为了解决渲染复杂组件时严重影响用户和浏览器交互的问题。实现原理可以简单分为以下几个步骤:

  1. 将一次任务拆解成单元。
  2. 以划分时间片的方式,按照Fiber的自己的调度方法,根据任务单元优先级,分批处理或吊起任务。
  3. 将一次更新分散在多次时间片中。
  4. 在浏览器空闲的时候,也可以继续去执行未完成的任务。

这样,React Fiber就能够充分利用浏览器每一帧的工作特性,避免渲染复杂组件时严重影响用户和浏览器交互的问题。

组件

React组件是组成React应用程序的可重复利用的模块。它们是用于构建Web和原生交互界面的库。

React组件可以分为两种类型:函数组件类组件

  • 函数组件是一个接受props作为参数并返回一个React元素的函数。它们通常用于简单的、无状态的组件。
  • 类组件是一个继承自React.Component的类,它包含一个render方法,该方法返回一个React元素。类组件通常用于更复杂的、有状态的组件。

主要的区别

  • 语法不同:函数组件是一个简单的JavaScript函数,而类组件是一个继承自React.Component的类。
  • 功能不同:函数组件通常用于简单的、无状态的组件,而类组件通常用于更复杂的、有状态的组件。
  • 状态管理不同:函数组件没有自己的状态和生命周期方法,而类组件具有自己的状态和生命周期方法。
  • 更新控制不同:类组件可以使用一些特殊的方法来控制组件的更新过程,而函数组件则无法使用这些方法。

示例:

// 函数组件
function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}

const Free = props =>{
  return <h1>Hello, {props.name}</h1>;
}

// 类组件
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
      </div>
    );
  }
}

通讯方式

  1. 父子组件之间:父向子,可以通过props的方式传递。子组件可以通过props对象访问这些数据。子向父,子组件可以通过调用父组件传递给它的回调函数来向父组件传递数据。
// 父向子传
function Parent() {
  const message = "来自父组件的问候";
  return <Child message={message} />;
}

function Child(props) {
  return <p>{props.message}</p>;
}

// 子向父传
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { message: "" };
    this.handleMessage = this.handleMessage.bind(this);
  }

  handleMessage(newMessage) {
    this.setState({ message: newMessage });
  }

  render() {
    return (
      <>
        <Child onMessage={this.handleMessage} />
        <p>{this.state.message}</p>
      </>
    );
  }
}

function Child(props) {
  function handleClick() {
    props.onMessage("来自幼儿园技术家的问候");
  }

  return <button onClick={handleClick}>发送消息</button>;
}
  1. 兄弟组件之间:兄弟组件之间的数据传递,可以利用组件的Props以及Props回调函数来进行,而这种使用方法通信的前提是:必须要有共同的父组件。父组件可以维护一个状态,并将状态作为props传递给兄弟组件。同时,父组件还可以定义一个回调函数,用于更新状态,并将该回调函数作为props传递给兄弟组件。这样,兄弟组件就可以通过调用回调函数来更新状态,从而实现兄弟组件之间的通信。
    示例:
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { message: "" };
    this.handleMessage = this.handleMessage.bind(this);
  }

  handleMessage(newMessage) {
    this.setState({ message: newMessage });
  }

  render() {
    return (
      <>
        <ChildA onMessage={this.handleMessage} />
        <ChildB message={this.state.message} />
      </>
    );
  }
}

function ChildA(props) {
  function handleClick() {
    props.onMessage("Hello from ChildA");
  }

  return <button onClick={handleClick}>Send Message</button>;
}

function ChildB(props) {
  return <p>{props.message}</p>;
}
  1. 跨组件层级:可以使用Context API来实现跨组件层级的通信。使用createContext方法创建一个Context对象,然后使用Provider组件包裹根组件,并通过value属性提供要共享的数据。在任意后代组件中,使用Consumer组件包裹整个组件,就可以获取到共享的数据。
    示例:
const MessageContext = React.createContext();

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { message: "Hello from Parent" };
  }

  render() {
    return (
      <MessageContext.Provider value={this.state.message}>
        <Child />
      </MessageContext.Provider>
    );
  }
}

function Child() {
  return (
    <>
      <Grandchild />
    </>
  );
}

function Grandchild() {
  return (
    <MessageContext.Consumer>
      {(message) => <p>{message}</p>}
    </MessageContext.Consumer>
  );
}
  1. 全局状态管理:对于非嵌套关系的组件通信,可以使用全局状态管理库,如Redux或MobX。这些库可以在应用程序的顶层维护一个全局状态,并允许组件订阅状态变化并更新其自身。这样,即使组件之间没有直接的嵌套关系,它们也可以共享状态并进行通信。
    示例:
import { createStore } from "redux";

// Redux store
const initialState = { message: "" };
function reducer(state = initialState, action) {
  switch (action.type) {
    case "SET_MESSAGE":
      return { message: action.message };
    default:
      return state;
  }
}
const store = createStore(reducer);

// Parent component
class Parent extends React.Component {
  render() {
    return (
      <>
        <ChildA />
        <ChildB />
      </>
    );
  }
}

// ChildA component
function ChildA() {
  function handleClick() {
    store.dispatch({ type: "SET_MESSAGE", message: "Hello from ChildA" });
  }

  return <button onClick={handleClick}>Send Message</button>;
}

// ChildB component
class ChildB extends React.Component {
  constructor(props) {
    super(props);
    this.state = { message: "" };
  }

  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      const state = store.getState();
      this.setState({ message: state.message });
    });
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  render() {
    return <p>{this.state.message}</p>;
  }
}

复用方式

React组件复用可以提高开发效率,减少Bug和程序体积。设计接口时,可以把通用的设计元素(按钮,表单框,布局组件等)拆成接口良好定义的可复用的组件。这样,下次开发相同界面程序时就可以写更少的代码。

复用方式有以下几种:

  1. Props:通过props将数据和回调函数传递给子组件,可以实现组件的复用。
    示例:
function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}
  1. 高阶组件(HOC):高阶组件是一种用于复用组件逻辑的高级技巧。它是一个接受组件作为参数并返回一个新组件的函数。
    示例:
function withGreeting(WrappedComponent) {
  return function(props) {
    return (
      <>
        <Greeting name={props.name} />
        <WrappedComponent {...props} />
      </>
    );
  };
}
  1. Render Props:Render Props是一种在React组件之间使用一个值为函数的prop共享代码的简单技术。
    示例:
function Greeting(props) {
  return props.children("Hello");
}

function App() {
  return (
    <Greeting>
      {greeting => (
        <>
          <h1>{greeting}, World</h1>
          <h2>{greeting}, React</h2>
        </>
      )}
    </Greeting>
  );
}

React Hooks

React Hooks是一种新的API,它允许你在函数组件中使用状态和其他React特性。

常用的钩子有:

  1. useState(状态钩子):useState是一个允许你在函数组件中添加状态的Hook。它返回一个状态变量和一个更新该状态变量的函数。
    示例:
import { useState } from "react";

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

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
    </div>
  );
}
  1. useEffect(副作用钩子):useEffect是一个允许你在函数组件中执行副作用的Hook。它接受一个函数作为参数,该函数将在组件渲染后执行。
    示例:
import { useState, useEffect } from "react";

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

  useEffect(() => {
    document.title = `你点击了 ${count} 次`;
  });

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
    </div>
  );
}
  1. useContext(共享状态钩子):useContext是一个允许你在函数组件中访问上下文的Hook。它接受一个上下文对象作为参数,并返回该上下文的当前值。
    示例:
import { useContext } from "react";

const ThemeContext = React.createContext("light");

function Example() {
  const theme = useContext(ThemeContext);

  return <p>Current theme: {theme}</p>;
}
  1. useReducer(action 钩子):useReducer是一个允许你在函数组件中使用类似于Redux的状态管理模式的Hook。它接受一个reducer函数和初始状态作为参数,并返回当前状态和一个dispatch函数。
    示例:
import { useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Example() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

其他钩子函数:useCallback(记忆函数),useMemo(记忆组件)和useRef(保存引用值)等。

其他

React diff算法

React的diff算法是一种高效的算法,它用来计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面,从而提高了页面渲染效率。简单来说,diff算法就是通过最小代价将旧的fiber树转换为新的fiber树。

React的每次更新,都会将新的ReactElement内容与旧的fiber树作对比,比较出它们的差异后,构建新的fiber树,将差异点放入更新队列之中,从而对真实dom进行render。

diff算法在React中处于主导地位,是React V-dom和渲染的性能保证,这也是React最有魅力、最吸引人的地方。React一个很大一个的设计有点就是将diffV-dom的完美结合,而高效的diff算法可以让用户更加自由的刷新页面,让开发者也能远离原生dom操作。

setState

setState()是React中用来更新组件状态的方法。当你调用setState()时,React会将你提供的对象与当前状态合并。例如,你的状态可能包含几个独立的变量:constructor(props) {super(props);this.state = {posts: [], comments: []};} 。

那么 setState 到底是同步还是异步的呢?
React中的setState()并不是真正意义上的异步,而是一个伪异步或者称为延迟执行。它的执行顺序在同步代码后、异步代码前。这种现象得益于React的合成事件,React的批处理更新也得益于合成事件。

注意:setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。

而 setState 关于同异步也可以分两种情况讨论:

  1. 在React事件处理程序和生命周期方法中,setState()是异步的,这意味着在调用setState()后,state不会立即更新。
    示例:
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 输出的是更新前的值
  }

  render() {
    return (
      <div>
        <p>你点击了 {this.state.count} 次</p>
        <button onClick={this.handleClick}>
          Click me
        </button>
      </div>
    );
  }
}
  1. 在setTimeout事件或者自定义的DOM事件中,setState()是同步的,这意味着在调用setState()后,state会立即更新。
    示例:
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // 输出的是更新后的值
    }, 0);
  }

  render() {
    return (
      <div>
        <p>你点击了 {this.state.count} 次</p>
      </div>
    );
  }
}

事件绑定原理

React事件绑定的原理与传统的DOM事件绑定有所不同。在传统的DOM事件中,我们通常会将事件处理程序直接绑定到DOM元素上。但是,在React中,事件处理程序并不是直接绑定到真实的DOM元素上,而是在document处监听所有支持的事件。当事件发生并冒泡到document处时,React会将事件内容封装并交由真正的处理函数运行。

React中的事件都是合成事件,不是把每一个dom的事件绑定在dom上,而是把事件统一绑定到document中,触发时通过事件冒泡到document进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation去阻止,而是使用e.preventDefault去阻止。

636026-20180912005017389-1335681466.png

这种设计可以提高性能,因为它避免了在每个DOM元素上都绑定事件处理程序。此外,它还使得React能够更好地控制事件的传播和处理。

React key的作用

在React中,key是一个特殊的字符串属性,它可以帮助React识别哪些元素发生了变化。当你渲染一个列表时,你应该给每个列表项分配一个稳定的、唯一的key。这样,当列表项的顺序发生变化时,React就能够正确地更新列表。

key的作用是帮助React确定哪些元素需要被重新渲染。当组件更新时,React会比较新旧两个Virtual DOM树,找出它们之间的差异。如果两个元素具有不同的key,React就会认为它们是不同的元素,并重新渲染它们。