做为从vue转向react的学习过程,我们先回顾下Vue和React对数据管理和界面渲染的解析流程
Vue :有数据劫持,数据发生改变时首先template会转化为render()函数去定义渲染的内容(用户不可见),通过AST抽象语法树过程优化,这一步主要是将模板语法v-model/v-if这些进行转换,之后调用 h 函数去创建虚拟dom节点
React : 采用 不可变数据 和 显式更新 机制。没有数据劫持,必须主动调用setState,用户可以看到有一个render函数,底层调用React.creatElement去实现,h函数作用相当于React.creatElement
一、表象认知:为什么我的状态更新总"迟到"?
当我们第一次在React事件处理函数中连续调用setState时,常常困惑于状态更新的滞后性:
kotlin
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出旧值
};
这种反直觉的表现源于React精心设计的异步批处理机制。在React的视角中,每个事件循环内的状态变更都是潜在的优化对象。通过将多个setState操作打包处理,React实现了:
- 避免频繁的组件重渲染,提高性能(多次state更新合并为单次渲染)
- 确保props与state的一致性(防止中间状态泄露)
- 为未来时间切片特性铺路(Concurrent Mode的调度基础)
二、内核机制:从调用到渲染的全链路解析
2.1 更新队列的幕后运作
每个类组件实例都持有独立的updater
对象,其核心结构如下:
javascript
class Component {
constructor() {
this.updater = {
isMounted: false,
pendingStates: [], // 待处理状态队列
pendingCallbacks: [], // 回调队列
enqueueSetState: function(partialState) {
// 合并逻辑...
scheduleUpdate();
}
};
}
}
2.2 事务系统的调度艺术(React 16前核心机制)
React早期采用事务机制处理更新流程,典型的事务流:
- 事务初始化:
initializeTransaction
- 预埋方法:
getUpdateQueue
- 执行主方法:
ReactUpdates.flushBatchedUpdates
- 清理阶段:
resetUpdateQueue
这种设计虽在Fiber架构中逐渐淡化,但其"原子化更新"的理念仍在延续。
2.3 Fiber架构下的优先级调度
现代React的调度器(Scheduler)实现了更细粒度的控制:
- 同步任务(ImmediatePriority)eg. 用户输入等紧急交互
- 用户阻塞任务(UserBlockingPriority) eg. 滚动加载
- 普通任务(NormalPriority) eg. 数据获取后的更新
- 低优先级任务(LowPriority)eg. 分析日志、非关键数据预加载
- 空闲任务(IdlePriority)eg. 埋点上报
每个setState根据触发场景被赋予不同优先级,通过requestIdleCallback
实现时间切片。
三、源码解析:从API到内核的完整过程
调用链路
ini
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback);
};
// ReactFiberClassComponent.js
enqueueSetState(inst, payload, callback) {
// 通过组件实例获取对应fiber
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// 获取优先级
const lane = requestUpdateLane(fiber);
// 创建update
const update = createUpdate(lane);
update.payload = payload;
// 赋值回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 将update插入updateQueue
enqueueUpdate(fiber, update);
// 调度update
scheduleUpdateOnFiber(fiber, lane);
}
合并更新策略
ini
function processUpdateQueue(workInProgress) {
let newBaseState = workInProgress.memoizedState;
let first = workInProgress.updateQueue.first;
while (first !== null) {
const update = first;
if (typeof update.payload === 'function') {
newBaseState = update.payload(newBaseState);
} else {
newBaseState = Object.assign({}, newBaseState, update.payload);
}
first = first.next;
}
workInProgress.memoizedState = newBaseState;
}
可以看到setState本质上合并是用了Object.assign这个方法
四、性能优化
不可变数据模式
ini
// 错误示例:直接修改原对象
this.setState(prev => {
prev.items.push(newItem); // 直接修改原state
return { items: prev.items };
});
// 正确模式:创建新引用
this.setState(prev => ({
items: [...prev.items, newItem]
}));
使用shouldComponentUpdate
阻断无效渲染,shouldComponentUpdate
(SCU)在pureComponment中用到,它采用shallowequal的方式进行浅层比较只比较第一层如果数据没发生改变就不更新。直接修改会导致浅比较失效。
自动批处理的演进
- React 17 及之前 :仅在 React 事件处理函数(如
onClick
)中自动批处理异步更新,setTimeout
或原生事件中的setState
会同步执行。 - React 18+ :默认启用 全自动批处理 ,所有场景(包括
setTimeout
、Promise
)中的多次更新均合并为单次渲染。
scss
// React 17行为
setTimeout(() => {
setCount(1); // 触发渲染
setFlag(true); // 再次触发渲染
}, 1000);
// React 18自动批处理
setTimeout(() => {
setCount(1); //
setFlag(true); // 合并为单次渲染
}, 1000);
五、常见问题
setState与useState的差异对比
特性 | setState (类组件) |
useState (函数组件) |
---|---|---|
更新方式 | 合并对象更新 | 替换当前状态 |
状态存储 | 单一对象存储所有状态 | 多个独立状态变量 |
闭包问题 | 无 | 需注意闭包陷阱 |
副作用处理 | 生命周期方法 | useEffect Hook |
性能优化 | shouldComponentUpdate |
React.memo + 引用更新 |
这篇文章为我React学习过程的第二篇文章,内容较为深入,同时也非常重要,过程中对我学习理解React设计理念有所启发。
进阶学习路线:
- 阅读ReactFiberHooks源码
- 实践React并发模式示例项目
- 学习状态机设计模式
- 探索原子化状态管理方案(Jotai/Recoil)