在 react 项目中,setState 被用于更新 state,从而实现组件重新渲染更新。经过查找阅读许多资料以及源码后,本文就来个人总结一下,简要解析 react 在 setState 后是如何更新组件的。
前言:setState 的同步和异步
- 异步:setState 一般情况下是异步的,由 react 的批量更新事务(ReactDefaultBatchingStrategy)控制(即 react 控制的事件,非调用 js 原生事件时是异步的),以及生命周期函数调用 setState 也不会同步更新 state。同一个函数中执行多个 setState 时会合并,并且同一个属性以最后一次 setState 的值为准。
- 同步:setState 在 setTimeout,setInterval 和 js 原生事件中被调用,则是同步的。
原理:同步和异步是由什么控制的? 是通过 batchedUpdates 函数中设置 isBatchingUpdates 为 true 时,则 setState 为异步更新,false 时为同步更新。
react15
先介绍事务(ReactDefaultBatchingStrategyTransaction)和 batchedUpdates 函数(用来启动事务的,启动事务后 setState 就是异步更新了),后介绍setState。
ReactDefaultBatchingStrategyTransaction 事务
setState 依靠事务进行更新,事务生命周期包含 initialize、perform、close 阶段。在开启事务后,遇到 setState 后 则将 partial state 存到组件实例的_pendingStateQueue 上, 接着调用 enqueueUpdate 排队更新方法,如下 setState 后解析。
// 批处理策略
batchUpdates
batchedUpdates先设置 isBatchingUpdates为true(ReactDefaultBatchingStrategy.isBatchingUpdates = true)开启事务,后将 callback函数放进事务里执行(transaction.perform(callback,...)),无论你传进去的函数是什么, 无论这个函数后续会做什么, 都会在执行完 callback(setState 的第二个参数)后调用事务的 close 方法
在 React 中,调用batchedUpdates有很多地方
第一种情况:首次渲染组件时(源码:在ReactMount.js里调用了ReactUpdates.batchedUpdates) 第二种情况:元素上或者组件上绑定了react控制的事件(非调用js原生事件),事件的监听函数中调用setState。(源码:在ReactEventListener.js里,react事件系统中的dispatchEvent函数启动了事务(调用了ReactUpdates.batchedUpdates))
重点:setState后
1、setState后
调用updater的enqueueSetState方法把需要更新的state(partial state)push进去等待队列_pendingStateQueue中, 接着调用enqueueUpdate排队更新。
2、enqueueUpdate
enqueueUpdate方法里需要判断batchingStrategy.isBatchingUpdates == true,即是否开启batch事务 情况一:如果已经开启batch,然后标记当前组件为dirtyComponent, 存到dirtyComponents数组中,等到 ReactDefaultBatchingStrategy事务结束时(close)调用runBatchedUpdates批量更新所有组件 情况二:方法中如果没有开启batch(或当前batch已结束,也就是说在事务的initialize或更新阶段)就调用batchedUpdates函数开启一次batch,再重新执行enqueueUpdate方法,判断isBatchingUpdates,现在为true了,标记当前组件为dirtyComponent, 存到dirtyComponents数组中, 并没有立即更新,而是继续执行后面事情,等到 ReactDefaultBatchingStrategy事务结束时(close)调用flushBatchedUpdates函数=>runBatchedUpdates函数批量更新所有组件(第三点)
// ReactBaseClasses.js :
3、事务结束时(close阶段)
批量更新的阶段,调用flushBatchedUpdates函数启动ReactUpdatesFlushTransaction事务负责批量更新,这个事务执行了runBatchedUpdates方法通过遍历dirtyComponents数组(在函数里ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件)进行批量更新。再结束本次batch事务(即ReactDefaultBatchingStrategy.isBatchingUpdates = false; )
var
4、updateComponent
ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件
performUpdateIfNecessary
5、updateComponent
react内部有3种不同组件:ReactCompositeComponent、ReactDOMComponent和ReactDOMTextComponent。 ReactCompositeComponent:re-render, 与之前 render 的 element 比较, 如果两者key && element.type 相等, 则进入下一层进行更新; 如果不等, 直接移除重新mount ReactDOMComponent: render前生成新的虚拟DOM,对前后虚拟DOM进行diff算法比较(传送门:react的diff算法),找出最小更新部分,批量更新 * ReactDOMTextComponent:直接更新Text
总结
react16:加入Fiber
react16版本中加入了Fiber架构(传送门:Fiber简介) Fiber主要分两个阶段
调度阶段(reconciliation):Fiber调度 1. 将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列. 2. 在任务队列中选出优先级高的 Fiber 执行,如果执行时间超过了deathLine,则设置为pending状态挂起状态(即执行一段时间,会跳出找Fiber任务队列中更高级的任务,如果有就放弃当前任务,即使当前任务执行了一半,可能已经经历了一些生命周期,都会被打断从来)。
渲染阶段(commit): 1. 进入render函数,构建真实的virtualDomTree,React将其所有的变更一次性更新到DOM上。
重点:setState后
调用updater的enqueueSetState方法,传入state, callback
Component
updater更新器中的enqueueSetState
var
enqueueSetState详解:
1、get获取组件实例上的fiber
function
2、计算到期时间/优先级computeExpirationForFiber
计算当前fiber的优先级(即过期时间),expirationTime 优先级 expirationTime 不为 1 的时候,则其值越低,优先级越高。Fiber任务的优先级:文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务。如下:
//用来计算fiber的到期时间,到期时间用来表示任务的优先级。
fiber优先级定义:
module
3、insertUpdateIntoFiber(fiber, update)
把更新信息任务插入update queue更新队列中。确保更新队列存在,不存在则调用ensureUpdateQueues(fiber)创建一个fiber队列,调用insertUpdateIntoQueue(queue, update)将update的值更新到queue队列中。
function
4、scheduleWork(fiber, expirationTime)
开启调度任务,进入fiber的调度阶段 scheduleWork执行流程:scheduleWork => scheduleWorkImpl => requestWork => 同步/异步 => performSyncWork => performWork => performWorkOnRoot => renderRoot/completeRoot => workLoop => performUnitOfWork => beginWork/completeUnitOfWork => updateClassComponent => reconcileChildrenAtExpirationTime => reconcileChildFibers => reconcileChildrenArray
scheduleWork执行流程比较长,下面讲下主要步骤(详细步骤代码):
- scheduleWorkImpl: 调用scheduleWorkImpl(fiber, expirationTime, false)函数更新每个node的优先级(即将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列)
- requestWork: 同步执行performSyncWork,异步执行scheduleCallbackWithExpiration, scheduleCallbackWithExpiration会调浏览器的requestidlecallback,在浏览器空闲的时候进行处理。 expirationTime:同步执行任务下expirationTime为0,即nowork=0。不为0时,使用requestidlecallback/requestAnimationFrame异步执行任务
- performSyncWork 主要的任务调度: 这里会找到高优任务先执行。 同步任务会直接调用performWorkOnRoot进行下一步, 异步任务也会调performWorkOnRoot,但处理不太一样 如果有上次遗留的任务,留到空闲时运行
- workLoop(fiber里的判断时间片,超过deathLine,则设置为pending状态): 异步任务在处理的时候会调用shouldYield,shouldYield会判断是不是已经超时了,超时暂时先不做。
- performUnitOfWork (reconcilation阶段) 调用beginWork处理组件,针对不同组件不同处理。此过程包括dom diff(生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列)。然后调用completeUnitOfWork对begin work产生的effect list进行一些处理,包括对ReactDOMComponent DOM组件的更新。
渲染阶段(commit)
进入render函数,构建真实的virtualDomTree,调用completeRoot/commitRoot,React将其所有的变更一次性更新到DOM上。
本文参考
从源码全面剖析 React 组件更新机制
React16——看看setState过程中fiber干了什么事情
react 16 渲染整理