Skip to content
On This Page

面试题[React生态系统]

1. React Router

1.1 React Router 设计原理

1.2 Router 组件有几种类型

在 React Router 中,主要有三种类型的 Router 组件:BrowserRouter、HashRouter、MemoryRouter。

  1. BrowserRouter

    使用 HTML5 的 history API (pushState, replaceState 和 popstate 事件) 来保持 UI 和 URL 同步,非常适用于现代 web 应用。

    • 使用场景:现代应用程序,SEO 友好,并且希望能使用浏览器的前进和后退按钮的地方。
    • 优点:URL 美观,没有多余的符号(如 #);可以直接导航到特定的 URL。
    • 缺点:若服务器没有设置好重定向,当直接访问某个子路由时可能会出现 404 错误,因此需要在服务器端做一些配置。
  2. HashRouter

    使用 URL 的 hash 部分(比如 window.location.hash)来保持 UI 和 URL 同步,主要用于兼容不支持 HTML5 history API 的浏览器。

    • 使用场景:老旧浏览器或者静态文件托管服务,不需要服务器端配置即可运行。
    • 优点:不需要后端支持,也没有 404 问题。
    • 缺点:URL 中包含 # 符号,不够美观;影响 SEO,因为搜索引擎通常不会索引 # 之后的内容。
  3. MemoryRouter

    保持内存中的一个 non-URL 路径历史记录,这个经常在测试和非浏览器环境(比如 React Native)中使用。

    • 使用场景:测试环境或在没有 URL 功能的环境(例如 React Native)中使用。
    • 优点:完全在内存中处理路由,无需浏览器环境,可以模拟各种导航情况。
    • 缺点:因为不使用实际 URL,用户无法直接通过 URL 访问特定视图,主要用于非 web 应用和自动化测试。

1.3 history 模式中,push 和 replace 的区别

在 React Router 的 history 模式中,push 和 replace 两个方法的主要区别在于它们如何管理浏览历史记录。

  • 1)push 方法:用于将一个新的记录添加到浏览历史记录中。用户可以通过点击浏览器的“返回”按钮回到前一个页面。
  • 2)replace 方法:用于将当前的历史记录替换为新的记录。使用这个方法用户无法通过浏览器的“返回”按钮回到前一个页面。

简单来说,push 会创建一个新的历史记录,而 replace 则是替换当前的历史记录。

1.4 React Router 中设置重定向

在 React Router 中设置重定向,你可以使用 <Redirect> 组件或使用 useHistory 钩子函数来实现重定向。(新版不同请注意)

  1. 使用 <Redirect> 组件:

    在 React Router v5 中,你可以通过在 <Switch> 组件中使用 <Redirect> 组件来设置重定向。例如:

    jsx
    import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
    
    const App = () => (
      <Router>
        <Switch>
          <Route exact path="/" component={HomePage} />
          <Route path="/about" component={AboutPage} />
          <Redirect from="/old-path" to="/new-path" />
          <Redirect to="/" />
        </Switch>
      </Router>
    );
  2. 使用 useHistory 钩子:

    在函数组件中,你可以使用 useHistory 钩子进行编程式导航,如下所示:

    jsx
    import { useHistory } from 'react-router-dom';
    
    const SomeComponent = () => {
      const history = useHistory();
    
      const handleRedirect = () => {
        history.push('/new-path');
      };
    
      return (
        <button onClick={handleRedirect}>Go to New Path</button>
      );
    };
  3. 扩展知识

    • React Router 的版本差异: React Router v6 进行了重大改动,移除了 <Redirect> 组件,改为使用 <Navigate> 组件,同时引入新的路由配置方案。使用示例如下:

      jsx
      import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
      
      const App = () => (
        <Router>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
            <Route path="old-path" element={<Navigate to="/new-path" />} />
            <Route path="*" element={<Navigate to="/" />} />
          </Routes>
        </Router>
      );
    • 编程式导航: 除了 useHistory,React Router v6 引入了 useNavigate 钩子来替代 useHistory,用法类似:

      jsx
      import { useNavigate } from 'react-router-dom';
      
      const SomeComponent = () => {
        const navigate = useNavigate();
      
        const handleRedirect = () => {
          navigate('/new-path');
        };
      
        return (
          <button onClick={handleRedirect}>Go to New Path</button>
        );
      };

2. React 测试

3. Redux

3.1 Redux 设计原理

3.2 Redux 和 Mobx 和其它 React 状态管理的区别

3.3 Redux 状态管理器与将变量挂载到 window 对象中的区别

Redux 状态管理器和将变量挂载到 window 对象中区别主要在于以下几个方面:

  1. 统一管理:Redux提供了一个集中的存储机制,可以将整个应用的状态集中在一个地方(key:value对象),并通过定义的动作(actions)来改变状态。这种集中管理方便对状态的更改进行跟踪,利于团队协作和调试。
  2. 调试工具支持:Redux 提供了强大的调试工具如 Redux DevTools,可以对状态变化进行可视化跟踪,帮助开发者迅速定位问题。而 window 对象上挂载的变量无法提供这种便利的调试支持。
  3. 性能优化:Redux 通过不可变数据结构和应答式更新机制(reactive updates)有效地优化了性能。它确保了只有受影响的组件重新渲染,而不是整个应用。
  4. 安全性:将变量挂载到 window 对象中存在潜在的安全隐患,因为所有全局变量都容易受到外部脚本的影响和修改。Redux 中的状态是私有的,不可以直接被外部访问和修改,这提升了应用的安全性。
  5. 扩展性和中间件支持:Redux 通过中间件(middleware)机制,可以扩展功能,如处理异步操作(redux-thunk,redux-saga等),在状态管理过程中进行日志记录、错误报告等,提升了应用的可扩展性。

除了上面提到的主要区别,下面我会从几个方面进行更详细的扩展:

  1. 应用规模和复杂度:对于小型应用,直接将变量挂载到 window 对象中可能比较简单直观,维护成本较低。但随着应用的规模和复杂度增加,管理全局状态会变得更加复杂,这时候引入状态管理库如 Redux 会更有利于维护和扩展。
  2. 不可变性和纯函数:Redux 以纯函数(reducers)来定义状态转换规则,是一个不可变的状态管理机制。每次状态变化都会创建一个新的状态对象,这支持时间旅行调试和状态回溯。相比之下,window 对象范式更可能导致数据的不可控更改。
  3. 模块化和可维护性:Redux 提倡模块化,通过 action、reducer 和 store 的分离,增强了代码的可读性和可维护性。window 对象更适用于临时和全局共享变量的情况,但随着变量和逻辑的增加,会更容易混乱和难以维护。
  4. 服务端渲染支持:Redux 有很好的服务端渲染(SSR)支持,可以方便地在服务端初始化状态。而将状态挂载到 window 对象中的方式则较为繁琐,容易引入不一致的情况。
  5. 开发者社区和生态系统:Redux 拥有庞大的开发者社区和丰富的生态系统,除了 Redux 本身,还有很多配套的工具和库(如 Redux Toolkit、redux-form 等)。而 window 依赖自身实现管理逻辑,没有统一的标准和社区支持。

3.4 Redux 中间件的实现原理

在 Redux 中,中间件(Middleware)是一个可以在 action 被发起之后,到达 reducer 之前对 action 进行加工处理的函数。其主要目的是扩展 Redux dispatch 行为,使我们能够在 action 到达 reducer 之前进行额外的操作,如日志记录、错误报告、异步操作等。

Redux 中间件的实现原理可以总结为:“中间件是一个返回一个新版本的 dispatch 函数的函数,从而在调用 dispatch 时执行自定义逻辑。”

在 Redux 中你定义中间件的方式是一个函数,它接收 storedispatchgetState 方法,并返回一个函数,接收 next 方法。next 方法调用后,会继续传递 action 给下一个中间件或 reducer。

一个简单的中间件例子如下:

javascript
const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
};
  1. 中间件在 Redux 应用中的作用

    • 1)日志记录:记录每一次的 action 和状态变化,便于调试和监控。
    • 2)错误报告:捕捉并处理错误,例如在生产环境中向错误报告服务发送错误信息。
    • 3)异步操作:支持异步 action,例如 redux-thunk 和 redux-saga,它们让你可以写出更复杂的异步逻辑。
    • 4)授权及权限控制:在特定的 action 被 dispatch 之前进行权限验证。
  2. 中间件的作用链机制

    中间件的工作方式可以通过“打桩”的形式来理解,每个中间件可以看成是一个桩,每个桩可以在 action 被传递给下一个桩或者最终传递给 reducer 之前做一些处理。这样多个中间件串联起来形成一个 middleware chain,这个链条形式使得 action 依次通过每一个中间件,最后才到达 reducer。

  3. 自定义中间件

    实现一个自定义中间件需要了解以下几点:

    • 1)中间件格式:中间件通常是一个函数,签名是 store => next => action => result
    • 2)调用 next(action):在自定义中间件中调用 next(action) 以保证 action 能够流向下一个中间件或 reducer。
    • 3)执行时机:中间件在 dispatch action 时被依次调用。

    举个例子,比如我们要实现一个简单的调试中间件,它可以为每个 action 打印日志:

    javascript
    const debugMiddleware = store => next => action => {
      console.debug('Action Type:', action.type);
      const result = next(action);
      console.debug('New State:', store.getState());
      return result;
    };
  4. 使用中间件

    在 Redux 中使用中间件可以通过 applyMiddleware 函数。在创建 Redux store 时,传入 applyMiddleware(...middlewares) 来注册中间件。 例如:

    javascript
    import { createStore, applyMiddleware } from 'redux';
    import rootReducer from './reducers';
    import logger from './middlewares/logger';
    
    const store = createStore(
      rootReducer,
      applyMiddleware(logger)
    );
  5. 多个中间件的例子

    假设我们有两个中间件 loggerthunk,可以这样组合使用:

    javascript
    import { createStore, applyMiddleware } from 'redux';
    import rootReducer from './reducers';
    import logger from './middlewares/logger';
    import thunk from 'redux-thunk';
    
    const store = createStore(
      rootReducer,
      applyMiddleware(thunk, logger)
    );

3.5 Redux 中如何设置初始状态

在 Redux 中设置初始状态的方法主要有两种:

  • 1)使用 createStore 时传递初始状态。
  • 2)在 Reducer 中定义初始状态。