面试题[React生态系统]
1. React Router
1.1 React Router 设计原理
1.2 Router 组件有几种类型
在 React Router 中,主要有三种类型的 Router 组件:BrowserRouter、HashRouter、MemoryRouter。
BrowserRouter
使用 HTML5 的 history API (pushState, replaceState 和 popstate 事件) 来保持 UI 和 URL 同步,非常适用于现代 web 应用。
- 使用场景:现代应用程序,SEO 友好,并且希望能使用浏览器的前进和后退按钮的地方。
- 优点:URL 美观,没有多余的符号(如
#
);可以直接导航到特定的 URL。 - 缺点:若服务器没有设置好重定向,当直接访问某个子路由时可能会出现 404 错误,因此需要在服务器端做一些配置。
HashRouter
使用 URL 的 hash 部分(比如
window.location.hash
)来保持 UI 和 URL 同步,主要用于兼容不支持 HTML5 history API 的浏览器。- 使用场景:老旧浏览器或者静态文件托管服务,不需要服务器端配置即可运行。
- 优点:不需要后端支持,也没有 404 问题。
- 缺点:URL 中包含
#
符号,不够美观;影响 SEO,因为搜索引擎通常不会索引#
之后的内容。
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
钩子函数来实现重定向。(新版不同请注意)
使用
<Redirect>
组件:在 React Router v5 中,你可以通过在
<Switch>
组件中使用<Redirect>
组件来设置重定向。例如:jsximport { 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> );
使用
useHistory
钩子:在函数组件中,你可以使用
useHistory
钩子进行编程式导航,如下所示:jsximport { 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> ); };
扩展知识
React Router 的版本差异: React Router v6 进行了重大改动,移除了
<Redirect>
组件,改为使用<Navigate>
组件,同时引入新的路由配置方案。使用示例如下:jsximport { 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
,用法类似:jsximport { 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 对象中区别主要在于以下几个方面:
- 统一管理:Redux提供了一个集中的存储机制,可以将整个应用的状态集中在一个地方(key:value对象),并通过定义的动作(actions)来改变状态。这种集中管理方便对状态的更改进行跟踪,利于团队协作和调试。
- 调试工具支持:Redux 提供了强大的调试工具如 Redux DevTools,可以对状态变化进行可视化跟踪,帮助开发者迅速定位问题。而 window 对象上挂载的变量无法提供这种便利的调试支持。
- 性能优化:Redux 通过不可变数据结构和应答式更新机制(reactive updates)有效地优化了性能。它确保了只有受影响的组件重新渲染,而不是整个应用。
- 安全性:将变量挂载到 window 对象中存在潜在的安全隐患,因为所有全局变量都容易受到外部脚本的影响和修改。Redux 中的状态是私有的,不可以直接被外部访问和修改,这提升了应用的安全性。
- 扩展性和中间件支持:Redux 通过中间件(middleware)机制,可以扩展功能,如处理异步操作(redux-thunk,redux-saga等),在状态管理过程中进行日志记录、错误报告等,提升了应用的可扩展性。
除了上面提到的主要区别,下面我会从几个方面进行更详细的扩展:
- 应用规模和复杂度:对于小型应用,直接将变量挂载到 window 对象中可能比较简单直观,维护成本较低。但随着应用的规模和复杂度增加,管理全局状态会变得更加复杂,这时候引入状态管理库如 Redux 会更有利于维护和扩展。
- 不可变性和纯函数:Redux 以纯函数(reducers)来定义状态转换规则,是一个不可变的状态管理机制。每次状态变化都会创建一个新的状态对象,这支持时间旅行调试和状态回溯。相比之下,window 对象范式更可能导致数据的不可控更改。
- 模块化和可维护性:Redux 提倡模块化,通过 action、reducer 和 store 的分离,增强了代码的可读性和可维护性。window 对象更适用于临时和全局共享变量的情况,但随着变量和逻辑的增加,会更容易混乱和难以维护。
- 服务端渲染支持:Redux 有很好的服务端渲染(SSR)支持,可以方便地在服务端初始化状态。而将状态挂载到 window 对象中的方式则较为繁琐,容易引入不一致的情况。
- 开发者社区和生态系统:Redux 拥有庞大的开发者社区和丰富的生态系统,除了 Redux 本身,还有很多配套的工具和库(如 Redux Toolkit、redux-form 等)。而 window 依赖自身实现管理逻辑,没有统一的标准和社区支持。
3.4 Redux 中间件的实现原理
在 Redux 中,中间件(Middleware)是一个可以在 action 被发起之后,到达 reducer 之前对 action 进行加工处理的函数。其主要目的是扩展 Redux dispatch 行为,使我们能够在 action 到达 reducer 之前进行额外的操作,如日志记录、错误报告、异步操作等。
Redux 中间件的实现原理可以总结为:“中间件是一个返回一个新版本的 dispatch 函数的函数,从而在调用 dispatch 时执行自定义逻辑。”
在 Redux 中你定义中间件的方式是一个函数,它接收 store
的 dispatch
和 getState
方法,并返回一个函数,接收 next
方法。next
方法调用后,会继续传递 action 给下一个中间件或 reducer。
一个简单的中间件例子如下:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
中间件在 Redux 应用中的作用
- 1)日志记录:记录每一次的 action 和状态变化,便于调试和监控。
- 2)错误报告:捕捉并处理错误,例如在生产环境中向错误报告服务发送错误信息。
- 3)异步操作:支持异步 action,例如 redux-thunk 和 redux-saga,它们让你可以写出更复杂的异步逻辑。
- 4)授权及权限控制:在特定的 action 被 dispatch 之前进行权限验证。
中间件的作用链机制
中间件的工作方式可以通过“打桩”的形式来理解,每个中间件可以看成是一个桩,每个桩可以在 action 被传递给下一个桩或者最终传递给 reducer 之前做一些处理。这样多个中间件串联起来形成一个 middleware chain,这个链条形式使得 action 依次通过每一个中间件,最后才到达 reducer。
自定义中间件
实现一个自定义中间件需要了解以下几点:
- 1)中间件格式:中间件通常是一个函数,签名是
store => next => action => result
。 - 2)调用 next(action):在自定义中间件中调用
next(action)
以保证 action 能够流向下一个中间件或 reducer。 - 3)执行时机:中间件在 dispatch action 时被依次调用。
举个例子,比如我们要实现一个简单的调试中间件,它可以为每个 action 打印日志:
javascriptconst debugMiddleware = store => next => action => { console.debug('Action Type:', action.type); const result = next(action); console.debug('New State:', store.getState()); return result; };
- 1)中间件格式:中间件通常是一个函数,签名是
使用中间件
在 Redux 中使用中间件可以通过
applyMiddleware
函数。在创建 Redux store 时,传入applyMiddleware(...middlewares)
来注册中间件。 例如:javascriptimport { createStore, applyMiddleware } from 'redux'; import rootReducer from './reducers'; import logger from './middlewares/logger'; const store = createStore( rootReducer, applyMiddleware(logger) );
多个中间件的例子
假设我们有两个中间件
logger
和thunk
,可以这样组合使用:javascriptimport { 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 中定义初始状态。