核心概念✅
什么是 JSX?
答案
JSX(JavaScript XML) 是 React 用来描述 UI 的模版语法,在构建阶段 React 通过(Babel) 等编译工具,转译为 React.createElement
调用。
由于在构建的时候需要把 JSX 转换为 React.createElement
,这也是为什么需要手动导入 React 的原因。在 React 17 之后,React 引入了新的 JSX 转换方式会转变如下代码,解决了手动导入 React 的问题。具体信息参考
介绍全新的 JSX 转换
// 由编译器引入(禁止自己引入!)
import { jsx as _jsx } from 'react/jsx-runtime'
function App () {
return _jsx('h1', { children: 'Hello world' })
}
JSX 的基本结构如下
function Counter() { const [count, setCount] = useState(0) return ( <div> <button onClick={() => setCount(count + 1)}>+</button> <strong style={{ padding: '0 5px' }}>{count}</strong> <button onClick={() => setCount(count - 1)}>-</button> </div> ); }
为了避免和 HTML 混淆,JSX 默认有如下规则。
- 大写字母开头的标签被视为 React 组件,小写字母开头的标签被视为 HTML 元素。
- 属性名采用小驼峰命名法,如
className
、onClick
等。 - 可以通过
{}
包裹 JavaScript 表达式,如变量、函数调用等。
延伸阅读
- Writing Markup with JSX 官方对 JSX 的介绍
- Introducing JSX React 官方文档对 JSX 的介绍
JSX 支持哪些功能?
答案
功能 | 描述 |
---|---|
{} 表达式插值 | 可以在 {} 中嵌入 JavaScript 表达 嵌入的内容会自动转移,避免 XSS 攻击 |
空渲染 | 可以返回 null 或 false 来表示不渲染任何内容,如 return null; ,在表达式中 {fals}、{null}、{undefined}、{ture} 均渲染为空 |
原生元素支持 | 可以采用 HTML 支持的元素, 使用小驼峰命名法绑定事件处理函数,如 <button onClick={handleClick}>Click me</button> |
样式处理 | 可以直接在 JSX 中使用内联样式对象,如 <div style={{ color: 'red', fontSize: '16px' }}>Hello</div> , 采用 className |
自定义组件 | 可以像 HTML 元素一样使用自定义组件,如 <MyComponent prop1={value} /> ,组件名必须大写开头 |
自定义属性 | 可以在组件上添加自定义属性,如 <MyComponent customProp="value" /> ,这些属性会作为 props 传递给组件,没有值的属性为布尔属性 |
属性扩展 | 可以使用对象展开语法传递多个属性,如 <MyComponent {...props} /> ,会将 props 对象中的所有属性传递给组件 |
动态组件 | 可以通过变量或表达式动态渲染组件,如 const Component = condition ? ComponentA : ComponentB; <Component /> |
插槽 | 默认 props.children ,可以通过 props 传递子元素,如 <Parent><Child /></Parent> ,在组件中使用 {props.children} 渲染 |
渲染属性 | 可以通过函数作为子元素传递渲染逻辑,如 <RenderProp render={() => <Child />} /> ,在组件中调用 props.render() 渲染内容 |
条件渲染 | 使用三元表达式或逻辑运算符进行条件渲染,如 condition ? <Component /> : null ,也可用 && 进行简化,如 {condition && <Component />} ,注意 condition 必须为布尔值,避免由于值为 0, '' 导致的非预期场景 |
列表渲染 | 使用 map 方法遍历数组生成列表,如 <ul>{items.map(item => <li key={item.id}>{item.name}</li>)}</ul> ,注意 key 来优化渲染 |
片段 | 使用 <>...</> 包裹多个元素,避免额外的 DOM 节点,如 <><Child1 /><Child2 /></> ,也支持直接返回数组 |
延伸阅读
- JSX JSX 的语法规范
- JSX In Depth
什么是 Virtual DOM, 它的作用是什么?
答案
Virtual DOM(VDOM)是一个对象,用来描述 React UI 的结构和状态。参考 源码 核心结构如下, 类型定义参考 ReactElementType
export type ReactElement = {
$$typeof: any, // 内部属性,标识组件的类型,例如 REACT_ELEMENT_TYPE 等
type: any, // 组件的类型,可以是字符串(如 'div')或函数/类组件
key: any, // 组件的唯一标识符,用于优化渲染
ref: any, // 组件的引用,用于访问 DOM 元素或组件实例
props: any, // 组件的属性,包含所有传递给组件的 props
// 以下是一些内部属性,只在 NODE_ENV === 'development' 时生效,仅做了解
_owner: any, // 内部只读属性,{ current: null | Fiber },记录负责创建此元素的组件关联对应的 Fiber 对象
_store: {validated: 0 | 1 | 2, ...}, // 备份 dom 的状态和其他额外调试信息
_self: self, // 标记组件的上下文
_source: source, // 标记文件名、行号等信息
_debugInfo: null | ReactDebugInfo, // 调试组件的信息,性能等
_debugStack: Error, // 调试栈信息
_debugTask: null | ConsoleTask, // 调试任务信息
};
所以可以直接返回一个对象来实现组件的渲染,例如
function App() { return ({ // 注意这个属性是必须的,react 只有识别到了内部类型才会触发渲染逻辑 $$typeof: Symbol.for('react.transitional.element'), type: 'h1', props: { children: [ { $$typeof: Symbol.for('react.transitional.element'), type: 'Button', props: { onClick: () => alert('Hello React'), children: `Hello React`, style: { color: '#7AC8E5', fontWeight: 'bold' } }, } ] }, }) }
VDOM 核心意义如下
- 渲染优化:VDOM 通过在内存中维护一个轻量级的 DOM 树,通过对比差异,增量更新,提升渲染效率
- 跨平台渲染:VDOM 使得 React 可以在不同的平台(如 Web、Native 等)上使用相同的渲染逻辑。通过将 VDOM 转换为特定平台的渲染指令,React 可以实现跨平台开发。
- 声明式编程:VDOM 使得开发者可以以声明式的方式描述 UI,而不必关注具体的渲染细节。这种方式提高了代码的可读性和可维护性。
一个典型的误区是认为用了 VDOM 一定比操作原生 DOM 快,VDOM 的核心优势是规避了直接操作 DOM 的心智负担。通过对比前后 VDOM 树的差异来进行增量更新,从而保持了一个较好的性能。 可以阅读 Virtual DOM is pure overhead 进一步理解
延伸阅读
Elements 和 Components 有什么区别?
答案
-
Element(元素) 是一个不可交互的对象,用来构建 React 应用的基础模块。支持树结构,一般由 Component 创建,也可以直接调用 createElement 方法生成。核心结构 ReactJSXElement 为
const ReactElement = {
type: 'h1', // 支持合法的 DOM 标签或者 React 组件
props: {}, // 传递给 Element 的属性 可以为 null,注意会排除 ref 和 key,children 会被放到 props 中实现树结构
key: null, // 可选 key 用来表示元素,作为 reconciliation 的依据
ref: null, // 可选 ref 用来引用元素
// ... 框架消费的属性
$$typeof: REACT_ELEMENT_TYPE, // 用于 React 内部标识 Element 类型
_owner: null, // 父节点
_store: {}, // 用于存储一些内部状态
_self: null, // 类组件用于存储 this 指向
_source: null // 用于存储源码位置,帮助调试
} -
Component(组件)是 React 中创建可复用 UI 的基本代码块,可以是函数组件或者类组件。函数组件和类组件的 render 方法返回 Element 元素。一般用 jsx 语法创建,例如
- 函数组件
- 类组件
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
延伸阅读
什么是 ReactNode 它和 Element 的区别是什么?
答案
参看 ReactNode 定义 ReactNode 可以理解为 React 中可以实际渲染的所有类型的节点,包括 ReactElement、Portal、文本节点、Fragment、Provider 和 Consumer 等。
export type ReactNode =
| React$Element<any>
| ReactPortal
| ReactText
| ReactFragment
| ReactProvider<any>
| ReactConsumer<any>;
而 ReactElement 是 ReactNode 的一个子集,表示一个具体的 React 元素。也就是我们常说的 Virtual DOM(VDOM),它是一个不可变的对象,描述了 UI 的结构和属性。结构参考 React$Element
type React$Element<
+ElementType: React$ElementType,
+P = React$ElementProps<ElementType>,
>: {
+type: ElementType,
+props: P,
+key: React$Key | null,
+ref: any,
};
什么是高阶组件 (Higher-Order Components HOC) ?
答案
高阶组件是一个函数,接受一个组件作为参数返回新的组件。const EnhancedComponent = higherOrderComponent(WrappedComponent);
可以采用此模式来对原始组件进行增强、拦截等各种操作。示例如下
消费高阶组件时需要注意如下问题
- 不要改变原始组件 高阶组件不应该修改传入的组件,而是使用组合的方式将其包裹起来。详见 不要修改原始组件
- 不要在 render 中使用高阶组件 高阶组件不应该在 render 方法中创建,render 需要保持为纯函数。详见 不要在 render 中使用高阶组件
- 注意 ref 和 static 丢失的问题 高阶组件可能会导致 ref 和 static 丢失,详见 ref 和 static 丢失
延伸阅读
- 高阶组件 官方文档高阶组件说明
state 和 props 区别是什么
答案
核心区别
state 和 props 是 React 组件数据流的两个核心概念,它们在数据来源、可变性和作用范围上有本质区别。
定义对比
- state: 组件内部的私有状态,由组件自身管理和更新
- props: 组件外部传入的属性,由父组件传递给子组件
主要区别
维度 | state | props | 说明 |
---|---|---|---|
数据来源 | 组件内部 | 父组件传递 | state 自管理,props 外部控制 |
可变性 | 可变 | 不可变 | 只有 state 可通过 setState 更新 |
作用范围 | 当前组件 | 父子组件间 | state 私有,props 用于通信 |
初始化 | constructor 或 useState | 父组件传入 | state 需要初始化,props 接收即可 |
代码示例
// 父组件
function Parent() {
const [count, setCount] = useState(0) // state: 组件内部状态
return (
<Child
title="计数器" // props: 传递给子组件
count={count} // props: 传递状态值
onIncrement={() => setCount(count + 1)} // props: 传递回调函数
/>
)
}
// 子组件
function Child({ title, count, onIncrement }) { // props: 接收父组件传递的数据
const [isVisible, setIsVisible] = useState(true) // state: 子组件内部状态
return (
<div>
<h3>{title}</h3> {/* 使用 props */}
<p>计数: {count}</p> {/* 使用 props */}
<button onClick={onIncrement}>增加</button> {/* 使用 props */}
{isVisible && <p>显示状态</p>} {/* 使用 state */}
<button onClick={() => setIsVisible(!isVisible)}>
切换显示 {/* 更新 state */}
</button>
</div>
)
}
使用场景
- state: 表单输入、开关状态、计数器等组件内部数据
- props: 组件配置、数据传递、事件回调等父子组件通信
常见误区
- 不要直接修改 state,必须通过 setState 或 useState 的更新函数
- 不要尝试修改 props,它们是只读的
- 避免将 props 直接赋值给 state,应该通过计算属性或 useEffect 处理
遵循"单向数据流"原则:数据从父组件流向子组件(props),状态变化通过回调函数向上传递。
延伸阅读
- React 官方文档:State 和生命周期
- React 官方文档:Props 传递
- React 单向数据流
- react basic react 的一些核心理念
什么是 Context 如何使用?
答案
Context 概念
Context 是 React 提供的一种跨组件传递数据的机制,解决了组件间的"props drilling"问题。它允许数据在组件树中向下传递,而不需要逐层手动传递 props。
核心API
- createContext: 创建 Context 对象,包含 Provider 和 Consumer 组件
- useContext: 在函数组件中访问 Context 值的 Hook
- Provider: 提供 Context 值的组件
- Consumer: 消费 Context 值的组件(较少使用)
基本使用
import React, { createContext, useContext, useState } from 'react'
// 1. 创建 Context
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })
// 2. 消费 Context
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
onClick={toggleTheme}
>
切换主题
</button>
)
}
// 3. 提供 Context 值
function App() {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<ThemedButton />
</ThemeContext.Provider>
)
}
使用场景
场景 | 说明 | 示例 |
---|---|---|
全局状态 | 应用级别的共享数据 | 主题、语言、用户信息 |
组件通信 | 跨多级组件传递数据 | 表单数据、配置项 |
功能模块 | 特定功能的上下文 | 购物车、权限管理 |
性能优化策略
- Context 分割: 将不同功能的数据分到独立的 Context 中
- useMemo 缓存值: 避免不必要的对象引用变化
- React.memo 包裹: 对消费组件进行浅比较优化
// 分割 Context
const ThemeContext = createContext()
const UserContext = createContext()
function App() {
// 使用 useMemo 缓存 Context 值
const themeValue = useMemo(() => ({ theme: 'dark' }), [])
const userValue = useMemo(() => ({ name: 'John' }), [])
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
<ComponentTree />
</UserContext.Provider>
</ThemeContext.Provider>
)
}
常见误区
- 过度使用 Context 替代简单的 props 传递
- 在 Context 中存储频繁变化的数据
- 不注意 Provider 值的引用稳定性
Context 适合共享相对稳定的数据,频繁变化的数据建议使用专门的状态管理库如 Redux 或 Zustand。
延伸阅读
什么是渲染属性(render props) ?
答案
渲染属性概念
渲染属性(Render Props)是 React 中一种基于组件组合的代码复用技术。它通过一个值为函数的 prop 来动态决定要渲染什么内容,这个函数返回一个 React 元素。
基本模式
渲染属性的核心思想是将渲染逻辑作为函数传递给组件,让组件调用该函数来确定渲染内容:
// 基本渲染属性组件
function DataProvider({ render, children }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchData().then(result => {
setData(result)
setLoading(false)
})
}, [])
// 调用 render 函数传递状态
return render({ data, loading })
}
// 使用渲染属性
function App() {
return (
<DataProvider
render={({ data, loading }) => (
loading ? <div>加载中...</div> : <div>数据: {data}</div>
)}
/>
)
}
常见实现方式
方式 | 语法 | 说明 |
---|---|---|
render prop | <Component render={fn} /> | 通过 render 属性传递函数 |
children as function | <Component>{fn}</Component> | 将函数作为 children 传递 |
自定义 prop | <Component customRender={fn} /> | 使用自定义属性名 |
代码示例
// 1. 使用 render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
document.addEventListener('mousemove', handleMove)
return () => document.removeEventListener('mousemove', handleMove)
}, [])
return render(position)
}
// 2. 使用 children as function
function Toggle({ children }) {
const [isOn, setIsOn] = useState(false)
return children({
isOn,
toggle: () => setIsOn(!isOn)
})
}
// 使用示例
function App() {
return (
<div>
{/* render prop 方式 */}
<MouseTracker
render={({ x, y }) => (
<p>鼠标位置: ({x}, {y})</p>
)}
/>
{/* children as function 方式 */}
<Toggle>
{({ isOn, toggle }) => (
<button onClick={toggle}>
{isOn ? '开' : '关'}
</button>
)}
</Toggle>
</div>
)
}
应用场景
- 状态逻辑复用: 在多个组件间共享状态管理逻辑
- 条件渲染: 根据不同条件渲染不同内容
- 数据获取: 封装数据请求逻辑,让使用者专注于渲染
- 动画组件: 提供动画状态,让使用者控制渲染样式
与其他模式对比
- vs HOC: 渲染属性更直观,避免了 HOC 的嵌套地狱和静态方法丢失问题
- vs Hooks: Hooks 出现后,大部分渲染属性的使用场景被 Hooks 替代
- vs 组件组合: 渲染属性更灵活,可以传递复杂的渲染逻辑
注意事项
- 函数组件中使用 useCallback 优化性能,避免每次渲染创建新函数
- 注意函数引用的稳定性,避免不必要的子组件重渲染
- 现代 React 推荐使用自定义 Hooks 替代渲染属性模式
虽然渲染属性是强大的模式,但在 React Hooks 出现后,大多数场景建议使用自定义 Hooks 来实现逻辑复用,代码更简洁易懂。
延伸阅读
什么是 React Hooks,它解决什么问题?
答案
React Hooks 是 React 16.8、React Native (0.59) 版本引入的一种新特性,它允许在函数组件中使用状态和钩子来执行副作用。hooks 解决如下的问题
- 通过自定义 hooks ,实现跨组件的状态逻辑复用,避免 HOC 和 render props 带来的嵌套复杂性和组件侵入式改动问题
- 通过自定义 hooks 组合实现复杂逻辑,避免类组件中逻辑分散到不同的生命周期中导致的难以维护的问题
- hooks 的函数式 API 为编译优化提供了更好的支持,避免了类组件中 this 的问题和复杂的生命周期管理
使用 Hooks 注意如下规则
- Hooks 只能出现在函数组件的顶层,不能在循环、条件或嵌套函数中调用,这样确保 hooks 的调用顺序在每次渲染中保持一致,避免状态错乱和副作用混乱, 进一步理解可以阅读 Why Do Hooks Rely on Call Order?
- Hooks 只能在 React 函数组件和自定义 hooks 中调用,不能在普通函数或类组件中使用
- Hooks 并不是完全兼容 Class 组件的生命周期,例如
getSnapshotBeforeUpdate, getDerivedStateFromError and componentDidCatch
详见 Do Hooks cover all use cases for classes? - 可以利用 eslint-plugin-react-hooks 校验 1 和 2 的规则,避免错误使用 Hooks
延伸阅读
- Introducing Hooks 官方介绍 React Hooks
- Rules of Hooks 旧版官方文档对 React Hooks 的使用规则说明
- Components and Hooks must be pure 新版官方文档对 React Hooks 的使用规则说明
- React calls Components and Hooks 新版官方文档组件中使用 Hooks 的规则说明
- 无意识设计-复盘React Hook的创造过程 介绍了 Hooks 的设计过程和思路
- hooks FAQ 旧版官方文档对 Hooks 的常见问题解答
- Why Do React Hooks Rely on Call Order? 解释为什么 Hooks 依赖调用顺序
什么是 Reconciliation?
答案
协调(Reconciliation) 是 React 内部比对 VDOM 树差异刷新 UI 的算法。
一些核心的比对策略如下
- 对于根节点类型不一样的元素直接替换为新节点,避免树对比的开销
- 对于同类型节点,比对属性,子节点进行递归对比
- 对于列表节点,通过
key
属性来优化对比,尽可能实现节点复用和最小化更新
经过上述优化整体的树更新从 O(n^3) 降低到 O(n),大大提升了性能。
什么是 合成事件(synthetic event) ,它有什么作用?
答案
合成事件(Synthetic Event) 是 React 对浏览器原生事件的封装,提供了一致的事件处理接口,解决了浏览器间事件模型不一致的问题。
React 会在调用 ReactDOM.createRoot(container) 的时候,内部执行 listenToAllSupportedEvents 方法进行委托,支持的事件列表详见 DOMEventNames。合成事件的对象结构详见 SyntheticBaseEvent 可以通过 nativeEvent
属性访问原生事件对象。
在 react16 及之前,由于存在合成事件, React 会采用事件池 进行管理,事件会在回调函数执行后被重置,导致无法在异步回调中访问事件对象。React 17 之后,合成事件不再使用事件池,事件对象会在回调函数执行后保留,可以直接访问。示例如下
function handleChange(e) {
setTimeout(() => {
// 无法访问到合成事件对象,因为在事件池中会被销毁
console.log(e.target.value); // Too late!
}, 100);
}
function handleChange(e) {
// 利用 persist() 方法保留合成事件对象
e.persist();
setTimeout(() => {
console.log(e.target.value); // 生效
}, 100);
}
延伸阅读
- SyntheticEvent 旧版官方文档对合成事件的说明
- React v17.0 Release Candidate: No New Features React 17 对事件委托和合成事件改进的说明
React Fiber 是什么,解决什么问题?
答案
React Fiber 是 React 16 引入的全新协调(Reconciliation)算法和架构,通过增量渲染,解决 React 15 中的性能瓶颈和渲染阻塞问题。
Fiber 本质是一个对象,和 React Element 一一对应,每一个 Fiber 本质就是一个渲染工作单元( Unit Work)。React 利用 Fiber 节点实现对渲染任务的调度。
延伸阅读
React 中纯函数的概念是什么?
答案
纯函数的在 React 语境下可以简单理解为幂等,及对于相同的输入总是返回相同的输出,并且没有副作用。在函数和类组件中一些典型的规则如下
组件类型 | 方法 | 规则 | 原因 |
---|---|---|---|
类组件 | constructor | 纯函数 | 确保初始化一致,避免修改状态 |
getDerivedStateFromProps | 纯函数 | 渲染前触发,规整 state 返回最终渲染的状态 | |
shouldComponentUpdate | 纯函数 | 判断是否需要更新,避免不必要的渲染 | |
render | 纯函数 | 返回 UI 结构,避免副作用 | |
setState(() => state) | 纯函数 | 更新组件状态,可能触发重新渲染 | |
componentDidMount | 非纯函数 | 可以执行副作用,如数据获取、订阅等 | |
componentDidUpdate | 非纯函数 | 可以执行副作用 | |
componentWillUnmount | 非纯函数 | 可以执行清理工作,如取消订阅、清除定时器等 | |
自定义方法 | 非纯函数 | 可以执行副作用,如事件处理、状态更新等 | |
setState(state, callBack) 的 callBack | 非纯函数 | 可以执行副作用 | |
函数组件 | 无 hooks | 纯函数 | 返回状态和更新函数,避免副作用 |
useState() | 纯函数 | 返回状态和更新函数,避免副作用 | |
[state,setState] = useState() 的 setState | 非纯函数 | 用来变更状态 |
此外注意 props
和 state
及组件中定义的变量的不可变性,禁止直接操作
延伸阅读
React 和 Vue 有什么区别?
答案
维度 | React | Vue |
---|---|---|
核心思想 | 函数式、声明式 UI,组件即函数 | 响应式、声明式 UI,数据驱动视图 |
视图层实现 | JSX 语法,全部用 JS 描述 UI | 单文件组件(SFC),模板+JS+CSS 分离 |
数据响应 | 手动 setState,单向数据流 | 响应式系统,数据变更自动驱动视图 |
组件通信 | props、context、状态提升 | props、$emit、provide/inject |
状态管理 | 官方无内置,常用 Redux/MobX/Recoil | 官方内置 Vuex/Pinia |
生命周期 | useEffect/useLayoutEffect 等 Hook | 生命周期钩子(onMounted、onUpdated) |
指令系统 | 无,全部用 JS 实现 | 丰富的模板指令(v-if、v-for 等) |
生态与扩展 | 依赖社区生态,灵活但需手动集成 | 官方工具链完善,开箱即用 |
性能优化 | 虚拟 DOM、Fiber、合成事件 | 虚拟 DOM、响应式依赖收集、编译优化 |
从使用角度看,React 更倾向于函数式编程,强调组件的纯函数特性和不可变数据流;而 Vue 则更接近于传统的 MVVM 模式,强调响应式数据绑定和模板语法。学习曲线上 Vue 更容易上手、心智负担更小。React 则更灵活但需要更多的配置和生态集成。开发者可以根据项目需求和团队技术栈选择合适的框架。
- react vs vue Vue 官方文档对 React 和 Vue 的对比
- React vs Vue 社区内核心开发对 react 和 Vue 的对比