主要内容为react原理hooks,以及部分react 和 vue的对比,比较重要的内容会有写出代码,You can keep watching if you need to ,OR NO 出门右转,内容持续更新中,后续会收集或遇到好的问题...
react 中的不同优先级
javascript
// 不同的优先级级别
const priorityLevels = {
ImmediatePriority: 1, // 最高优先级
UserBlockingPriority: 2, // 用户阻塞级
NormalPriority: 3, // 普通优先级
LowPriority: 4, // 低优先级
IdlePriority: 5 // 空闲优先级
};
// React 源码中的优先级定义
export const NoPriority = 0; // 无优先级
export const ImmediatePriority = 1; // 立即执行
export const UserBlockingPriority = 2; // 用户阻塞
export const NormalPriority = 3; // 正常
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲
类组件中 setState 发生了什么 是同步的还是异步的
- setState 获取到state
- enqueueState 将state 添加到更新队列中
- requestUpdateLane 获取当前执行更新的state 的优先级
- scheduleUpdateOnFiber 调度更新,区分更新模式,是同步更新(微任务异步 performSyncWorkOnRoot),还是并发更新(宏任务异步 performConcurrentWorkOnRoot postMessage实现的)
是异步的
如果是同步的话每次更新都会触发组件重新渲染浪费性能,异步还可以批量处理相同相似的更新
javascript
// 1. setState 获取 state
function setState(newState) {
// 获取当前组件实例的 state
const instance = this;
const currentState = instance.state;
}
// 2. enqueueState 将更新加入队列
function enqueueState(instance, newState) {
// 创建更新对象
const update = {
payload: newState,
next: null
};
// 将更新添加到更新队列
const updateQueue = instance.updateQueue || (instance.updateQueue = []);
updateQueue.push(update);
}
// 3. requestUpdateLane 确定优先级
function requestUpdateLane() {
// 根据不同情况返回不同的优先级
// 例如:用户交互 > 网络请求 > 普通更新
if (isUserInteraction) {
return UserBlockingPriority;
}
return NormalPriority;
}
// 4. scheduleUpdateOnFiber 调度更新
function scheduleUpdateOnFiber(fiber, lane) {
if (lane === SyncLane) {
// 同步更新:微任务
queueMicrotask(() => {
performSyncWorkOnRoot();
});
} else {
// 并发更新:宏任务
window.postMessage('update', '*');
}
}
// 同步更新示例
function performSyncWorkOnRoot() {
// 立即执行更新
}
// 并发更新示例
window.addEventListener('message', () => {
performConcurrentWorkOnRoot();
});
什么是合成事件
React 的合成事件(SyntheticEvent)是 React 对原生 DOM 事件的封装。
// 执行顺序:
// 1. React 捕获阶段 (handleCapture)
// 2. 原生捕获阶段
// 3. 目标阶段
// 4. 原生冒泡阶段** --> 为什么有阻止冒泡了? 因为addEventListener第二个参数默认为false冒泡阶段 ,**冒泡阶段从内到外会触发父元素点击事件
// 5. React 冒泡阶段 (handleClick -> handleBubble)
javascript
interface EventExamples {
// 鼠标事件
onClick: (e: React.MouseEvent) => void;
onMouseEnter: (e: React.MouseEvent) => void;
// 键盘事件
onKeyPress: (e: React.KeyboardEvent) => void;
// 表单事件
onChange: (e: React.ChangeEvent) => void;
onSubmit: (e: React.FormEvent) => void;
// 焦点事件
onFocus: (e: React.FocusEvent) => void;
}
所有事件都委托到 root 节点统一处理(React 17 之后)
原来的是委托到document元素上,事件冒泡需要冒泡到document 元素上更长了
javascript
// React 16
// 事件冒泡到 document,路径更长
button -> div -> body -> document
// React 17
// 事件冒泡到 root,路径更短
button -> div -> root
跨浏览器兼容
事件委托减少事件监听器
当存在相同时间时,不需要创建多个事件,不需要多次绑定到root元素上,只需要绑定一次
javascript
// 1. 跨浏览器兼容性
function handleChange(e) {
// 在不同浏览器中都能正常工作
const value = e.target.value;
}
// 2. 性能优化
<div>
<button onClick={handleClick}>按钮1</button>
<button onClick={handleClick}>按钮2</button>
<button onClick={handleClick}>按钮3</button>
{/* 只在 root 上绑定一个事件处理器 */}
</div>
// 3. 统一的事件处理
function handleEvent(e) {
// 所有事件都有统一的接口
e.preventDefault();
e.stopPropagation();
e.nativeEvent;
}
提高了性能和可维护性。
请描述一下react 的渲染流程是什么
首先可以分为两个阶段 || 三个阶段
- 两个阶段
- reconciler 协调阶段(初始化挂载阶段 & 更新阶段)异步,可中断的
- commit 提交阶段
- 初始化挂载阶段:从APP跟组件开始执行,根据组件的render函数层层递归创建dom元素(并不会挂载),再根据虚拟dom元素创建fiber树结构,使return sbling child三个属性进行关联,并对副作用比如hooks state进行标记
- 更新阶段:会对初始化挂载阶段创建的fiber对象进行深度优先遍历,然后对新老节点进行diff对比,如果有变化就会标记为副作用,并记录变化的props相关参数,在commit阶段进行更新。
- 三个阶段
- reconciler协调阶段
- render渲染阶段
- comiit 提交阶段
- commit阶段:主要用于页面真正的渲染,这个阶段是同步的不可打断的,commit又分为三个阶段。 分别是commit之前,commit中,commit之后。
- commit之前: 执行组件渲染之前相关的hooks,比如useEffect。
- commit中,执行真正的渲染,这个时候dom会渲染到浏览器上。
- commit之后,处理一些渲染完成需要执行的事情,比如执行,useLayoutEffect。
- commit阶段:主要用于页面真正的渲染,这个阶段是同步的不可打断的,commit又分为三个阶段。 分别是commit之前,commit中,commit之后。
让我们来详细查看一下内容:
1. Reconciliation 阶段(可中断)
- Scheduler(调度器) :
- 调度任务优先级
- 时间分片
- 可中断渲染
javascript
// 1. 优先级调度
function scheduler() {
// 设置任务优先级
const priorities = {
ImmediatePriority: 1, // 最高优先级,需要同步执行
UserBlockingPriority: 2, // 用户交互
NormalPriority: 3, // 正常优先级
LowPriority: 4, // 低优先级
IdlePriority: 5 // 空闲优先级
};
// 任务时间分片
requestIdleCallback(() => {
// 在浏览器空闲时执行低优先级任务
});
}
- Reconciler(协调器) :
- 构建 Fiber 树
- Diff 算法
- 标记副作用 (hooks、state)
javascript
// 1. 构建/更新 Fiber 树
function reconciler() {
// a. 初始化挂载
function mount() {
// 根据组件创建 Fiber 节点
// 通过 return、child、sibling 连接
// 标记 Hooks、State 等副作用
}
// b. 更新处理
function update() {
// 深度优先遍历
// Diff 算法对比
// 标记需要更新的节点
}
}
2. Commit 阶段(不可中断)
before commit:执行 pre-commit hooks
javascript
function beforeCommit() {
// 1. 处理 DOM 渲染前的准备工作
// 2. 执行组件的 getSnapshotBeforeUpdate
// 3. 执行 useEffect 的清理函数
}
markdown
- **commit**:执行 DOM 操作
javascript
function commit() {
// 1. 处理 DOM 操作
switch (effectTag) {
case 'PLACEMENT':
// 插入新节点
break;
case 'UPDATE':
// 更新节点
break;
case 'DELETION':
// 删除节点
break;
}
// 2. 执行对应的生命周期
// 3. 更新 ref
}
after commit:执行 useLayoutEffect 等
javascript
function afterCommit() {
// 1. 执行 useLayoutEffect
// 2. 调用 componentDidMount/Update
// 3. 执行 useEffect
// 4. 清空副作用标记
}
这样就清晰多了:两个主要阶段,其中 Reconciliation 阶段包含调度和协调两个子过程。
React和React-Dom的区别?为什么有两个库?
- react:这是React库的核心。它定义了React组件的创建和生命周期方法,以及React元素的概念。你可以将其视为React的"引擎"。
- react-dom:这个库提供了在浏览器环境中使用React的方法,例如将React组件渲染到DOM中,或者在DOM中触发React组件的更新。你可以将其视为React的"驱动程序"。
为什么是两个包? 因为为了跨平台支持
javascript
// Web 平台
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, container);
// Native 平台
import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('App', () => App);
// 测试环境
import TestRenderer from 'react-test-renderer';
const testInstance = TestRenderer.create(<App />);
React 严格模式的作用
- 不安全的生命周期方法:某些生命周期方法在未来的React版本中将被弃用。严格模式会警告这些不安全的方法的使用。
- 使用过时或遗留的API:严格模式会警告使用过时或遗留的API。
- 意外的副作用:严格模式可以帮助你发现组件中可能的意外副作用。
防止你写一些有问题的代码 - 与旧版本React不兼容的代码:严格模式会警告你的代码中可能与未来版本的React不兼容的部分。
JSX的作用
可以在js 中编辑dom,结果babel编译成js认识的代码
1. JSX 的本质
JSX 本质上是 React.createElement()
的语法糖:
jsx
// JSX 写法
const element = (
<div className="greeting">
<h1>Hello, world!</h1>
</div>
);
// 编译后的 JavaScript
const element = React.createElement(
'div',
{ className: 'greeting' },
React.createElement('h1', null, 'Hello, world!')
);
2. JSX 的主要作用
- 声明式渲染:
jsx
// JSX 让界面描述更直观
function Welcome() {
return (
<div>
<h1>Welcome</h1>
<p>This is JSX</p>
</div>
);
}
// 不使用 JSX 则很难读
function Welcome() {
return React.createElement('div', null,
React.createElement('h1', null, 'Welcome'),
React.createElement('p', null, 'This is JSX')
);
}
- JavaScript 表达式支持:
jsx
const name = 'John';
const element = (
<div>
{/* 可以直接使用 JavaScript 表达式 */}
<h1>{name}</h1>
<p>{2 + 2}</p>
{isLoggedIn && <UserGreeting />}
</div>
);
- 属性传递:
jsx
// 动态属性
const buttonText = 'Click me';
const className = 'primary-button';
const button = (
<button className={className} disabled={false}>
{buttonText}
</button>
);
3. JSX 的特性
- 防止 XSS 注入:
jsx
const userInput = '<script>alert("XSS!")</script>';
// JSX 会自动转义,确保安全
const element = <div>{userInput}</div>;
- 支持展开运算符:
jsx
const props = {
firstName: 'John',
lastName: 'Doe'
};
const greeting = <Greeting {...props} />;
- 条件渲染:
jsx
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<UserGreeting />
) : (
<GuestGreeting />
)}
</div>
);
}
5. JSX 的优势
- 开发效率:
- 更直观的代码结构
- 更好的开发体验
- IDE 支持(语法高亮、自动完成)
- 可维护性:
- 声明式代码更容易理解
- 组件化开发
- 更容易重构
- 性能优化:
- 编译时优化
- 运行时优化
- 虚拟 DOM 的高效更新
6. 实际应用示例
jsx
// 一个完整的组件示例
function UserProfile({ user, onLogout }) {
const [isEditing, setIsEditing] = useState(false);
return (
<div className="user-profile">
<header>
<h2>{user.name}</h2>
{isEditing ? (
<button onClick={() => setIsEditing(false)}>
Save
</button>
) : (
<button onClick={() => setIsEditing(true)}>
Edit
</button>
)}
</header>
<main>
{isEditing ? (
<UserForm user={user} />
) : (
<UserDetails user={user} />
)}
</main>
<footer>
<button onClick={onLogout}>
Logout
</button>
</footer>
</div>
);
}
JSX 是 React 的一个重要特性,它让组件的编写更加直观和高效,同时保持了 JavaScript 的灵活性。
getDerivedStateFromProps 和 getSnapshotBeforeUpdate的作用
getDerivedStateFromProps
- 代替老版本中的componentWillReceiveProps() 方法判断前后两个props是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做以来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。
- 新版本中通过 getDerivedStateFromProps 来更新值,并且在 其中不能访问this.props,在 componentDidUpdate 里处理回调事件,也可以在 getDerivedStateFromProps中使用 componentDidUpdate,相当于return 就是修改 state
- 它的返回值,会对state进行相同属性覆盖,如果没有和state一样的属性就什么都不做。
执行时机: 挂载和更新都执行,在render之前。
什么场景下可以用这个方法?- 人民币转换大小写
1. 注意事项
1.1 避免无限循环:
js
// 错误示例
class Wrong extends React.Component {
state = { value: '' };
static getDerivedStateFromProps(props) {
// 错误:每次都返回新对象会导致无限更新
return { value: props.value };
}
}
// 正确示例
class Correct extends React.Component {
state = { value: '' };
static getDerivedStateFromProps(props, state) {
// 只在 props.value 变化时更新
if (props.value !== state.prevValue) {
return {
value: props.value,
prevValue: props.value
};
}
return null;
}
}
1.2 替代方案:
js
// 很多情况下可以用其他方式替代
function BetterComponent({ value }) {
// 1. 直接使用 props
return <div>{value}</div>;
// 2. 使用 useMemo
const computed = useMemo(() => {
return expensiveComputation(value);
}, [value]);
}
getSnapshotBeforeUpdate
- 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
- commit阶段 (提交) 包含 componentDIdMount | componetDidUpdate | componentWillUnmont
- getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。 - 当有 该生命周期有返回值时 componentDidUpdate 就存在第三个参数 (prevProps,prevState,snapshot)
- 执行时机:只会更新的时候执行,在render之后,浏览器页面更新之前
场景: 长列表,需要不断的往里面塞入内容。 更新之前,记住滚轮的位置,传递给componentDidUpdtate,保持滚轮的位置。 getSnapshotBeforeUpdate更新之前记录滚轮的原始位置,传递给componentDidUpdtate。 componentDidUpdtate就使用原始的位置更新dom,保证滚轮的正确性。
js
class ScrollList extends React.Component {
listRef = React.createRef();
getSnapshotBeforeUpdate(prevProps, prevState) {
// 如果列表有新增内容,返回当前滚动位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果有快照值,说明列表发生了变化
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef} style={{ height: '200px', overflow: 'auto' }}>
{this.props.list.map(item => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
}
}
useEffect的执行时机?useEffect 的返回函数执行时机?
- 回调函数执行时机
- 初始化阶段
- 依赖更新时
- 销毁函数
- 依赖更新时
- 组件卸载时
- 问: 为什么卸载函数在依赖更新时也会触发,举例聊天室重连的情况
这是因为 React 需要确保每次 effect 在重新执行前都能正确清理之前的副作用。让我解释一下具体原因:
1. Effect 的设计原理
jsx
function Example({ count }) {
useEffect(() => {
// 这是一个新的 effect
console.log(`订阅 count: ${count}`);
return () => {
// 清理前一个 effect
console.log(`取消订阅 count: ${count}`);
};
}, [count]);
}
每次依赖更新时:
- React 需要清理旧的 effect(执行清理函数)
- 然后执行新的 effect
这样设计的原因是为了保证数据的一致性。
2. 实际场景示例
- 事件订阅:
jsx
function UserStatus({ userId }) {
useEffect(() => {
// 订阅新用户的状态
const subscription = subscribe(userId);
return () => {
// 在订阅新用户之前,需要取消旧用户的订阅
subscription.unsubscribe();
};
}, [userId]);
}
// 执行顺序(当 userId 从 1 变为 2):
// 1. 取消订阅用户 1
// 2. 订阅用户 2
- WebSocket 连接:
jsx
function ChatRoom({ roomId }) {
useEffect(() => {
// 连接新房间
const connection = createConnection(roomId);
connection.connect();
return () => {
// 在连接新房间前,断开旧房间连接
connection.disconnect();
};
}, [roomId]);
}
3. 为什么需要这样设计
- 避免资源泄露:
jsx
function DataFetcher({ id }) {
useEffect(() => {
const controller = new AbortController();
fetch(`/api/data/${id}`, {
signal: controller.signal
});
return () => {
// 在发起新请求前取消旧请求
controller.abort();
};
}, [id]);
}
- 保持数据一致性:
jsx
function Timer({ delay }) {
useEffect(() => {
const timer = setInterval(() => {
console.log(`当前 delay: ${delay}`);
}, delay);
return () => {
// 在设置新定时器前清除旧定时器
clearInterval(timer);
};
}, [delay]);
}
4. 执行顺序演示
jsx
function Counter({ count }) {
useEffect(() => {
console.log(`Effect: count is ${count}`);
return () => {
console.log(`Cleanup: count was ${count}`);
};
}, [count]);
return <div>{count}</div>;
}
// 当 count 从 1 变为 2 时的日志:
// 1. "Cleanup: count was 1" // 清理旧的 effect
// 2. "Effect: count is 2" // 执行新的 effect
5. 注意事项
- 闭包陷阱:
jsx
function Example({ count }) {
useEffect(() => {
const timer = setInterval(() => {
// 这里的 count 是创建 effect 时的值
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组!
}
- 正确的依赖处理:
jsx
function Example({ count }) {
useEffect(() => {
const timer = setInterval(() => {
// 现在会正确响应 count 的变化
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]); // 添加正确的依赖
}
这种设计确保了:
- 副作用的正确清理
- 数据的一致性
- 避免资源泄露
- 更可预测的行为
虽然有时可能看起来有额外的开销,但这种设计对于维护应用的状态一致性是必要的。
为什么需要在依赖更新时清理:
因为 React 需要确保每次 effect 执行前都是"干净"的状态
- 防止副作用(会影响组件外部的操作 fetch 订阅 修改dom)重叠
javascript
useEffect(() => {
// 设置定时器
const timer = setInterval(() => {
console.log('当前count:', count);
}, 1000);
return () => clearInterval(timer); // 清理旧的定时器
}, [count]); // count 更新时
如果不清理,每次 count 变化都会创建新定时器,导致多个定时器同时运行
- 防止数据混乱
javascript
useEffect(() => {
// 订阅数据
const subscription = api.subscribe(userId, data => {
setUserData(data);
});
return () => subscription.unsubscribe(); // 取消旧的订阅
}, [userId]); // userId 更新时
如果不清理,切换用户时可能收到旧用户的数据
这就像:在重新装修房间前,需要先清理掉旧的装修,否则会造成混乱。
Hooks钩子为什么必须在顶层作用域?
因为在react中,组件的hooks是用链表这种数据结构进行连接的,通过next属性保持执行顺序的,如果从中间断开,或者执行的顺序不同,会导致找不到后面的钩子,使组件的渲染达到不可预期的情况。
它的底层就是在fiber 树中的memoizedState中存储的以链的形式 hook1=> hook2=> hook3的链表结构
hooks它解决了什么问题:
- hooks状态复用、逻辑复用。 class组件UI复用,比较臃肿。
- React官方的理念,并不想把hooks和生命周期混为一谈。你会发现hooks并不能完全和class组件对号入座?
Hooks 在 Fiber 中的存储结构:
javascript
// Fiber 节点结构(简化)
{
memoizedState: {
// hook1
memoizedState: value1,
next: {
// hook2
memoizedState: value2,
next: {
// hook3
memoizedState: value3,
next: null
}
}
}
}
fiber对象的颗粒度到底长什么样?
要注意,关于fiber的面试,会问fiber怎么体现组件间的关联关系的? 这里实际上就是3个属性,child,sibling,return 将整个项目所有fiber对象关联起来。 fiber对象的遍历方式是使用深度优先遍历。 深度优先遍历,这里面试官可能会追问,怎么遍历的? 有兴趣同学可以了解下,深度和广度遍历两种算法。 blog.csdn.net/qq_44918331...
javascript
// Fiber 节点的基本结构
type Fiber = {
// 标记 Fiber 类型
tag: WorkTag, // 例如:函数组件、类组件、原生DOM等
// 元素的标识
key: null | string,
elementType: any, // React 元素类型
type: any, // 组件类型
// DOM 相关
stateNode: any, // 实际 DOM 节点或组件实例
// Fiber 树结构
return: Fiber | null, // 父节点
child: Fiber | null, // 第一个子节点
sibling: Fiber | null, // 下一个兄弟节点
firstEffect: null, // 第一个副作用
lastEffect: null, // 最后一个副作用
nextEffect: null, // 下一个副作用
// 更新相关
pendingProps: any, // 新的 props
memoizedProps: any, // 当前 props
memoizedState: any, // 当前 state
};
// JSX 结构 实例
function App() {
return (
<div> // Fiber 节点
<Header /> // Fiber 节点
<Main> // Fiber 节点
<Article /> // Fiber 节点
<Sidebar /> // Fiber 节点
</Main>
<Footer /> // Fiber 节点
</div>
);
}
// 对应的 Fiber 树结构
const fiber = {
tag: HostComponent,
type: 'div',
child: headerFiber,
sibling: null,
return: parentFiber,
// ...其他属性
};
useEffect和useLayoutEffect的区别?
执行时机不同:
- useEffect
javascript
// 异步执行,在浏览器渲染完成后执行
useEffect(() => {
// 不会阻塞浏览器渲染
console.log('浏览器渲染后执行');
});
- useLayoutEffect
javascript
// 同步执行,在浏览器渲染前执行
useLayoutEffect(() => {
// 会阻塞浏览器渲染
console.log('浏览器渲染前执行');
});
应用场景:
加载时间过长导致添加的dom 元素 或者内容跳动 就可以使用他 案例
案例 :useEffect 是异步执行的,当浏览器渲染成功后才会执行,随后计算元素的位置,因为存在一个默认位置,此时如果计算元素位置这个步骤耗时很长就会出现元素抖动的情况,所有使用useLayoutEffec**t 同步执行**,当js修改dom之后再执行的useLayoutEffect,就可以在浏览器开始渲染执行计算元素的位置,之后再显示元素这样就可以解决抖动的问题, 意思就是说useLayoutEfect是同步操作,但是一般不适用于**耗时**的业务,因为会阻塞页面的渲染
使用场景:
- useEffect:
- 大多数副作用(网络请求、订阅等)
- 不需要同步更新的场景
- 性能更好,是默认推荐的选择
- useLayoutEffect:
- 需要操作 DOM 并且立即看到变化
- 防止页面闪烁
- 需要同步计算布局的场景
简单记忆:
- useEffect:渲染后执行,像"事后处理"
- useLayoutEffect:渲染前执行,像"同步处理"
useMemo useCallback React.memo的应用场景?
useMemo:缓存变量
useMemo 是用来解决react产生的无用渲染性能问题,就想生命周期中的**shouldComponentUpdate**异曲同工之妙 , 缓存的是变量 、 案例
:::color1 适用于 父组件需要更新 子组件不需要更新的情况, 因为react 的执行机制是至上而下的,当父组件更新子组件也会跟着更新 就可以用到useMemo, 前提是 **子组件的方法很耗时才需要使用到 **否则不建议使用,
问:在vue中有相应的方法吗?
computed
:::
javascript
// 缓存计算结果或值
const memoizedValue = useMemo(() => {
return expensiveComputation(a, b);
}, [a, b]);
// 该案例是在子组件内部做的缓存 也可以在父组件中做子组件prop 的缓存
// 下面的案例就是结合 React.Memo 做的
import React, { useState, useEffect ,useMemo } from 'react';
function Memo(){
const [state1,setState1] = useState('状态一')
const [state2,setState2] = useState('状态二')
return(
<div>
<button onClick={()=>setState1(Date.now())}>状态一</button>
<button onClick={()=>setState2(Date.now())}>状态二</button>
<MemoChildrens name={state1}>{state2}</MemoChildrens>
</div>
)
}
function MemoChildrens({name,children}){
// console.log(name,children)
function handleClick(name){
console.log('点击执行')
return name
}
//! 控制当name状态一发生变化 ,该函数才知晓,因为状态二和该函数没有关系
//! 第二个参数为空时,只会在组件创建时只需第一个参数
//! 它和useCallBack不同的地方是返回的是一个变量
const actionState1 = useMemo(()=>handleClick(name),[name])
return(
<>
<div>{actionState1} 状态一</div>
<div>{children} 状态二 </div>
</>
)
}
export default Memo;
使用场景:
- 复杂计算的结果
- 防止对象频繁创建
useCallback:缓存函数
是用来**缓存方法**的,当组件创建时,第二个参数发生变化时该方法会被重新创建,否则使用缓存中的方法, 和 useEffect类似 案例, 为什么会使用他,因为每次组件被创建或更新的时候函数都是创建一个新的个引用,前后不一样,所有需要使用到useCallback
使用场景:
传递React.memo的子组件的回调使用
作为其他hooks 的依赖时使用
复杂的回调逻辑使用
javascript
1. // 缓存函数
const memoizedFunction = useCallback(() => {
doSomething(a, b);
}, [a, b]);
2. useCallback作为其他hooks 的依赖时使用
function SearchComponent() {
const [query, setQuery] = useState('');
// ✅ 好的使用场景:函数会被用作其他 Hook 的依赖
const searchApi = useCallback(async (searchQuery) => {
const response = await fetch(`/api/search?q=${searchQuery}`);
return response.json();
}, []);
// searchApi 作为 useEffect 的依赖项
useEffect(() => {
searchApi(query);
}, [query, searchApi]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
3. userCallback+ react.memo
//该案例使用
//应用场景是 在父组件中对子组件的props 进行缓存用户就需要结合 React.memo
// 当然也可以在子组件内 做缓存 就不需要 React.memo ,上面一个案例就是子组件内部做缓存
function Bpp(props){
console.log('bpp 执行')
const {click} = props
return(
<div>Bpp</div>
)
}
// 将组件进行渲染优化 //此时父组件更新 data子组件不会更新
const BppMemo = React.memo(Bpp)
function App(){
const [count, setCount] = useState(0);
console.log('app 执行')
const click = useCallback(()=>{
alert(1)
},[])
let data = useMemo(()=>{
return {}
},[])
return(
<div>
<button onClick={()=>setCount(count +1)}>加</button>
{count}
<BppMemo data={data} click={click}/>
</div>
)
}
使用场景:
- 传递给子组件的函数
- 防止函数重新创建
React.memo:缓存组件
多用于计算密集型组件,使用React.memo来避免不必要的重复渲染,是用于组件级别的缓存,而useMemo是值级别的缓存
实例有 userTransiution 中for 的多次循环显示子组件,还有userCallback 中对子组件的 缓存函数案例
问:和 useMemo的区别是什么
React.memo 是缓存组件级的一般是配置useCallback 一起使用,useMemo是缓存变量的
javascript
// 缓存整个组件
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>;
});
// 父组件
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
console.log("按钮点击");
}, []);
console.log("父组件渲染");
return (
<div>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => setCount(count + 1)}>
count: {count}
</button>
{/* 没有使用 React.memo,即使 props 没变,也会重新渲染 */}
<ChildComponent onClick={handleClick} name="test" />
</div>
);
}
// 普通子组件(没有使用 React.memo)
function ChildComponent({ onClick, name }) {
console.log("子组件渲染");
return (
<div>
<h3>名字: {name}</h3>
<button onClick={onClick}>点击</button>
</div>
);
}
//////对比使用和不使用 React.memo 的情况:
// 1. 不使用 React.memo(会重新渲染)
function ChildComponent({ onClick, name }) {
console.log("普通子组件渲染");
return <div>{name}</div>;
}
// 2. 使用 React.memo(不会重新渲染)
const MemoChildComponent = React.memo(function ChildComponent({ onClick, name }) {
console.log("memo子组件渲染");
return <div>{name}</div>;
});
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 当点击按钮时,即使子组件的 props 没变
// ChildComponent 也会重新渲染
// 而 MemoChildComponent 不会重新渲染
return (
<div>
<button onClick={() => setCount(count + 1)}>
count: {count}
</button>
<ChildComponent name="test" />
<MemoChildComponent name="test" />
</div>
);
}
使用场景:
- 纯展示组件
- 接收简单 props 的组件
- 重复渲染的组件
简单记忆:
- useMemo:记住值
- useCallback:记住函数
- React.memo:记住组件
forwardRef useRef的作用? 在vue 中中的Ref的区别
React 中的 Ref:
- useRef:用于保存可变值或获取 DOM 元素
javascript
function Component() {
// 1. 保存值(不会触发重渲染)
const countRef = useRef(0);
countRef.current++; // 修改值
// 2. 获取 DOM 元素
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
- forwardRef:转发 ref 到子组件
javascript
// 子组件
const ChildInput = forwardRef((props, ref) => {
return <input ref={ref} />;
});
// 父组件
function Parent() {
const inputRef = useRef(null);
return <ChildInput ref={inputRef} />;
}
Vue 中的 Ref:
- 模板引用:
vue
<template>
<input ref="inputRef">
</template>
<script setup>
const inputRef = ref(null);
onMounted(() => {
inputRef.value.focus();
});
</script>
- 响应式数据:
vue
<script setup>
const count = ref(0); // 响应式数据
count.value++; // 修改会触发视图更新
</script>
主要区别:
- React 的 useRef:
- 不是响应式的
- 修改不会触发重渲染
- 主要用于存储 DOM 或值
- Vue 的 ref:
- 是响应式的
- 修改会触发视图更新
- 既可以引用 DOM 也是响应式数据
讲一下react 的并发更新
并发更新是基于fiber数据结构,react将任务进行细粒度的拆分,在源码中将任务分为5个等级,并发任务是在时间切片中执行,并且规定时间切片时间为5ms(超出时间片则让出主线程),当切片时间有空闲时间就会用来执行并发任务(保证主线程不被阻塞), 这个浏览器空闲时间 是requestIdLeCallback 的思想, 现在是通过messageChennel 中的postmassage 来模拟实现的,
- 高优先级任务可以打断低优先级任务
- 被打断的任务会在后续时间片中继续执行
- 确保重要的更新(如用户输入)能够及时响应
useTransition 和 useDeferredValue 区别?
都是用于处理性能优化的 Hook,但有不同的使用场景:
useTransition:控制状态更新优先级
javascript
function SearchComponent() {
const [isPending, startTransition] = useTransition();
const [searchQuery, setSearchQuery] = useState('');
const handleChange = (e) => {
// 输入框立即更新(高优先级)
setSearchQuery(e.target.value);
// 搜索结果延迟更新(低优先级)
startTransition(() => {
setSearchResults(search(e.target.value));
});
};
return (
<div>
<input onChange={handleChange} />
{isPending && <div>Loading...</div>}
<SearchResults results={searchResults} />
</div>
);
}
useDeferredValue:延迟值的更新
javascript
function SearchList({ query }) {
// 延迟处理搜索词
const deferredQuery = useDeferredValue(query);
// 使用延迟的值来渲染列表
return <ExpensiveList query={deferredQuery} />;
}
主要区别:
- useTransition:
- 主动控制更新优先级
- 可以显示 pending 状态
- 用于控制状态更新
- useDeferredValue:
- 被动接收需要延迟的值
- 类似防抖效果
- 用于延迟展示
简单理解:
- useTransition 像是"主动控制交通灯"
- useDeferredValue 像是"被动等待高峰期过去"
useInsertionEffect
主要是服务于 css -in- js 库的作者特意打造的
useSyncExternalStore的作用?
解决的问题是状态撕裂的问题,
这个问题是由react并发更新的机制引起的,比如我们正在执行更新任务,redux 将状态改为A,结果此时有个更高优先级的任务插入进来,旧的任务就会先暂停,但是这个时候新任务把redux 状态该为了B,然后react就不知道到到底依赖的是A 还是B
redux 中的useSelector的作用和原理
作用:读取store的某一个数据
原理:首先执行的 useContext(ReactReduxContext) 获取到store, 再通过 selector 获取到当前所需要的数据,最后通过store.subscribe注册回调订阅store的变化,当state发生变化时,会重新调用selectore函数获取到当前state,与原来的state进行对比,发送变化就强制更新组件
javascript
function useSelector<State, Selected>(
selector: (state: State) => Selected,
equalityFn = Object.is // 默认比较函数
) {
// 1. 获取 Redux store
const store = useContext(ReactReduxContext);
// 2. 获取当前选择的值
const selectedState = selector(store.getState());
// 3. 强制更新组件的函数
const [, forceRender] = useReducer(s => s + 1, 0);
// 4. 订阅 store 变化
useEffect(() => {
const unsubscribe = store.subscribe(() => {
// 获取最新状态
const newSelectedState = selector(store.getState());
// 比较新旧值
if (!equalityFn(selectedState, newSelectedState)) {
// 值变化时强制更新组件
forceRender();
}
});
// 清理订阅
return unsubscribe;
}, [store, selector, equalityFn]);
return selectedState;
}
useEffect 钩子,如果A组件嵌套B组件,执行顺序是什么
节点层级越深,越靠前 effect的回调收集过程是倒着收集的。 从最底层节点,往上收集。
Redux解决了什么问题?
实际上redux的模型并不是组件通信,redux的核心原理实际上是action修改store,store变化引起组件的重新渲染。
- Store 就是一个公共的数据仓库
- 可以理解为一个大对象,存放着所有需要共享的数据
- 修改数据的唯一方式是派发 Action
- Action 就像是一个命令,告诉 Redux "我要改什么数据"
- 比如:dispatch({ type: 'ADD_TODO', payload: '写作业' })
- 数据变化后,相关组件自动更新
- 只要组件订阅了 Store 中的数据
- Store 中的数据改变时,组件就会重新渲染
Redux 核心模型
javascript
// 1. Store: 存储状态
const store = createStore(reducer, initialState);
// 2. Action: 描述状态变化
const addTodo = (text) => ({
type: 'ADD_TODO',
payload: text
});
// 3. Reducer: 处理状态更新
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
default:
return state;
}
};
数据流动过程
javascript
// 1. 派发 Action
function TodoComponent() {
const dispatch = useDispatch();
const handleAdd = () => {
// 触发 action
dispatch(addTodo('新任务'));
};
}
// 2. Reducer 处理更新
// 3. Store 状态更新
// 4. 组件重新渲染
function TodoList() {
// 订阅 store 变化
const todos = useSelector(state => state.todos);
// store 变化导致组件重新渲染
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
单向数据流
javascript
// Action -> Reducer -> Store -> UI
const dataFlow = {
// 1. Action 触发更新
dispatch(action) ---->
// 2. Reducer 处理状态
reducer(state, action) ---->
// 3. Store 更新状态
store.setState(newState) ---->
// 4. UI 重新渲染
component.render()
};
redux 有什么中间件
immer 是什么?有什么用?
首先:react 规定不能直接修改数据,因为react 使用浅比较来判断状态是否改变,从而决定是否更新组件
javascript
// 1. 直接修改原始数据的情况
const user = { name: 'John' };
user.name = 'Mike'; // 修改后,user 还是原来的对象引用
// React 在比较时
prevState === currentState // true,因为是同一个对象引用
// 结果:React 认为状态没有变化,不会触发重新渲染
// 2. 创建新对象的情况
const prevUser = { name: 'John' };
const newUser = { ...prevUser, name: 'Mike' }; // 创建新对象
// React 在比较时
prevState === currentState // false,因为是不同的对象引用
// 结果:React 检测到变化,会触发重新渲染
如果使用传统的方式修改state的话会很繁琐,所以immer 就出现了,它简化了对数据的修改
javascript
### 2. 传统解决方案:创建新对象
// ✅ 正确方式:但很繁琐
const updateCity = () => {
setUser({
...user, // 复制第一层
address: {
...user.address, // 复制第二层
city: 'New York' // 修改目标属性
}
});
};
### 3. Immer 解决方案:看起来像直接修改,但实际上是安全的
// ✅ 使用 Immer:简单直观
const updateCity = () => {
setUser(draft => {
draft.address.city = 'New York'; // 看起来是直接修改
// 但 Immer 在背后帮我们处理了所有复制工作
});
};
当我们对一个对象既需要拷贝,拷贝完又要对拷贝完的对象修改时,如果使用浅拷贝,会有一个修改对象的深层属性同时修改原对象深层属性的问题,如果使用深拷贝,当对象的数据层级过深结构过复杂时,但是我们只需要修改一两个属性,却要对整个对象结构进行深拷贝,那么对象结构的很多部分进行的深拷贝,是一种对计算机资源的严重的浪费!所以我们就想要一种按需拷贝的方案,就是需要修改的一小部分结构进行深拷贝,不需要修改的大部分结构进行浅拷贝,immer就实现了这种方案!immutablejs也解决了同样的问题,
什么是受控组件?
将UI状态完全交给state或者props管理,不允许直接修改value。
错误边界组件的作用是什么?vue 中的类似的API 嘛?
react 的错误边界组件
在react 中出现错误的时候就会把整个页面删除,错误边界要做的就是 将错误的组件报错就行
在vite 项目中自带 react-error-boundary 插件,无需安装 案例
javascript
import React,{ useState } from "react";
import { ErrorBoundary,useErrorBoundary } from "react-error-boundary";
const MyComponent = () => {
const [value, setValue] = useState('');
const { showBoundary } = useErrorBoundary();
try{
const handleClick = () => {
// 这里故意抛出一个错误
if (value === '') {
// 这个是自动收集的方式
throw new Error('Value cannot be empty');
}
}
useEffect(() => {
fetchGreeting(name).then(
response => {
// Set data in state and re-render
},
error => {
// Show error boundary 这是手动的方式将错误传递给边界函数ErrorBoundary
showBoundary(error);
}
);
});
return (
<div>
Error
<button onClick={handleClick}>Click me 手动触发错误</button>
</div>
);
}catch(error){
showBoundary(error);
}
};
const MyComponent2 = () => {
return (
<div>另外一个组价</div>
)
}
const ErrorFallback = ({error, resetErrorBoundary }) => {
return (
<div>
<h1>Something went wrong. 显示错误消息的组件</h1>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
};
const Errors = () => {
return (
<>
<MyComponent2 />
<ErrorBoundary FallbackComponent={ErrorFallback} >
<MyComponent />
</ErrorBoundary>
</>
);
};
export default Errors;
vue 中错误API
Vue 提供了几种错误处理方式:
- 全局错误处理:
javascript
// main.ts
app.config.errorHandler = (err, instance, info) => {
// 处理错误
console.error('全局错误:', err)
}
- 组件级错误处理:
typescript
// 使用 onErrorCaptured 生命周期钩子
onErrorCaptured((err, instance, info) => {
// 处理错误
return false // 阻止错误继续传播
})
react 的diff 算法 和 vue 的diff算法对比
Vue 的处理:
- 通过双端对比,可以快速识别节点移动
- 最终只需要移动节点位置
React 的处理:
- 会依次比对,发现不同就创建新节点
- 可能会有更多的 DOM 操作
Tree Diff(树比较)
jsx
// 旧的结构
<div>
<p>Hello</p>
</div>
// 新的结构
<span>
<p>Hello</p>
</span>
// 因为根节点 div -> span 类型不同
// React 会直接删除旧树,创建新树
Component Diff(组件比较)
jsx
// 旧的结构
<div>
<Counter />
</div>
// 新的结构
<div>
<Timer />
</div>
// Counter 和 Timer 是不同的组件
// React 会删除 Counter,插入 Timer
Element Diff(元素比较)
jsx
// 旧的列表
<ul>
<li key="1">First</li>
<li key="2">Second</li>
</ul>
// 新的列表
<ul>
<li key="2">Second</li>
<li key="1">First</li>
</ul>
// 通过 key,React 知道只需要调整顺序
// 而不是删除后重建
简单说:
- dom 元素比较两棵树时,先看根节点类型
- 比较组件时,看组件类型
- 比较列表时,用 key 来判断
这样 React 就能高效地找出需要更新的部分。
Vue 的处理:
为什么 React 不用双端对比?
- 设计理念不同
- React 更注重简单性和可预测性
- Vue 更注重性能优化和减少 DOM 操作
- Fiber 架构的影响
- React 的 Fiber 架构需要支持中断和恢复
- 双端对比算法难以在中断后恢复状态
- 权衡取舍
- React 团队认为复杂的 Diff 算法收益有限
- 单端对比虽然可能多一些 DOM 操作,但代码更简单、可维护