Appearance
react
React 16、17、18 版本的对比
React16 于 2017 年发布,标志着 React 发展史上的一个转折点。它引入了多项关键特性,极大地提升了 React 的易用性和效率:
- 更佳的组件性能: React16 优化了组件的性能,减少了不必要的重新渲染,从而大幅提升了应用程序的整体运行速度。
- 错误边界: React16 推出了错误边界(Error Boundaries),使开发者能够在组件中捕获错误,避免它们传播到其他组件,提高了应用程序的稳定性。
- 懒加载: React16 支持懒加载(Lazy Loading),允许开发者在需要时动态加载组件,降低了初始加载时间,提升了应用程序的性能表现。
React17 于 2020 年发布,专注于优化用户体验。
- 并发模式: React17 引入了并发模式,使应用程序能够在不阻塞主线程的情况下更新 UI。这显著增强了应用程序的响应性,即使在处理复杂任务时也能保持流畅的交互。
- 稳定性提升: React17 进一步提高了组件的稳定性,减少了不必要的重新渲染,提升了应用程序的整体性能和用户体验。
React18 于 2022 年发布,是 React 迄今为止最重大的更新。它带来了令人振奋的新特性
- 自动批量更新: React18 引入了自动批量更新机制,可以自动将多个更新合并成一个批处理,大幅减少了不必要的重新渲染,极大提升了应用程序的性能。
- Suspense: React18 引入了 Suspense API,使开发者能够在加载数据时显示占位符,提升了应用程序的加载速度和用户体验。
- 流式渲染: React18 支持流式渲染,允许开发者将组件的更新拆分成更小的块,逐步应用到 DOM 中,大幅提升了应用程序的渲染性能。
React18 的自动批量更新是如何工作的?
自动批量更新机制在更新提交时会自动将多个更新合并成一个批处理,减少了不必要的重新渲染,从而显著提升应用程序的性能。
Suspense API 在哪些场景下很有用?
Suspense API 非常适合在加载数据时显示占位符的场景,可以提升应用程序的加载速度和用户体验。例如,在加载网络数据或大型数据集时,可以使用 Suspense API 来显示加载进度条或占位内容。
流式渲染是如何提高应用程序渲染性能的?
流式渲染将组件的更新拆分成更小的块,逐步应用到 DOM 中,避免了传统的批量渲染导致的卡顿和延迟。这极大提升了应用程序的渲染性能,即使在处理复杂组件时也能保持流畅的交互。
React18 有哪些更新?
- 并发模式
- 更新 render API
- 自动批处理
- Suspense 支持 SSR
- startTransition
- useTransition
- useDeferredValue
- useId
- 提供给第三方库的 Hook
组件通信方式有哪些
- props:适用于父到子通信
- 回调函数:子传父通信,调用父组件传到子组件的回调函数,在父组件中改变状态。
- 状态提升:兄弟组件通信,把状态提升到共同的父组件中,然后再分发。
- context API:跨层级通信,类似 Vue 的 provite/inject。
- ForwardRef + useImperativeHandle:父组件获取子组件中的数据。
- Redux、Mobx:状态管理库。
- 事件总线:通过 EventEmitter 或自定义发布-订阅模式实现(非 React 推荐方式,慎用)。
React JSX 与 Vue 模板的比较
JSX (React) 的优点
- 灵活性高:JSX 是 JavaScript 的语法扩展,可以充分利用 JavaScript 的全部功能
- 与 JavaScript 无缝集成:可以直接在标记中嵌入 JavaScript 表达式
- 组件化更彻底:组件就是普通的 JavaScript 函数/类
- 强大的工具支持:得益于 JavaScript 生态,有丰富的工具链支持
- 动态性更强:更适合需要高度动态交互的复杂应用
- 类型支持:与 TypeScript 集成非常好
JSX 的缺点
- 学习曲线较陡:需要同时掌握 JSX 和 JavaScript
- 模板与逻辑混合:可能导致代码可读性下降
- 需要额外工具:通常需要 Babel 进行转译
- 对设计者不友好:设计师可能更熟悉 HTML 而非 JSX
Vue 模板的优点
- 更接近标准 HTML:对设计师和初学者更友好
- 清晰的关注点分离:模板、脚本和样式分离
- 更简单的指令系统:如
v-if
,v-for
等,学习成本低 - 更好的性能优化:Vue 的编译器能对模板进行静态分析优化
- 更直观的绑定语法:双向绑定(
v-model
)更简洁 - 渐进式框架:可以逐步采用,适合各种规模的项目
Vue 模板的缺点
- 灵活性较低:受限于模板语法,某些复杂逻辑实现起来较麻烦
- 需要学习特定语法:如指令、修饰符等
- 与 JavaScript 生态融合度较低:相比 JSX 需要更多适配
- 动态性较弱:对于高度动态的 UI 结构不如 JSX 灵活
选择建议
- 选择 JSX:如果你需要最大灵活性、熟悉 JavaScript、项目复杂度高、团队有 React 经验
- 选择 Vue 模板:如果你重视开发体验一致性、需要更好维护性、项目相对简单、团队有 Vue 经验或设计背景成员
值得注意的是,Vue 也支持 JSX,而 React 也可以通过库实现类似模板的功能,但各自的主要范式仍然是上述比较的重点。
React 为什么要使用 JSX
可以从以下三个方面回答:
- 一句话解释 JSX。首先能一句话说清楚 JSX 到底是什么。
- 核心概念。JSX 用于解决什么问题?如何使用?
- 方案对比。与其他的方案对比,说明 React 选用 JSX 的必要性。
JSX 是一个 JavaScript 的语法扩展,结构类似 XML。
JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。
所以从这里可以看出,React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。
接下来与 JSX 以外的三种技术方案进行对比。
- 首先是模板,React 团队认为模板不应该是开发过程中的关注点,因为引入了模板语法、模板指令等概念,是一种不佳的实现方案。
- 其次是模板字符串,模板字符串编写的结构会造成多次内部嵌套,使整个结构变得复杂,并且优化代码提示也会变得困难重重。
- 最后是 JXON,同样因为代码提示困难的原因而被放弃。
所以 React 最后选用了 JSX,因为 JSX 与其设计思想贴合,不需要引入过多新的概念,对编辑器的代码提示也极为友好。
Babel 插件如何实现 JSX 到 JS 的编译
它的实现原理是这样的。Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement 的函数。如下代码所示:
js
module.exports = function (babel) {
var t = babel.types;
return {
name: "custom-jsx-plugin",
visitor: {
JSXElement(path) {
var openingElement = path.node.openingElement;
var tagName = openingElement.name.name;
var args = [];
args.push(t.stringLiteral(tagName));
var attribs = t.nullLiteral();
args.push(attribs);
var reactIdentifier = t.identifier("React"); //object
var createElementIdentifier = t.identifier("createElement");
var callee = t.memberExpression(reactIdentifier, createElementIdentifier)
var callExpression = t.callExpression(callee, args);
callExpression.arguments = callExpression.arguments.concat(path.node.children);
path.replaceWith(callExpression, path.node);
},
},
};
};
什么是函数组件和类组件?它们有什么区别?
特性 | 函数组件 | 类组件 |
---|---|---|
语法 | js 函数 | ES6 class 继承 React.Component |
状态管理 | useState Hook | 使用 this.state 和 this.setState |
生命周期 | 使用 useEffect Hook 模拟 | 内置生命周期方法 |
this 绑定 | 无 this 问题 | 有 this 指向问题 |
复用 | 更好复用 | 复用相对困难 |
代码量 | 相对简洁 | 相对复杂 |
性能 | 轻微优势(无实例化开销) | 轻微劣势 |
未来兼容性 | React 未来发展的主要方向 | 仍支持但不再新增特性 |
什么是纯组件?为什么要使用纯组件?
纯组件:
定义:纯组件是一种特殊的组件,它通过 React.memo(函数组件)或 PureComponent(类组件)来实现。纯组件会在 props 或 state 发生变化时进行浅比较,如果前后值相同,则跳过重新渲染。
优点:
- 性能优化:减少不必要的重新渲染,提高应用性能。
- 简化逻辑:开发者不需要手动实现 shouldComponentUpdate 方法来优化性能。
使用场景:
- 静态数据:组件的 props 和 state 不经常变化。
- 复杂组件:组件内部逻辑复杂,重新渲染开销大。
什么是受控组件和非受控组件
受控组件(Controlled Components)
受控组件是指那些其输入值由 React 的状态(state)控制的表单组件。每次用户输入时,都会触发一个事件处理器,更新组件的状态,从而更新表单的值。
jsx
import React, { useState } from 'react';
const ControlledForm = () => {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted with value:', value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
};
export default ControlledForm;
非受控组件(Uncontrolled Components)
非受控组件是指那些其输入值不由 React 状态管理的表单组件。相反,它们依赖于 DOM API 来获取表单的值。
- DOM API:通过 ref 获取表单的值。
- 初始值:可以通过 defaultValue 或 defaultChecked 属性设置初始值。
jsx
import React, { useRef } from 'react';
const UncontrolledForm = () => {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted with value:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">Submit</button>
</form>
);
};
export default UncontrolledForm;
什么是高阶组件(HOC)
它是一个函数,它接受一个组件并返回一个新的组件。相当于在传入组件的基础上扩展功能
HOC 用于复用组件逻辑,增强组件的功能。
js
import React from 'react';
// 高阶组件
const withLogging = (WrappedComponent) => {
return class extends React.Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} mounted`);
}
componentWillUnmount() {
console.log(`Component ${WrappedComponent.name} will unmount`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
};
// 被包裹的组件
const MyComponent = (props) => {
return <h1>Hello, {props.name}!</h1>;
};
// 使用 HOC
const MyComponentWithLogging = withLogging(MyComponent);
export default MyComponentWithLogging;
React 初始化加载组件会渲染两次的问题
这是因为开启了严格模式,React 在开发环境下,会刻意渲染两次组件,用来检测一些潜在问题,生产环境下只会渲染一次。 如果开发环境下,不想要渲染两次,只需要把 React.StrictMode
组件删除即可。
可检测的问题:
- 检测已废弃的 API
- 检测组件是否有副作用,因为 React 希望组件是一个纯函数。
tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
什么是 Fragment?它的作用是什么?
Fragment 是 React 提供的一个特殊组件,用于包裹多个子元素,而不会在 DOM 中添加额外的节点。这在需要返回多个根节点时非常有用。
jsx
import React from 'react';
const App = () => {
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</React.Fragment>
);
};
// 简写形式
const App = () => {
return (
<>
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</>
);
};
export default App;
如何避免 React 多次重复渲染导致的性能问题
- 函数组件利用 React.memo 方法, 在 props 相同时跳过重新渲染。 类组件通过继承 React.PureComponent 在 props 和 state 相同时跳过重新渲染。
- 使用 useCallback 缓存函数, useMemo 缓存计算结果,进行性能优化
简述 React 的生命周期
React 组件的生命周期可以分为三个阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。以下是各个生命周期方法及其作用:
一些最重要的生命周期方法是:
- componentWillMount**()** – 在渲染之前执行,在客户端和服务器端都会执行。
- componentDidMount**()** – 仅在第一次渲染后在客户端执行。
- componentWillReceiveProps**()** – 当从父类接收到 props 并且在调用另一个渲染器之前调用。
- shouldComponentUpdate**()** – 根据特定条件返回 true 或 false。如果你希望更新组件,请返回true 否则返回 false。默认情况下,它返回 false。
- componentWillUpdate**()** – 在 DOM 中进行渲染之前调用。
- componentDidUpdate**()** – 在渲染发生后立即调用。
- componentWillUnmount**()** – 从 DOM 卸载组件后调用。用于清理内存空间。
挂载阶段(Mounting)
- constructor(props)
- 作用:初始化组件的 state 和绑定事件处理器。
- 调用时机:组件实例被创建时。
- static getDerivedStateFromProps(props, state)
- 作用:在组件实例被创建和更新时调用,用于根据 props 更新 state。
- 调用时机:组件实例被创建和每次更新之前。
- render()
- 作用:返回组件的 JSX,描述 UI 的结构。
- 调用时机:组件实例被创建和每次更新时。
- componentDidMount()
- 作用:组件挂载完成后调用,通常用于发起网络请求、设置定时器等。
- 调用时机:组件首次渲染到 DOM 后。
更新阶段(Updating)
- static getDerivedStateFromProps(props, state)
- 作用:在组件更新时调用,用于根据新的 props 更新 state。
- 调用时机:组件接收到新 props 或 state 变化时。
- shouldComponentUpdate(nextProps, nextState)
- 作用:决定组件是否需要重新渲染,默认返回 true。
- 调用时机:组件接收到新 props 或 state 变化时。
- render()
- 作用:返回组件的 JSX,描述 UI 的结构。
- 调用时机:组件实例被创建和每次更新时。
- getSnapshotBeforeUpdate(prevProps, prevState)
- 作用:在组件更新前捕获一些信息,这些信息可以在 componentDidUpdate 中使用。
- 调用时机:组件更新前。
- componentDidUpdate(prevProps, prevState, snapshot)
- 作用:组件更新完成后调用,通常用于更新 DOM 或发起网络请求。
- 调用时机:组件更新后。
卸载阶段(Unmounting)
- componentWillUnmount()
- 作用:组件卸载前调用,通常用于清理工作,如取消网络请求、清除定时器等。
- 调用时机:组件从 DOM 中移除前。
什么是 Hooks?它们解决了什么问题?
Hooks 是 React 16.8 引入的新特性,允许你在不编写类组件的情况下使用状态和其他 React 特性。Hooks 使得函数组件可以拥有状态和生命周期方法,从而提高了代码的可读性和可维护性。
解决的问题
- 状态管理:在函数组件中管理状态,而不需要转换为类组件。
- 生命周期方法:在函数组件中使用生命周期方法,而不需要编写复杂的类组件。
- 逻辑复用:通过自定义 Hooks 复用组件逻辑,提高代码复用性。
- 代码简洁:使得函数组件的代码更加简洁和易读。
hooks 的使用限制
- 只能在顶层调用 Hooks,不能在循环、条件语句或嵌套函数中调用 Hooks。
- 只在 React 函数组件或自定义 Hook 中调用,不能在普通 JavaScript 函数中调用 Hooks。
- 自定义 Hook 必须以 "use" 开头。
- 不要在事件处理函数中调用 Hooks,跟第二点一个意思。
- 类组件不能使用 Hooks。
React 用链表来严格保证hooks的顺序。。如果在条件语句或循环中使用 Hook,会导致 Hook 的调用顺序不一致,从而引发 bug。导致当前 hooks 拿到的不是自己对应的 Hook 对象。
React 中常用的 Hooks
useState
useState(initialState): 用于在函数组件中添加状态。类似 Vue 的响应式变量。
jsx
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
为什么 useState 返回的是数组而不是对象?
因为解构赋值的原因
- 返回数组,可以对数组中的变量命名。
- 返回对象,那就必须和返回的值同名,不能重复使用了。
useRef
useRef(initialValue):创建一个可变的引用对象。其 .current 属性可以保存任何值。返回的对象在组件的整个生命周期内保持不变,组件每次重新渲染数据都不会重置,只有组件卸载才会被重置。
jsx
import React, { useRef } from 'react';
const FocusInput = () => {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
export default FocusInput;
useRef 和 useState 和普通 js 对象的区别
jsx
import { useRef, useEffect } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('你点击了 ' + ref.current + ' 次!');
}
return (
<button onClick={handleClick}>
点击我!
</button>
);
}
// 另一个例子 访问 DOM 元素
export default function FocusInput() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
useRef | useState |
---|---|
useRef(initialValue) 返回 | useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue]) |
更改时不会触发组件重新渲染 | 更改时触发组件重新渲染 |
可变 —— 你可以在渲染过程之外修改和更新 current 的值。 | “不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。 |
你不应在渲染期间读取(或写入) current 值。 | 你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照。 |
- useRef 返回的对象在组件的整个生命周期内保持不变,组件每次重新渲染数据都不会重置,只有组件卸载才会被重置。
- 普通对象依赖于它的声明方式和作用域。如果是在组件内声明的,组件每次重新渲染都会重置。
jsx
import { useState, useRef } from 'react'
function App() {
const [num, setNum] = useState(1)
const obj = useRef(1)
const obj2 = {num: 1}
function add() {
setNum(num + 1)
}
function addRef() {
obj.current = obj.current + 1
obj2.num = obj2.num + 1
}
function logs() {
console.log('num=>', num);
console.log('ref=>', obj.current);
console.log('普通对象=>', obj2.num);
}
return (
<>
<div>数字:{num}</div>
<button onClick={add}>增加State</button>
<button onClick={addRef}>增加Ref</button>
<button onClick={logs}>打印数据</button>
</>
)
}
组件销毁时会自动回收 ref 么?
在 React 中,组件销毁时并不会自动回收 ref。ref 是一个特殊的属性,用于引用组件实例或 DOM 元素,在组件销毁时,ref 引用的对象并不会自动被销毁,而是需要手动进行清理操作。
useEffect
useEffect(setup, dependencies?): 用于在函数组件中执行副作用操作,如数据获取、订阅或手动更改 DOM。一个组件里可以有多个 useEffect。
有两个参数:
- 参数1-函数: 传入一个函数A,组件渲染时会执行函数A,函数A可以再返回一个函数B(可选),当组件卸载时会执行该函数B。
- 参数2-依赖项:
- 传入一个变量数组:初始渲染以及数组里的依赖项变更渲染后 ,执行参数1的函数。
- 传入一个空数组:只在初始渲染后 ,执行参数1的函数。
- 不传参:会在组件的每次渲染后 ,执行参数1的函数。
jsx
import { useState, useEffect } from 'react';
function ChangeRoom() {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
const [message, setMessage] = useState('');
useEffect(() => {
// ...
}, [serverUrl, message]); // 如果 serverUrl 或 message 不同则会再次运行
useEffect(() => {
// ...
}, []);
useEffect(() => {
// ...
});
return <>
<div>
Your message: {message}
</div>
</>
}
useLayoutEffect
useLayoutEffect(setup, dependencies?):跟 useEffect 类似,但是它是在浏览器重新绘制屏幕之前执行。
useLayoutEffect 和 useEffect 的区别
useEffect 是在浏览器完成绘制(paint)后异步执行的
useLayoutEffect 是在 DOM 更新之后、浏览器绘制之前同步执行的
useEffect 不会阻塞浏览器绘制,用户可能会先看到未应用效果的 UI
useLayoutEffect 会阻塞浏览器绘制,确保用户看到的是应用了效果的最终 UI
适合使用 useEffect 的情况:
- 数据获取
- 事件订阅
- 大多数不需要同步 DOM 操作的副作用
适合使用 useLayoutEffect 的情况:
- 需要测量 DOM 元素(如获取元素尺寸、位置等)
- 需要根据 DOM 变化同步更新状态
- 需要执行动画或过渡效果,避免闪烁
useContext
useContext(SomeContext): 用于在函数组件中访问 React 的 Context。 用来组件之间的传参,祖孙组件传参,类似 Vue 的 provide/inject。
jsx
import React, { createContext, useContext } from 'react';
// 创建 Context
const ThemeContext = createContext();
// 提供 Context
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = React.useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// 使用 Context
const App = () => {
return (
<ThemeProvider>
<Header />
<Content />
</ThemeProvider>
);
};
const Header = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<header style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<button onClick={toggleTheme}>Toggle Theme</button>
</header>
);
};
const Content = () => {
const { theme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<h1>Welcome to the App</h1>
</div>
);
};
useReducer
useReducer(reducer, initialArg, init?): 用于在函数组件中管理复杂的状态逻辑。
- 定义 Reducer:传入一个 reducer 函数,该函数接收当前状态和一个 action,返回新的状态。
- 初始化状态:传入初始状态或一个初始化函数。
- 派发 Action:返回一个数组,第一个元素是当前状态,第二个元素是一个用于派发 action 的函数。
jsx
import React, { useReducer } from 'react';
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: 'increment' });
};
const decrement = () => {
dispatch({ type: 'decrement' });
};
return (
<div>
<h1>Count: {state.count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default Counter;
useMemo
useMemo(calculateValue, dependencies): 每次重新渲染的时候能够缓存计算的结果。传入一个计算函数和一个依赖数组,只有当依赖数组中的值发生变化时,才会重新计算。需要返回一个值。相当于 Vue 的 computed。
适用于复杂的计算、数据转换或大型数据结构的创建。
jsx
import React, { useMemo } from 'react';
const ExpensiveComponent = ({ count }) => {
const expensiveCalculation = useMemo(() => {
console.log('Performing expensive calculation');
return count * 1000;
}, [count]);
return <div>Expensive Calculation Result: {expensiveCalculation}</div>;
};
export default ExpensiveComponent;
useCallback
useCallback(fn, dependencies):缓存传入的函数。
因为组件每次重新渲染,函数都相当于是一个新函数,这样就没法配合 memo 方法进行性能优化。
默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件。memo 允许组件在 props 没有改变的情况下跳过重新渲染。
所以如果子组件传了一个函数给 memo 包裹的子组件,要使 memo 生效,必须配合 useCallback 使用。
jsx
import React, { useCallback, memo } from 'react';
/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */
const ShippingForm = memo(function Shipping({ onSubmit }) {
// ...
});
function ProductPage({ productId, referrer, theme }) {
// 在多次渲染中缓存函数
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
export default ProductPage;
useImperativeHandle
useImperativeHandle(ref, createHandle, dependencies?): 用于子组件自定义暴露给父组件的数据。类似于 Vue3 的 defineExpose
jsx
import { Button } from 'antd';
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyButton = forwardRef((props, ref) => {
const buttonRef = useRef(null);
const { children, ...otherProps } = props;
useImperativeHandle(ref, () => ({
click: () => {
if (buttonRef.current) {
console.log(buttonRef.current.offsetTop);
}
},
fsdfs: 123213
}));
return <Button ref={buttonRef} {...otherProps}>{children}</Button>;
});
function PageEntry() {
const postRef = useRef(null);
function handleClick() {
postRef.current.click();
console.log(postRef.current);
}
return <>
<Button type="primary" size="large" onClick={handleClick}>打印数据</Button>
< br />
<MyButton ref={postRef} type="primary" size="large">自定义组件</MyButton>
</>
}
export default PageEntry;
React 事件机制和原生 DOM 事件流有什么区别
react 中的事件是绑定到 document 上面的,
而原生的事件是绑定到 dom 上面的,
因此相对绑定的地方来说,dom 上的事件要优先于 document 上的事件执行
SetState 是同步还是异步的,setState 做了什么
在 React 中,setState()函数通常被认为是异步的,这意味着调用 setState()时不会立刻改变 react 组件中 state 的值,setState 通过触发一次组件的更新来引发重汇,多次 setState 函数调用产生的效果会合并
调用 setState 时,React 会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。这将启动一个称为和解(reconciliation)的过程。和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新 UI。 为此,React 将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。
一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较
如何实现动态路由权限和按钮权限
动态路由
- 通过接口获取菜单路由数据,然后把数据处理成对应的格式。若需添加嵌套路由,需确保路径完整
js
{
path: "/admin",
children: [
{ path: "/admin/dashboard", element: <Dashboard /> }, // 子路由路径需完整
],
}
- 使用 useRoutes 的返回值替换 Route 组件
jsx
// router.tsx
function CustomRoutes() {
// 接口返回的数据
const routes = [
{
path: "/admin",
children: [
{ path: "/admin/dashboard", element: <Dashboard /> }, // 子路由路径需完整
],
}
];
const element = useRoutes([
...routes,
{ path: "*", element: <NotFound /> }, // 404 处理
]);
return <>{element}</>
}
// App.tsx
function App() {
return (
<div>
<CustomRoutes />
</div>
)
}
- 利用 React.lazy 方法和 Suspense 组件实现路由懒加载
js
const About = lazy(() => import("./About"));
{
path: "/about",
element: (
<Suspense fallback={<div>Loading...</div>}>
<About />
</Suspense>
),
}
动态路由刷新页面后,匹配不到路由的问题
一般是接口还没返回菜单数据,导致动态路由没创建成功
可以使用 react-Router v6库中提供的 useLocation 获取当前的url地址,动态路由创建完成后再使用 useNavigate 进行路由的跳转
jsx
import { useNavigate, useLocation } from 'react-router-dom'
const navigate = useNavigate()
const { pathname } = useLocation()
useEffect(() => {
const { id } = localcache.getCacha('userinfo')
getMenus(id).then((res) => {
navigate(pathname)
})
}, [])
按钮权限
Redux 的主要特点是什么?
Redux 是一个基于 Flux 架构的 JavaScript 状态管理库。
- 单一数据源:整个应用的状态存储在一个单一的 store 中,确保了状态的一致性。
- 状态不可变:状态是不可变的,每次状态变化时,都会生成一个新的状态对象。
- 纯函数:通过纯函数(reducer)来处理状态变化,使得状态变化可预测。
- 中间件支持:支持中间件,可以扩展 Redux 的功能,如异步操作、日志记录等。
- 开发者工具:提供了强大的开发者工具,可以调试、回溯和重放状态变化。
不可变状态和可变状态的区别
特性 | 不可变状态 | 可变状态 |
---|---|---|
修改方式 | 创建新副本 | 直接修改原对象 |
引用比较 | 每次修改产生新引用 | 引用保持不变 |
检测变化 | 通过引用比较 (===) | 需要深度比较或观察机制 |
内存使用 | 更高 (保留旧版本) | 更低 |
典型库 | Redux, Immer | MobX, Vuex |
调试 | 状态历史清晰 | 需要特殊工具跟踪变化 |
如何在 React 中使用 Redux?
- 安装
npm install redux react-redux @reduxjs/toolki
- 创建 Reducer,必须是一个纯函数。
js
// dataSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
editingItem: null
};
const dataSlice = createSlice({
name: 'data',
initialState,
reducers: {
addItem: (state, action) => {
state.items.push(action.payload);
},
updateItem: (state, action) => {
const index = state.items.findIndex(item => item.id === action.payload.id);
if (index !== -1) {
state.items[index] = action.payload;
}
},
setEditingItem: (state, action) => {
state.editingItem = action.payload;
},
deleteItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});
export const { addItem, updateItem, setEditingItem, deleteItem } = dataSlice.actions;
export default dataSlice.reducer;
- 创建 store
js
// store.js
import { configureStore } from '@reduxjs/toolkit';
import dataReducer from './dataSlice';
export const store = configureStore({
reducer: {
data: dataReducer
}
});
- 在 App.ts 中引入 store
js
// App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
- 在组件中使用 Redux
jsx
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addItem, updateItem, setEditingItem, deleteItem } from './dataSlice';
const DataComponent = () => {
const [formData, setFormData] = useState({ id: '', name: '', description: '' });
const items = useSelector(state => state.data.items);
const editingItem = useSelector(state => state.data.editingItem);
const dispatch = useDispatch();
// 处理表单输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
// 处理表单提交
const handleSubmit = (e) => {
e.preventDefault();
if (editingItem) {
// 更新现有项目
dispatch(updateItem({ ...formData, id: editingItem.id }));
} else {
// 添加新项目
dispatch(addItem({ ...formData, id: Date.now() }));
}
// 重置表单
setFormData({ id: '', name: '', description: '' });
dispatch(setEditingItem(null));
};
// 编辑项目
const handleEdit = (item) => {
setFormData(item);
dispatch(setEditingItem(item));
};
// 删除项目
const handleDelete = (id) => {
dispatch(deleteItem(id));
};
return (
<div>
<h2>{editingItem ? 'Edit Item' : 'Add New Item'}</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div>
<label>Description:</label>
<input
type="text"
name="description"
value={formData.description}
onChange={handleInputChange}
/>
</div>
<button type="submit">{editingItem ? 'Update' : 'Add'}</button>
{editingItem && (
<button type="button" onClick={() => {
setFormData({ id: '', name: '', description: '' });
dispatch(setEditingItem(null));
}}>
Cancel
</button>
)}
</form>
<h2>Items List</h2>
<ul>
{items.map(item => (
<li key={item.id}>
<strong>{item.name}</strong>: {item.description}
<button onClick={() => handleEdit(item)}>Edit</button>
<button onClick={() => handleDelete(item.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default DataComponent;
Redux 的工作流程
- 单向数据流
当用户与视图交互时:
View → Action → Reducer → Store → View (更新)
- 触发 Action:组件调用 dispatch(action)
- Redux Store 处理:store 调用 reducer,传入当前 state 和 action
- Reducer 计算新状态:reducer 根据 action 类型计算新状态
- Store 保存新状态:store 保存 reducer 返回的新状态
- 通知订阅者:store 调用所有订阅的监听器
- 更新视图:连接 Redux 的组件检查 props 是否需要更新
Redux 如何处理异步
- 使用 redux-thunk,适合简单场景
Redux Thunk 是一个中间件,允许你在 action 创建函数中返回一个函数而不是一个 action 对象。这个返回的函数可以包含异步逻辑,并在适当的时候 dispatch 一个或多个 action。
js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const initialState = { data: null };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'FETCH_DATA_SUCCESS':
return { ...state, data: action.payload };
default:
return state;
}
};
const fetchData = () => async (dispatch) => {
const response = await fetch('/api/data');
const data = await response.json();
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
};
const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(fetchData());
- 使用 redux-saga (适合复杂异步场景)
Redux Saga 是一个用于管理应用副作用(如异步操作)的库,使用 Generator 函数来处理异步逻辑。
js
import { takeLatest, call, put } from 'redux-saga/effects';
import { fetchDataSuccess, fetchDataFailure } from './actions';
function* fetchDataSaga() {
try {
const response = yield call(fetch, '/api/data');
const data = yield response.json();
yield put(fetchDataSuccess(data));
} catch (error) {
yield put(fetchDataFailure(error));
}
}
function* watchFetchData() {
yield takeLatest('FETCH_DATA_REQUEST', fetchDataSaga);
}
export default function* rootSaga() {
yield watchFetchData();
}
- 使用 RTK Query (Redux Toolkit 的现代方案),专门处理数据获取和缓存。
js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: builder => ({
getUser: builder.query({
query: userId => `/users/${userId}`
})
})
});
export const { useGetUserQuery } = apiSlice;
function UserComponent({ userId }) {
const { data, error, isLoading } = useGetUserQuery(userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return <div>{data.name}</div>;
}
- 不使用第三方插件(不推荐)
js
const fetchUser = (userId) => {
return async (dispatch) => {
try {
dispatch({ type: 'FETCH_USER_START' });
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_USER_ERROR', payload: error });
}
};
};
Redux 与 Vuex 的区别
相同点
- state 共享数据
- 流程一致:定义全局state,触发,修改state
- 原理相似,通过全局注入store。
不同点
- Redux 使用的是不可变状态,而 Vuex 的数据是可变状态。
- Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的。
- Redux 同步和异步都使用 dispatch ,vuex 触发方式有两种 commit 同步和 dispatch 异步。
Mobx 的使用
在 Mobx6 之前,可以使用 @observable 这样的装饰器。在 Mobx 6 中,为了与标准 JavaScript 的最大兼容性,我们在默认情况下放弃了装饰器。但如果启用它们,它们仍然可以使用。
- 安装
npm install mobx mobx-react-lite
- 创建一个管理数据的 Store
js
// src/stores/ItemStore.js
import { makeAutoObservable } from "mobx";
class ItemStore {
items = [];
editingItem = null;
constructor() {
makeAutoObservable(this);
}
addItem(item) {
this.items.push({ ...item, id: Date.now() });
}
updateItem(updatedItem) {
const index = this.items.findIndex(item => item.id === updatedItem.id);
if (index !== -1) {
this.items[index] = updatedItem;
}
this.editingItem = null;
}
setEditingItem(item) {
this.editingItem = item;
}
deleteItem(id) {
this.items = this.items.filter(item => item.id !== id);
}
}
const itemStore = new ItemStore();
export default itemStore;
- 在组件中引用
jsx
// src/components/ItemList.jsx
import { observer } from "mobx-react-lite";
import { useState } from "react";
import itemStore from "../stores/ItemStore";
const ItemList = observer(() => {
const [formData, setFormData] = useState({
name: "",
description: ""
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
if (itemStore.editingItem) {
itemStore.updateItem({
...itemStore.editingItem,
...formData
});
} else {
itemStore.addItem(formData);
}
setFormData({ name: "", description: "" });
};
const handleEdit = (item) => {
itemStore.setEditingItem(item);
setFormData({
name: item.name,
description: item.description
});
};
return (
<div>
<h2>{itemStore.editingItem ? "编辑项目" : "添加新项目"}</h2>
<form onSubmit={handleSubmit}>
<div>
<label>
名称:
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</label>
</div>
<div>
<label>
描述:
<input
type="text"
name="description"
value={formData.description}
onChange={handleInputChange}
/>
</label>
</div>
<button type="submit">
{itemStore.editingItem ? "更新" : "添加"}
</button>
{itemStore.editingItem && (
<button type="button" onClick={() => {
itemStore.setEditingItem(null);
setFormData({ name: "", description: "" });
}}>
取消
</button>
)}
</form>
<h2>项目列表</h2>
<ul>
{itemStore.items.map(item => (
<li key={item.id}>
<strong>{item.name}</strong>: {item.description}
<button onClick={() => handleEdit(item)}>编辑</button>
<button onClick={() => itemStore.deleteItem(item.id)}>删除</button>
</li>
))}
</ul>
</div>
);
});
export default ItemList;
Mobx 和 Redux 的区别
特性 | Redux | Mobx |
---|---|---|
核心理念 | 单一不可变状态树 + 纯函数更新 | 自动追踪(观察者模式实现)和响应状态变化 |
数据流向 | 严格的单向数据流:View -> Action -> Reducer -> Store -> View | 多向数据流 |
可变性 | 不可变状态 | 可变状态 |
store 管理 | 整个应用只有一个 store | 可以创建多个 store |
更新策略 | 会先检查所有使用了 store 的组件去判断是否更新,不是只检查使用了要修改的数据的组件 | 自动优化组件更新,只更新真正依赖变化的组件 |
各自适用场景
推荐使用 MobX 的情况
- 需要快速开发原型或中小型应用
- 团队更熟悉面向对象编程
- 应用中有大量细粒度的状态更新
- 希望减少样板代码
推荐使用 Redux 的情况
- 大型复杂应用需要严格的状态管理
- 需要明确的状态变更历史和时间旅行调试
- 团队已经熟悉函数式编程概念
- 需要中间件处理复杂副作用(如异步流)
如何理解 React Fiber 架构?
React Fiber 是 React 16 中引入的核心架构重构,旨在解决 React 在大型应用中的性能问题(尤其是动画、手势等高频更新场景),并支持增量渲染、任务优先级调度等高级特性。
Fiber 的本质
Fiber 是 React 的虚拟堆栈帧,是对旧版 Stack Reconciler(基于递归调用栈)的重构。它的核心思想是将渲染工作拆解为多个可中断、可恢复的小任务单元(即 Fiber 节点),每个单元对应一个组件或 DOM 节点。
- 旧版问题:递归遍历组件树时,一旦开始无法中断,可能导致主线程长时间阻塞(如动画卡顿)。
- Fiber 方案:将递归改为链表结构的循环遍历,通过浏览器 API(如 requestIdleCallback)实现任务分片。
Fiber 的核心目标
- 可中断渲染:将渲染过程分解为增量任务,避免阻塞主线程。
- 优先级调度:高优先级任务(如用户输入)可打断低优先级任务(如后台数据加载)。
- 并发模式(Concurrent Mode)的基础:支持异步渲染、Suspense 等特性。
Fiber 的数据结构
每个 Fiber 节点是一个 JavaScript 对象,包含以下关键属性:
js
{
type: 'div' | FunctionComponent | ClassComponent, // 组件类型
key: string, // 唯一标识
stateNode: HTMLElement | Instance, // 对应的真实 DOM 或实例
return: Fiber, // 父节点
child: Fiber, // 第一个子节点
sibling: Fiber, // 下一个兄弟节点
alternate: Fiber, // 指向当前/上一次渲染的 Fiber(双缓存)
effectTag: 'Placement' | 'Update' | 'Deletion', // 标记 DOM 操作类型
memoizedState: any, // Hook 或 Class 组件的状态
// ... 其他调度相关属性(expirationTime, lanes 等)
}
Fiber 的工作流程
React 的渲染分为两个阶段:
a. 协调阶段(Reconciliation)
- 目的:找出需要更新的组件(Diff 算法)。
- 过程:
- 从根节点开始,深度优先遍历生成 Fiber 树(称为 workInProgress 树)。
- 对比新旧 Fiber 树,标记变更(如 effectTag)。
- 可中断:浏览器空闲时继续未完成的任务。
b. 提交阶段(Commit)
- 目的:将变更一次性应用到 DOM。
- 过程:
- 遍历所有标记了 effectTag 的 Fiber 节点。
- 执行 DOM 操作(创建/更新/删除)。
- 不可中断:必须同步完成,避免 UI 不一致。
关键机制
a. 双缓存技术
- 同时存在两棵 Fiber 树:
- current:当前屏幕上显示的版本。
- workInProgress:正在构建的新版本。
- 完成后交换指针,避免内存分配。
b. 优先级调度
- 通过 expirationTime 或 lanes 标记任务优先级。
- 高优先级任务可抢占低优先级任务。
c. 副作用列表(Effect List)
- 提交阶段仅处理有副作用的节点,减少遍历成本。