1、react是什么?
- React 是一个用于构建用户界面的 JavaScript 库,由 Facebook(现 Meta)开发并开源。它专注于通过组件化的方式高效构建动态、交互式的 Web 和移动应用界面。以下是 React 的核心特点和应用场景:
2、React 的核心特性是什么?
- 虚拟DOM、组件化、单向数据流、JSX 语法、声明式语法。
3、虚拟DOM
- React 在内存中维护一个轻量级的虚拟 DOM,当数据变化时,先更新虚拟 DOM,再通过对比(Diffing Algorithm)找出实际需要更新的部分,最后高效更新真实 DOM。这减少了直接操作 DOM 的性能损耗。
4、组件化
- 将界面拆分为独立、可复用的组件(如按钮、表单、页面等),通过组合组件构建复杂 UI。
- 组件可管理自身状态(数据)和逻辑,提升代码复用性和可维护性。
5、单向数据流
- 单向数据流是前端框架(如 React、Vue 等)中常见的一种数据传递模式,核心思想是数据只能按照单一方向流动,通常从父组件传递到子组件,且子组件不能直接修改父组件的数据。这种设计使得数据流更清晰、可预测,便于调试和维护。
6、JSX 语法
-
允许在 JavaScript 中直接编写类似 HTML 的代码,使组件结构更直观。例如:
javascriptfunction Button() { return <button className="primary">点击我</button>; }
7、声明式语法
- 我的理解声明式语法就是将命令式语法进行了优化升级,我们不用将每一步都写清楚比如我们之前创建一个dom需要一下这些操作document.getElementById,然后document.createElement在document.appendChild这样一步一步的去把每一步写清楚。
对比"命令式 vs 声明式"
-
命令式(How):
代码直接操作 DOM,详细描述每一步。
例子(原生 JavaScript 实现点击计数)
javascript// 1. 找到按钮和显示区域 const button = document.getElementById('btn'); const text = document.getElementById('count'); let count = 0; // 2. 监听点击事件,手动更新 DOM button.addEventListener('click', () => { count++; text.innerText = `点了 ${count} 次`; // 手动修改 DOM }); -
声明式(What):
代码描述"UI 应该长什么样",状态变化时 React 自动更新 DOM。
例子(React 实现点击计数):
javascriptfunction Counter() { const [count, setCount] = useState(0); // 声明状态 return ( <button onClick={() => setCount(count + 1)}> 点了 {count} 次 {/* React 自动更新 */} </button> ); }
React 声明式的核心体现"
-
用 JSX 描述 UI:
直接写类似 HTML 的结构,比如 点了 {count} 次,而不是拼接字符串或操作 DOM。
-
状态驱动视图:
更新数据(如 setCount)后,React 自动计算如何高效更新 DOM。
你只需关心"数据是什么",不用手动操作 DOM。
-
虚拟 DOM 的抽象层:
React 内部通过虚拟 DOM 对比(Diffing)找出变化部分,再批量更新真实 DOM,隐藏了具体操作步骤。
声明式的好处"
- 代码更简洁:不用写 document.getElementById、element.appendChild 这类繁琐操作。
- 可维护性更强:UI 和逻辑绑定在组件内,改代码时不用全局搜索 DOM 操作。
- 性能优化自动化:React 的 Diff 算法会自动跳过不必要的 DOM 更新。
一句话总结"
声明式语法本质上就是把命令式语法中那些繁琐的底层操作(How),封装成更简洁的描述性表达(What),让开发者不用再手动处理每一步细节。
-
JSX 是语法糖,让写 React 元素更直观
-
声明式 是思想,关注描述最终结果而非具体步骤
-
React 使用 JSX 来实现声明式 UI 开发
-
你可以不用 JSX 但依然保持声明式,只是代码会更冗长
没有 JSX 的声明式"
即使不用 JSX,React 仍然可以是声明式的:
javascript
// 使用 React.createElement 的声明式
function Counter() {
const [count, setCount] = useState(0);
return React.createElement(
'div',
null,
React.createElement('p', null, 'Count: ', count),
React.createElement(
'button',
{ onClick: () => setCount(count + 1) },
'Increment'
)
);
}
8、react全家桶
- React Router、Redux、Ant Design、Webpack
8、类组件 vs 函数组件的区别?
- 生命周期、状态管理(类用 this.state,函数用 useState)、性能优化方式。
9、什么是 JSX?它的作用是什么?
- JSX 是 JavaScript 的语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构,JSX 本身无法被浏览器直接执行,需通过工具(如 Babel)编译为标准的 JavaScript 代码。编译后的代码(React 元素)由 React 库处理,最终生成页面内容。
10、受控组件(Controlled Component)和非受控组件(Uncontrolled Component)的区别?
- 受控组件:表单数据由 React 组件的 state 完全控制,输入元素的 value 属性直接绑定到 state,并通过 onChange 事件同步更新状态;
- 由 DOM 自身管理,通过 ref 直接操作 DOM 节点获取值。初始值可通过 defaultValue 或 defaultChecked 设置,但后续修改不依赖 React 状态。
11、为什么列表渲染需要 key?
- key 帮助 React 识别元素的变化,优化虚拟 DOM 的 Diff 算法效率。
12、state 和 props 的区别?
- state 是组件内部管理的可变数据,props 是父组件传递给子组件的只读数据。
13、如何实现父子组件通信?
- 父传子:通过 props;子传父:父组件通过 props 传递回调函数给子组件调用。
14、组件的生命周期方法?
React组件的生命周期可以分为三个阶段:挂载阶段、更新阶段和卸载阶段。
- 挂载阶段包括constructor、render、componentDidMount等方法,用于初始化组件、渲染到真实DOM和处理副作用。
- 更新阶段包括shouldComponentUpdate、render、componentDidUpdate等方法,用于控制组件的重新渲染和处理更新后的副作用。
- 卸载阶段包括componentWillUnmount方法,用于清理组件产生的副作用和资源。
15、如何实现兄弟组件或跨层级组件通信?
状态提升(Lifting State Up)、Context API、Redux 等状态管理库。
16、什么是状态提升(Lifting State Up)?
- 将多个组件需要共享的状态提升到它们的最近公共父组件中管理。
17、Context API 是什么?
-
Context 直接让数据穿透组件层级,实现跨组件共享。
javascriptconst UserContext = React.createContext(null); function App() { const [user, setUser] = useState({ name: 'Alice' }); return ( <UserContext.Provider value={user}> <Navbar /> </UserContext.Provider> ); } function Navbar() { const user = useContext(UserContext); return <div>欢迎回来, {user.name}</div>; }如果在某个组件中改变了值那么其他组件通过useContext 订阅的组件也会改变
javascriptimport React, { useState, useContext, useMemo } from 'react'; // 1. 创建 Context const ThemeContext = React.createContext({ mode: 'light', toggleTheme: () => {}, // 占位函数,避免未提供 Provider 时出错 }); // 2. 定义 App 组件(顶层 Provider) function App() { // 管理主题状态 const [theme, setTheme] = useState({ mode: 'light' }); // 定义切换主题的函数 const toggleTheme = () => { setTheme(prev => ({ mode: prev.mode === 'light' ? 'dark' : 'light', })); }; // 优化:使用 useMemo 避免每次渲染生成新对象 const themeValue = useMemo( () => ({ mode: theme.mode, toggleTheme, }), [theme.mode] // 仅在 theme.mode 变化时重新生成对象 ); // 提供 Context 数据 return ( <ThemeContext.Provider value={themeValue}> <Toolbar /> <PageContent /> </ThemeContext.Provider> ); } // 3. 子组件 Toolbar(展示当前主题,含切换按钮) function Toolbar() { // 消费 Context 数据 const { mode, toggleTheme } = useContext(ThemeContext); return ( <div style={{ background: mode === 'light' ? '#fff' : '#333', padding: 20 }}> <h3>Toolbar - 当前主题: {mode}</h3> <button onClick={toggleTheme}>切换主题</button> </div> ); } // 4. 子组件 PageContent(展示主题相关内容) function PageContent() { // 消费 Context 数据 const { mode } = useContext(ThemeContext); return ( <div style={{ background: mode === 'light' ? '#f0f0f0' : '#222', color: mode === 'light' ? '#000' : '#fff', padding: 20, marginTop: 10 }}> <h2>页面内容</h2> <p>当前主题模式: {mode}</p> </div> ); } export default App;
18、Redux
-
Redux 是一个用于管理 JavaScript 应用状态的可预测化状态容器,最初是为 React 设计的,但也可用于其他框架(如 Vue、Angular)或纯 JavaScript 应用。它的核心目标是让应用的状态管理更清晰、可维护且可追踪。
为什么需要 Redux?
- 在复杂的前端应用中,组件间的状态(如用户登录信息、页面数据、UI 状态等)可能需要在多个组件间共享或传递。传统的组件间通信(如 props 层层传递)会变得繁琐且难以维护。Redux 通过集中管理全局状态,解决了这类问题。
-
Redux 的三大核心原则
-
单一数据源 (Single Source of Truth)
- 整个应用的状态存储在一个唯一的 Store(对象树)中。
- 便于调试和跟踪状态变化。
-
状态是只读的 (State is Read-Only)
- 不能直接修改状态,必须通过 Action(一个描述发生了什么的对象)来触发变更。
-
使用纯函数修改状态 (Changes via Pure Functions)
- Reducer 是一个纯函数,接收旧状态和 Action,返回新状态。
- 保证状态变化的可预测性。
-
-
Redux 的核心概念
-
Store
- 存储全局状态的容器,通过 createStore(reducer) 创建。
- 提供 getState() 获取当前状态,dispatch(action) 触发状态变更,subscribe(listener) 监听变化。
-
Action
-
一个普通 JavaScript 对象,必须包含 type 字段描述操作类型,例如:
javascript{ type: 'ADD_TODO', text: 'Learn Redux' }
-
-
Reducer
-
根据 Action 的类型处理状态变更。例如:
javascriptfunction todoReducer(state = [], action) { switch (action.type) { case 'ADD_TODO': return [...state, { text: action.text }]; default: return state; } }
-
4 Middleware(可选)
- 扩展 Redux 的功能,例如处理异步操作(常用 redux-thunk 或 redux-saga)。
-
-
Redux 工作流程
- 触发 Action:用户操作或事件(如点击按钮)触发一个 Action。
- 派发 Action:调用 dispatch(action) 将 Action 发送到 Store。
- 执行 Reducer:Store 调用 Reducer,传入当前状态和 Action,生成新状态。
- 更新视图:Store 保存新状态,并通知所有订阅状态的组件重新渲染。
-
适用场景
- 组件需要共享大量状态。
- 需要跟踪状态变更历史(如实现撤销/重做)。
- 复杂的异步数据流管理。
我自己封装的redux
reducers.js
javascript// reducers.js const CLEAR_PAGE_PARAM = 'CLEAR_PAGE_PARAM'; const initialState = { userInfo: '', pageListQuery: {}, menuInfo: [], selectedPage: {}, tabsList: [], isLoading:false, pageParam: {}, openMenuKeys:[], permissions:{}, isDelPage:false, unorganizedMenuData:[], tabsAndPageChange:'' }; const permissionsReducer = (state = initialState.permissions, action) => { const handlers = { 'SET_PERMISSIONS': () => action.payload, 'RESET_PERMISSIONS': () => initialState.permissions, }; return handlers[action.type] ? handlers[action.type]() : state; }; const userReducer = (state = initialState.userInfo, action) => { const handlers = { 'SET_USER_INFO': () => action.payload, 'RESET_USER_INFO': () => initialState.userInfo, }; return handlers[action.type] ? handlers[action.type]() : state; }; const openMenuKeysReducer = (state = initialState.openMenuKeys, action) => { const handlers = { 'SET_OPEN_MENU_KEYS': () => action.payload, 'RESET_OPEN_MENU_KEYS': () => initialState.openMenuKeys, }; return handlers[action.type] ? handlers[action.type]() : state; }; const pageListQueryReducer = (state = initialState.pageListQuery, action) => { const handlers = { 'SET_PAGE_LIST_QUERY': () => action.payload, 'RESET_PAGE_LIST_QUERY': () => initialState.pageListQuery, }; return handlers[action.type] ? handlers[action.type]() : state; }; const menuInfoReducer = (state = initialState.menuInfo, action) => { const handlers = { 'SET_MENU_INFO': () => action.payload, 'RESET_MENU_INFO': () => initialState.menuInfo, }; return handlers[action.type] ? handlers[action.type]() : state; }; const selectedPageReducer = (state = initialState.selectedPage, action) => { const handlers = { 'SET_SELECTED_PAGE': () => action.payload, 'RESET_SELECTED_PAGE': () => initialState.selectedPage, }; return handlers[action.type] ? handlers[action.type]() : state; }; const tabsListReducer = (state = initialState.tabsList, action) => { const handlers = { 'SET_TABS_LIST': () => action.payload, 'RESET_TABS_LIST': () => initialState.tabsList, }; return handlers[action.type] ? handlers[action.type]() : state; }; const isLoadingReducer = (state = initialState.isLoading, action) => { switch (action.type) { case 'SHOW_LOADING': return true; case 'HIDE_LOADING': return false; default: return state; } }; const isDelPageReducer = (state = initialState.isDelPage, action) => { switch (action.type) { case 'TRUE_DEL_PAGE': return true; case 'FALSE_DEL_PAGE': return false; default: return state; } }; const pageParamReducer = (state = initialState.pageParam, action) => { const handlers = { 'SET_PAGE_PARAM': () => action.payload, 'RESET_PAGE_PARAM': () => initialState.pageParam, 'CLEAR_PAGE_PARAM': () => { const newState = { ...state }; delete newState[action.payload]; // 假设 payload 是要删除的键名 return newState; }, }; return handlers[action.type] ? handlers[action.type]() : state; }; const unorganizedMenuDataReducer = (state = initialState.unorganizedMenuData, action) => { const handlers = { 'SET_UNORGANIZED_MENU_INFO': () => action.payload, 'RESET_UNORGANIZED_MENU_INFO': () => initialState.unorganizedMenuData, }; return handlers[action.type] ? handlers[action.type]() : state; }; const tabsAndPageChangeReducer = (state = initialState.tabsAndPageChange, action) => { const handlers = { 'SET_TABS_PAGE_CHANGE': () => action.payload, 'RESET_TABS_PAGE_CHANGE': () => initialState.tabsAndPageChange, }; return handlers[action.type] ? handlers[action.type]() : state; }; function clearPageParam(paramKey) { return { type: CLEAR_PAGE_PARAM, payload: paramKey, }; } export { clearPageParam, isDelPageReducer, isLoadingReducer, menuInfoReducer, openMenuKeysReducer, pageListQueryReducer, pageParamReducer, permissionsReducer, selectedPageReducer, tabsAndPageChangeReducer, tabsListReducer, unorganizedMenuDataReducer, userReducer };在store.js引入reducers.js,做持久化配置。
javascript// store.js import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { persistReducer, persistStore } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import { isDelPageReducer, isLoadingReducer, menuInfoReducer, openMenuKeysReducer, pageListQueryReducer, pageParamReducer, permissionsReducer, selectedPageReducer, tabsAndPageChangeReducer, tabsListReducer, unorganizedMenuDataReducer, userReducer } from './reducers'; const rootReducer = combineReducers({ userInfo: userReducer, pageListQuery: pageListQueryReducer, menuInfo: menuInfoReducer, selectedPage: selectedPageReducer, tabsList: tabsListReducer, isLoading:isLoadingReducer, pageParam:pageParamReducer, openMenuKeys:openMenuKeysReducer, permissions:permissionsReducer, isDelPage:isDelPageReducer, unorganizedMenuData:unorganizedMenuDataReducer, tabsAndPageChange:tabsAndPageChangeReducer }); //持久化配置 const persistConfig = { key: 'root',// 存储的 key storage,// 存储方式 blacklist: ['isLoading'],// 不持久化 }; const persistedReducer = persistReducer(persistConfig, rootReducer); const store = configureStore({ reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false, }), }); const persistor = persistStore(store); export { persistor, store };- 在index.js中引入store,通过 包裹整个应用,react-redux 会利用 React 的 Context API 将 Redux 的 store 对象传递给所有子组件,任何子组件(如 及其内部组件)都可以通过 useSelector 或 connect 访问全局状态。
- 使用 redux-persist 库时, 会在应用启动前从本地存储(如 localStorage)加载已保存的状态,并合并到 Redux Store 中。
javascriptimport { ConfigProvider, message } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import updateLocale from 'dayjs/plugin/updateLocale'; import { default as React } from 'react'; import ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; import App from './App'; import './index.scss'; import reportWebVitals from './reportWebVitals'; import { persistor, store } from './store'; //antd 时间组件中文 dayjs.extend(updateLocale); dayjs.updateLocale('zh-cn'); // 全局配置 message message.config({ maxCount: 3,// 最大显示数, 超过限制时,最早的消息会被自动关闭 prefixCls: 'my-message', }); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <Provider store={store}> <PersistGate loading={null} persistor={persistor}> <ConfigProvider locale={zhCN}> <App /> </ConfigProvider> </PersistGate> </Provider> ); reportWebVitals();
19、什么是hook?
-
React Hooks 是 React 16.8 版本引入的一种特性,它允许开发者在函数组件中使用状态(state)、生命周期方法(lifecycle methods)等 React 特性,而无需编写 class 组件。Hooks 旨在简化组件逻辑、提高代码复用性,并解决 class 组件中常见的代码冗余和逻辑分散问题。
我自己的理解就是在React16.8版本将之前不好用的方法进行了替换成了新方法升级并且又用了语法糖的形式进行了封装让我们更好的调用和开发
-
React Hooks 与 16.8 之前版本定义变量的核心区别
-
状态变量的定义方式
-
16.8 之前(类组件):
必须通过 this.state 定义状态变量,且只能在类组件中使用。
javascriptclass Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; // 状态需集中定义在对象中:ml-citation{ref="7" data="citationList"} } } -
Hooks(函数组件):
使用 useState 直接定义单个状态变量,无需包裹在对象中。
javascriptfunction Example() { const [count, setCount] = useState(0); // 独立声明状态变量:ml-citation{ref="1,5" data="citationList"} }
-
-
变量更新的方式
-
16.8 之前:
javascriptthis.setState({ count: this.state.count + 1 }); // 需要手动合并对象:ml-citation{ref="7" data="citationList"} -
Hooks:
通过 useState 返回的 setter 函数直接更新变量,且更新是独立的。
javascriptsetCount(count + 1); // 直接赋值,无需合并对象:ml-citation{ref="5,7" data="citationList"}
-
-
-
总结
Hooks 通过函数式的方式,解决了类组件中状态分散、逻辑复用困难、闭包陷阱等问题,同时简化了代码结构并提升了可维护性
19、react 常用的hook有哪些
-
useState
-
用途:在函数定义状态。
-
示例:
javascriptconst [count, setCount] = useState(0);
-
-
useEffect
-
用途:处理副作用。
-
示例:
依赖数组设为空数组,组件挂载时执行(仅一次)
javascriptuseEffect(() => { init(); }, []);依赖数组不为空数组,监听数组内容变化,每次变化都会执行
javascriptuseEffect(() => { const updatedPageListQuery = { ...pageListQuery, banquetCumulativeIncentiveList }; dispatch({ type: 'SET_PAGE_LIST_QUERY', payload: updatedPageListQuery }); }, [banquetCumulativeIncentiveList]);
useEffect适合执行不需要在浏览器布局更新之前同步进行的操作,如数据请求、订阅事件等
-
-
useLayoutEffect
-
用途:与 useEffect 类似,但会在 DOM 更新后同步执行(适合需要直接操作 DOM 的场景)。
-
示例:
javascriptuseLayoutEffect(() => { measureDOMNode(); }, []);useLayoutEffect适合执行需要在浏览器布局更新之前同步进行的操作,如优化布局、控制动画、计算DOM尺寸等
-
-
useMemo
-
用途:缓存计算结果,避免重复计算。
-
示例:
javascriptconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);useMemo 接收两个参数一个函数和一个依赖项数组,当依赖项发生变化时,useMemo会重新执行该函数并返回新的计算结果,没有发生变化则返回上一次的计算结果,所以useMemo也有缓存机制,useMemo类似vue的computed。
-
-
useCallback
-
用途:缓存函数引用,避免子组件不必要的重新渲染。
-
示例1:
javascriptconst handleClick = useCallback(() => { doSomething(a, b); }, [a, b]);useCallback 接收两个参数:一个是要缓存的函数,另一个是依赖项数组。依赖项数组用于指定哪些变量发生变化时,缓存函数需要重新生成。当依赖项发生变化时,useCallback 会自动重新生成缓存函数。
- 示例2:
javascriptimport React, { useState, useCallback } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []);// 依赖项为空,函数仅在组件挂载时创建一次 const decrement = useCallback(() => { setCount((prevCount) => prevCount - 1); }, []);// 依赖项为空,函数仅在组件挂载时创建一次 return ( <> <button onClick={decrement}>-</button> <span>{count}</span> <button onClick={increment}>+</button> </> ); } export default Counter;- 缓存生效:由于依赖项为空,increment 函数在组件首次渲染时创建后,后续无论组件如何重新渲染,increment 都会返回同一个函数引用。
- 点击行为:每次点击按钮时,实际执行的都是 首次渲染时创建的缓存函数。
-
-
useRef
-
用途:若子组件是 原生 HTML 标签(如
<div>、<input>),父组件可直接通过 useRef 获取其 DOM 节点: -
示例:
javascript// 父组件 import { useRef } from 'react'; function Parent() { const childInputRef = useRef(null); const focusChildInput = () => { childInputRef.current.focus(); // ✅ 操作子组件 DOM }; return ( <div> <Child ref={childInputRef} /> <button onClick={focusChildInput}>聚焦子组件输入框</button> </div> ); } // 子组件(原生 input) const Child = React.forwardRef((props, ref) => { return <input ref={ref} />; // ✅ 转发 ref 到原生元素 }); -
用途:若子组件是 自定义组件(Class Component),父组件可通过 useRef 获取其实例,并调用其方法
-
示例:
javascript//父组件 const areaRef = useRef(null); areaRef.current.setAreaIds(vo.activitySchemeRuleVo.districts) <AreaSelectModal isDisabled={parentData.pageType === 'detail'} ref={areaRef} districtFlag={true} isRequired={true} /> //子组件 /* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable no-unreachable */ import React, { forwardRef, useImperativeHandle, useState } from 'react'; import './AreaSelectModal.scss'; const AreaSelectModal = forwardRef(({ districtFlag, isRequired, isDisabled }, ref) => { const [areaIds, setAreaIds] = useState([]); const getAreaIds = () => { return areaIds; }; //传递给父组件的 ref 对象 useImperativeHandle(ref, () => ({ getAreaIds })); return ( <div style={{ display: 'flex', alignItems: 'center', width: '100%' }}> <label className={`name ${isRequired ? 'is-required' : ''}`}>地区:</label> </div> ); }); export default AreaSelectModal;
-
20、useMemo 和 useCallback 的区别?
- useMemo 缓存计算结果,useCallback 缓存函数本身,用于性能优化。
21、为什么 Hooks 不能写在条件语句或循环中?
-
底层原理:Hooks 的链表管理机制
-
React 如何追踪 Hooks?
React 内部通过链表结构 管理 Hooks。每次组件渲染时,Hooks 必须按照完全相同的顺序被调用,React 才能将每个 Hook 与链表中的对应节点正确关联。
-
伪代码模拟实现
javascriptlet hooks = []; // 存储所有 Hook 状态的数组 let currentIndex = 0; // 当前 Hook 的索引 function renderComponent() { currentIndex = 0; // 每次渲染前重置索引 // 执行组件函数... } function useState(initialValue) { if (!hooks[currentIndex]) { hooks[currentIndex] = initialValue; // 初始化状态 } const state = hooks[currentIndex]; const setState = (newValue) => { hooks[currentIndex] = newValue; // 更新状态 }; currentIndex++; // 索引递增,指向下一个 Hook return [state, setState]; }- 关键点:Hooks 的调用顺序直接依赖 currentIndex 的递增逻辑。顺序或次数变化会导致索引错位。
-
-
问题场景:条件语句和循环的破坏性
-
条件语句破坏调用顺序
javascriptfunction Component({ condition }) { if (condition) { const [name, setName] = useState(""); // 第一次渲染调用 } const [count, setCount] = useState(0); // 第二次渲染时,若 condition 为 false,此 Hook 的索引会错位 }
- 结果:
- 当 condition 从 true 变为 false 时,count 会错误地读取 name 的状态。
- React 抛出错误:Rendered fewer hooks than expected。
-
循环导致调用次数不一致
javascriptfunction Component({ items }) { const values = []; for (let i = 0; i < items.length; i++) { const [value, setValue] = useState(items[i]); // 每次渲染 Hook 数量随 items 长度变化 values.push(value); } // 如果 items.length 变化,后续 Hooks 全部错乱 }- 结果:
- 循环次数变化时,React 无法正确匹配链表中的 Hook 节点。
- React 抛出错误:Hooks must be called in the exact same order。
- 结果:
-
-
解决方案:保证调用顺序和次数
-
始终在顶层调用 Hooks
-
规则:Hooks 必须在函数组件的顶层(不在任何条件、循环或嵌套函数中)调用。
-
正确示例:
javascriptfunction Component() { const [count, setCount] = useState(0); // ✅ 顶层调用 useEffect(() => {}); // ✅ 顶层调用 }
-
-
将条件逻辑移至 Hook 内部
-
场景:需要根据条件执行副作用或动态计算值。
-
正确示例:
javascript// 条件执行副作用 useEffect(() => { if (condition) { // 条件满足时执行操作 ✅ } }, [condition]); // 依赖数组控制条件 // 动态计算初始值 const initialCount = props.mode === "edit" ? 10 : 0; const [count, setCount] = useState(initialCount); // ✅ 提前计算参数
-
-
使用 useMemo/useCallback 封装动态逻辑
-
场景:依赖外部条件的复杂计算。
-
正确示例:
javascriptconst expensiveValue = useMemo(() => { return condition ? calculateA() : calculateB(); }, [condition]); // ✅ 缓存计算结果
-
-
-
总结与扩展
-
核心规则
- 调用顺序和次数必须严格一致:这是 React Hooks 设计的核心约束。
- 底层实现依赖链表结构:顺序错位会导致状态管理完全崩溃。
-
扩展思考
-
为什么 Class 组件没有此限制?
→ Class 组件通过 this.state 集中管理状态,而函数组件通过 Hooks 分散管理,依赖调用顺序的稳定性。
-
Hooks 的设计取舍
→ 牺牲部分灵活性(如动态条件),换取更好的逻辑复用和代码可读性。
-
-
最佳实践
- 静态调用:所有 Hooks 在组件顶层声明,不嵌套在任何逻辑块中。
- 动态参数:通过依赖数组(useEffect)或提前计算(useState)实现动态性。
- 最终结论:遵守 Hooks 的调用规则是避免不可预测 Bug 的核心前提,理解其底层机制有助于写出更健壮的 React 代码。
-
22、如何自定义hook?为什么要自定义hook普通函数不行吗?
-
如何自定义hook
-
将可复用的逻辑封装为函数,函数名以 use 开头,内部可调用其他 Hooks。
-
实例
javascriptconst useCounter = (initialValue = 0) => { const [count, setCount] = useState(initialValue); const increment = () => setCount(c => c + 1); return { count, increment }; };
-
-
为什么要自定义hook
- 在 React 中,普通函数和自定义 Hook 虽然都能封装逻辑,但它们的核心区别在于 能否访问 React 特性(如状态、生命周期) 和 对组件生命周期的绑定方式。
| 对比维度 | 普通函数 | 自定义 Hook |
|---|---|---|
| 访问 React 特性 | ❌ 无法使用 useState, useEffect 等 Hook | ✅ 可直接使用所有 React Hook |
| 状态管理 | ❌ 只能通过参数传递或闭包临时保存状态(易污染) | ✅ 通过 useState/useReducer 管理独立、隔离的状态 |
| 生命周期绑定 | ❌ 无法感知组件挂载/更新/销毁 | ✅ 可通过 useEffect 绑定组件生命周期 |
| 作用域隔离 | ❌ 多次调用共享同一作用域(可能互相干扰) | ✅ 每次调用 Hook 都有独立作用域(闭包机制) |
| 代码复用场景 | 纯计算、数据转换等无副作用逻辑 | 涉及状态、副作用、生命周期的组件逻辑 |
23、React 18 的新特性有哪些?
- 并发模式(Concurrent Mode)、自动批处理(Automatic Batching)、Suspense 支持服务端渲染等。
24、什么是错误边界(Error Boundary)?如何实现?
- 错误边界(Error Boundary)是 React 中用于捕获子组件树中的 JavaScript 错误,并显示备用 UI 的组件。它防止因局部组件错误导致整个应用崩溃,提升用户体验。
- 通过 static getDerivedStateFromError() 和 componentDidCatch() 捕获子组件的错误(仅类组件)。
25、React 服务端渲染(SSR)的原理是什么?
- React 服务端渲染(SSR)的原理是通过在服务器端将 React 组件渲染为 HTML 字符串,直接发送给客户端,以提升首屏加载速度和 SEO 优化。
26、React Fiber 的作用是什么?
- 新的协调算法,支持任务分片和中断/恢复,以实现并发模式下的高性能渲染。
27、解释 React 的协调(Reconciliation)过程。
- 通过 Diff 算法对比新旧虚拟 DOM,生成最小化的真实 DOM 更新。
28、如何实现一个防抖(Debounce)的搜索输入框?
- 使用 useEffect 和 setTimeout 延迟 API 请求,清理函数中取消定时器。
29、如何优化长列表的性能?
- 使用虚拟滚动库(如 react-window 或 react-virtualized),仅渲染可见区域内容。
30、如何处理组件间的复杂状态逻辑?
- 使用 Redux(单向数据流)或 Context API + useReducer 组合。
31、如何实现路由守卫(如登录验证)?为什么要实现路由守卫?
-
如何实现路由守卫(如登录验证)
在 React Router 中使用高阶组件或自定义
<Route>包装逻辑。方案 1:封装高阶组件拦截路由(适合简单场景)
javascript// src/guards/AuthGuard.jsx import { Navigate, useLocation } from 'react-router-dom'; const AuthGuard = ({ children }) => { const location = useLocation(); const isAuthenticated = localStorage.getItem('token'); // 实际项目建议用状态管理 if (!isAuthenticated) { // 记录来源路径,登录后自动跳回 return <Navigate to="/login" state={{ from: location }} replace />; } return children; }; // 使用示例 <Route path="/dashboard" element={ <AuthGuard> <Dashboard /> </AuthGuard> } />方案 2:全局路由布局拦截(推荐,统一管理)
javascript// src/layouts/AuthLayout.jsx import { Outlet, Navigate } from 'react-router-dom'; const AuthLayout = () => { const isAuthenticated = checkAuthToken(); // 自定义校验逻辑 return isAuthenticated ? <Outlet /> : <Navigate to="/login" replace />; }; // 路由配置 <Routes> <Route path="/login" element={<Login />} /> <Route element={<AuthLayout />}> {/* 所有子路由受保护 */} <Route path="/dashboard" element={<Dashboard />} /> <Route path="/profile" element={<Profile />} /> </Route> </Routes>方案 3:动态权限路由表(适合复杂权限系统)
javascript// src/routes.js const routes = [ { path: '/login', element: <Login />, isPublic: true, }, { path: '/admin', element: <AdminPanel />, requiredRole: 'admin', // 需要管理员权限 }, ]; // 动态渲染路由 const Router = () => { const { role } = useUser(); // 从全局状态获取用户角色 return ( <Routes> {routes.map((route) => { if (route.isPublic) { return <Route key={route.path} {...route} />; } // 权限校验 if (route.requiredRole && role !== route.requiredRole) { return <Route key={route.path} path={route.path} element={<Forbidden />} />; } return <Route key={route.path} {...route} />; })} </Routes> ); }; -
为什么要实现路由守卫?
| 场景 | 问题风险 | 路由守卫的作用 |
|---|---|---|
| 用户未登录访问 /profile | 敏感数据泄露 | 强制跳转到登录页 |
| 普通用户访问 /admin | 越权操作 | 根据角色权限拦截路由 |
| 页面直接输入URL 访问 | 绕过前端按钮逻辑,直接进入未授权页面 | 统一权限校验入口 |
| JWT Token 过期 | 发起无效请求,后端返回 401 | 自动检测 Token 有效性并刷新/跳转 |
32、React 的优缺点是什么?
- 优点:组件化、生态丰富、性能优化手段多;缺点:学习曲线陡峭、频繁的版本更新。
33、如何调试 React 应用?
- 浏览器调试工具、console.log、错误边界
34、如何在react中使用vue3封装的组件
方法:使用Web Components封装Vue组件
步骤1:将Vue组件转换为自定义元素
在Vue项目中,使用defineCustomElement将组件包装成自定义元素。
javascript
// MyVueComponent.vue
<script>
import { defineCustomElement } from 'vue'
export default {
name: 'MyVueElement',
props: {
initialCount: Number
},
data() {
return { count: this.initialCount }
},
methods: {
increment() {
this.count++
// 通过CustomEvent传递事件
this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }))
}
},
// 样式封装在Shadow DOM中
styles: [`
button { color: red; }
`]
}
// 转换为自定义元素
const MyVueElement = defineCustomElement(MyVueComponent)
customElements.define('my-vue-element', MyVueElement)
</script>
步骤2:构建Vue组件为独立JS文件
使用Vite或Vue CLI构建组件,生成可在浏览器中使用的JS文件。
示例Vite配置:
javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: 'src/MyVueComponent.vue',
formats: ['es']
}
}
})
运行构建命令:
vite build
将生成的JS文件(如dist/MyVueComponent.js)复制到React项目的public目录。
步骤3:在React中引入自定义元素
在React入口文件(如index.js)中导入JS文件:
javascript
// index.js
import './public/MyVueComponent.js'
步骤4:在React组件中使用自定义元素
通过ref监听事件并传递属性:
javascript
import React, { useRef, useEffect, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const elementRef = useRef(null)
useEffect(() => {
const element = elementRef.current
if (element) {
// 设置初始属性
element.initialCount = 0
// 监听事件
const handleEvent = (e) => setCount(e.detail)
element.addEventListener('count-changed', handleEvent)
return () => element.removeEventListener('count-changed', handleEvent)
}
}, [])
// 更新属性示例(可选)
useEffect(() => {
if (elementRef.current) {
elementRef.current.initialCount = count
}
}, [count])
return (
<div>
<my-vue-element ref={elementRef} />
<p>React中的计数:{count}</p>
</div>
)
}
export default App
35、react高阶组件(hoc)是什么?
高阶组件是一个函数,它接收一个组件,返回一个增强的新组件,用于逻辑复用。
-
核心特点
-
本质:函数(不是组件)
-
输入:被包装的组件
-
输出:增强后的新组件
-
目的:组件逻辑复用和横切关注点
-
-
基本结构
javascriptconst EnhancedComponent = hoc(WrappedComponent); -
常见应用场景
-
认证授权 - 检查用户权限
-
数据获取 - 统一数据加载逻辑
-
状态管理 - 共享状态逻辑
-
性能优化 - 条件渲染、缓存
-
样式主题 - 注入主题配置
-
-
简单示例
javascript
// 认证 HOC
const withAuth = (Component) => {
return (props) => {
const isLogin = localStorage.getItem('token');
return isLogin ? <Component {...props} /> : <div>请先登录</div>;
};
};
// 使用
const PrivatePage = withAuth(UserPage);
-
与 Hooks 对比
-
HOC:组件层面的复用,可能产生嵌套
-
Hooks:逻辑层面的复用,更灵活直观
-
现状:Hooks 更流行,但理解 HOC 对维护老代码很重要
-
-
面试回答要点
- "高阶组件是 React 的代码复用模式,通过函数包装组件来添加额外功能。虽然现在 Hooks 更常用,但理解 HOC 对于处理遗留代码和某些复杂场景仍有价值。"
我自己的理解hoc的用法,就是把多个组件中重复的逻辑抽出来,提成一个公共函数减少代码量,只不过我们平常传递的是参数hoc传的是组件,原理是一样的。