什么操作会导致react组件更新
- 组件本身state的改变:类组件 setState、函数组件 useState
- props发生变化:由父组件传递给子组件(父组件的state更新)
- 组件使用到的上下文context发生变化(provider提供者的value变化,而value一般也由state维护)
🚧🚧总结:react组件的更新是由state变化引起的
render过程中重要函数的流程图

- 更新入口:scheduleUpdateOnFiber,标记childLane,然后发起调度函数ensureRootIsScheduled
- schedule调度中心执行回调callback后,执行performSyncWorkOnRoot函数,开始真正的渲染
- render阶段是个workLoop循环,对fiber节点进行调和和更新;commit提交真正的DOM节点,更新结束
调度schedule
为什么使用调度
老版本的react在更新时会一次性遍历大量虚拟DOM,占用js线程,浏览器无法绘制导致页面卡顿。所以新版react的更新交由浏览器控制,让浏览器在空闲的时间段执行更新任务
事件切片 time slicing
每次事件循环React申请时间片,若浏览器有空闲时间,将执行react的更新任务
调度原理
调度入口 ensureRootIsScheduled
- 判断是否需要注册新的调度任务,不需要则退出
- 进行
existingCallbackPriority === newCallbackPriority的判断 - 不相等的情况注册新的任务调度
- 相等情况下则认为之前已经注册了调度任务了(比如连续进行了两次及以上的setState),退出函数。即多次触发更新只有第一次会进入调度,这个原则与react的批量更新实现有关。
- 进行
- 注册调度任务,将performSyncWorkOnRoot(同步)或performConcurrentWorkOnRoot(异步)封装到scheduleCallback,等到调度中心执行
任务队列 taskQueue timerQueue
scheduleCallback函数内部创建新任务,根据startTime > currentTime判断是否为超时任务
- 超时任务放入taskQueue中,等待
scheduler中的workLoop去执行 - 未超时的任务放入timerQueue中
- 调用requestHostTimeout延时执行函数handleTimeOut,延时时间为startTime - currentTime
- handleTimeOut函数将重新调取requestHostCallback请求回调执行函数
- handleTimeOut函数中会判断timerQueue中的task是否超时,若超时,则调用advanceTimers将timerQueue中的任务push到taskQueue中
通信实现 MessageChannel
// MessageChannel接受消息
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
// 执行回调callback
scheduledHostCallback(hasTimeRemaining, currentTime);
} else {
isMessageLoopRunning = false;
}
};
//messageChannel
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
// 请求回调
requestHostCallback = function(callback) {
// 赋值scheduledHostCallback
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// MessageChannel发送消息
port.postMessage(null);
}
};
- 建立channel通道和port接口
- requestHostCallback请求回调,将要执行的函数赋值给scheduledHostCallback,然后通过MessageChannel发送消息
- MessageChannel接受消息,调用performWorkUntilDeadline,执行scheduledHostCallback
🚧🚧注意:MessageChannel在事件循环中是宏任务,所以callback都是异步执行的
调度中心示意图

render 阶段
react的整个渲染过程(具体看上一篇文章)
- JSX代码通过React.createElement方法生成ReactElement结构
- Element结构通过schedule的调度reconcile去生成fiber结构树
- fiber结构树通过commit操作生成真实DOM元素

react的fiber树
举个🌰
export default function App() {
const [count,setCount] = useState(0)
const handleClick=()=>{
setCount(count+1)
}
return(
<div>
hello akechi,
<p> 这是我的count,值为{count} </p>
<button onClick={handleClick} >按钮</button>
</div>
)
}
画出它的fiber树架构

触发更新:当点击按钮的时候,setState调用了更新
- 类组件触发更新
enqueueSetState(inst: any, payload: any, callback) {
const fiber = getInstance(inst);
const lane = requestUpdateLane(fiber); //获取任务优先级lane
scheduleUpdateOnFiber(fiber, lane, eventTime); //调用更新入口
}
- 函数组件触发
function dispatchReducerAction(fiber,queue,action){
const lane = requestUpdateLane(fiber); //获取任务优先级lane
scheduleUpdateOnFiber(fiber, lane, eventTime); //调用更新入口
}
🚧🚧注意:更新并无区别,计算出任务优先级lane后调用更新函数scheduleUpdateOnFiber
scheduleUpdateOnFiber做了什么
- 调用了
markUpdateLaneFromFiberToRoot方法,在fiber树中从更新节点递归向上标记此次的更新优先级,如果无更新退出函数
举个🌰:当点击了count的时候

- 红色块为触发更新的区域,后续会被调和且render
- 蓝色块为它的祖父节点,后续会被调和但不执行render
- 绿色块将在workLoop的调和中被忽略
- 调用
ensureRootIsScheduled,开始任务的调度(调度请看上面的章节)。调度中心产生回调callback,最后执行performSyncWorkOnRoot函数,即进行render和commit
function performSyncWorkOnRoot(root) {
/* render阶段 */
let exitStatus = renderRootSync(root, lanes);
/* commit阶段 */
commitRoot(root);
/*更新剩余任务*/
ensureRootIsScheduled(root, now());
}
调和阶段
首先先看下render阶段几个比较重要的函数片段
function renderRootSync(root,lanes){
workLoopSync();
/* workLoop完毕后,证明所有节点都遍历完毕,那么重置状态,进入 commit 阶段 */
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
}
function workLoopSync() {
/* 循环执行 performUnitOfWork ,一直到 workInProgress 为空 */
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork){
/*向下调和fiber节点*/
let next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
/*向上回溯fiber节点*/
completeWork(unitOfWork);
} else {
workInProgress = next;
}
}
- render阶段的workLoop是个while循环,由fiberRoot为基递归式的遍历子节点
- beginWork调和的是父子节点
child,由父节点向子节点调和 - complteWork调和的是兄弟节点
sibling,如果没有兄弟节点则向上调和return
注意:更新组件时,fiber树中的节点是不需要每个都深度遍历过去的,beginwork有一系列优化策略
- 以下情况不考虑memo、pureComponet、shouldComponentUpdate等优化策略
- 判断父组件传的props是否发生变化,props发生变化则该节点需要reRender(优化策略下不刷新)
- 判断节点本身的lane与renderLane是否相等,相等则说明节点本身有更新,需要进行reRender(前置:判断优化策略)
- 判断节点本身的childLane与renderLane是否相等,若相等则说明该fiber节点的子节点存在更新节点,继续向下调和;不相等说明该fiber节点的子节点不存在更新,不需要调和。

补充:为什么memo、pureComponet、shouldComponentUpdate等优化策略可以避免组件reRender
- 默认情况下react在对比父组件的传值props时采用的是浅比较,即比较对象之间的地址;使用
memo、pureComponet优化策略之后采用深比较,即比较对象中的每一个属性是否相等 - 浅比较中,即使是同一个对象,比如{name:akechi},由于父组件更新导致props对象重新生成了,地址发生了改变,所以react会判断子组件需要reRender
- 深比较中,只要props的对象的值不发生变化,react不会判断子组件需要reRender
shouldComponentUpdate方法可以让class组件在本身的state更新时进行判断,组件是否需要更新
commit阶段
- commit阶段主要执行
commitRootImpl函数 - 调用
flushPassiveEffects,借由schedule异步调用useEffect方法,所以useEffect是在commit阶段完成后才执行 - before Muation:类组件执行
getSnapShotBeforeUpdate获取DOM节点的快照,函数组件同理 - Muation: 删除、更新、新增不需要的组件,函数组件执行
useInsertionEffect方法 - Layout:类组件执行
DidMount、DidUpdate方法,执行setState的callback方法;函数组件执行useLayOutEffect方法 - 完成commit阶段且浏览器绘制完成后,执行
useEffect方法
总结
react的更新一文到此结束,其实react的每个版本中的关键源码都有些许差异,甚至不同模式下的思路也不同,但是这并不影响我们对react的学习,毕竟源码方法的学习主要是学习一个思路,而不是记API。若在学习中有不同意见,请发表在评论区与我讨论,互相学习。