1. setState是同步还是异步?
- 在legacy模式下,在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout等是同步的
- 在concurrent模式下,即使是在setTimeout中也是“异步”的
- 严格意义上来说,应该不是异步,只是执行时间比同步晚,这里直接用“异步”来讲
2. setState是如何实现异步批量更新的?
当 setState 方法被调用后,方法内部会创建一个包含过期时间和优先级lane的update(更新器),将最新的state挂载在update的 payload上,最后将此 update 存放到 fiber的 updateQueue队列中
简单点说,就是每个fiber身上都会有一个更新队列,在调用setState时,会将状态临时存储到更新器中,当需要更新组件时再来执行更新队列中的内容,清空更新队列
- 批量模式:如果是批量模式,则会将执行更新,清空更新队列的方法延迟调用。此时scheduleUpdateOnFiber 方法内只会调用 ensureRootIsScheduled ,在事件方法结束后,才会调用 flushSyncCallbackQueue 方法
- 非批量模式:非批量模式下,则会在 ensureRootIsScheduled 调用结束后直接执行更新方法(flushSyncCallbackQueue)
3. setState执行流程分析(伪代码)
- 首先,调用 setState 时,会调用 this.updater.enqueueSetState 方法
// 以下皆是伪代码
// 源码参考位置:ReactBaseClasses: react/src/ReactBaseClasses.js
export class Component{
constructure(){
this.updater = ClassComponentUpdater
}
setState(partialState,callback){
this.updater.enqueueSetState(this,partialState,callback,'setState')
}
}
// 源码参考位置:ReactFiberClassComponent: react-recondiler/src/ReactFiberClassComponent.js
let ClassComponentUpdater = {
enqueueSetState(inst,payload,callback){
const fiber = getInstance(inst)
const eventTime = requestEventTime()
// 源码参考位置: ReactFiberLane: react-reconciler/src/ReactFiberLane.js
const lane = requestUpdateLane(fiber)
const update = createUpdate(eventTime,lane)
update.payload = payload
callback && (update.callback = callback)
// Add the update to fiber's updateQueue
enqueueUpdate(fiber,update)
// schedule update
scheduleUpdateOnFiber(fiber)
}
}
- getInstance 方法会获取 当前组件的fiber对象
function getInstance(inst){
return inst._reactInternal
}
-
requestEventTime 方法用来计算事件优先级
-
createUpdate会创建一个包含过期时间和优先级lane的update(更新器),然后将最新的state挂载在update的 payload上
-
enqueueUpdate 会将此更新器存放到当前fiber的更新队列中
// updateQueue 在react源码中是一个循环链表,此处用数组模拟
function enqueueUpdate(fiber,update){
fiber.updateQueue.push(update)
}
2. enqueueSetState 在创建更新之后,会调用 scheduleUpdateOnFiber 来 schedule 更新
function scheduleUpdateOnFiber(fiber){
const root = markUpdateLaneFormFiberToRoot(fiber)
if(root === null){
return null
}
// create a task to update from root
ensureRootIsScheduled(root)
// NoContext 和 NoMode 的情况,即不是 concurrent 模式
if(excutionContext === NoContext && (fiber.mode && ConcurrentMode) === NoMode){
// 非并发模式下,不启用批量更新,直接调用更新方法
flushSyncCallbackQueue()
}
}
// Note:react源码中用位操作和进制表示 Context 和 Mode 等
// 参考文件:mode: react-reconciler/src/ReactTypeOfMode.js
// 参考文件:Lane : react-reconciler/src/ReactFiberLane.new.js
// 参考文件:Context: react-reconciler/src/ReactFiberWorkLoop.new.js
-
markUpdateLaneFormFiberToRoot 用来获取根结点,从根结点出发schedule更新
3. 调用 ensureRootIsScheduled 开始实施更新
function ensureRootIsScheduled(rootFiber){
// reconciler root
let nextLanes = SyncLane // 1
let nextCallbackPriority = SyncLanePriority // 12
// is working fiber's priority
let existingCallbackPriority = rootFiber.callbackPriority
// 如果这个新的更新和当前根结点已调度的更新相等,那就直接返回,复用上次的更新,不再创建新的更新任务
// 并发模式下即使使用 setTimeout 也无法打断批量更新的原因就是在于这里
if(existingCallbackPriority === nextCallbackPriority){
return
}
scheduleSyncCallback(performWorkOnRoot.bind(null,rootFiber))
// put in micro task
// 用微任务来 模拟 延迟flushSyncCallbackQueue
queueMicrotask(flushSyncCallbackQueue)
rootFiber.callbackPriority = nextCallbackPriority
}
4. 调用 scheduleSyncCallback 将更新函数存放到 syncQueue 队列中等待更新
// put performWorkOnRoot to a queue, waiting for excuting
function scheduleSyncCallback(cb){
syncQueue.push(cb)
}
5. 在 performWorkOnRoot方法中 实施更新,从根结点依次向下更新
// render (dom diff / render)
function performWorkOnRoot(workInProcess){
let root = workInProcess
// start reconciler
while(workInProcess){
if(workInProcess.tag === ClassComponent){
let inst = workInProcess.stateNode // get instance
// get the newState
inst.state = processUpdateQueue(inst,workInProcess)
// re-render to gain the new Virtual Dom,then diff
inst.render()
}
// update child
workInProcess = workInProcess.child
}
commitRoot(root)
}
-
commitRoot 方法中会重置 callbackPriority
6. 在 processUpdateQueue 中获取最新的 state
function processUpdateQueue(inst,fiber){
return fiber.updateQueue.reduce((state,action)=>{
if(typeof update.payload === 'function'){
// type protect
update.payload = update.payload(state)
}
return {
...state,
...update.payload
}
},inst.state)
}
7. 利用flushSyncCallbackQueue 清空 syncQueue 更新队列,执行更新
// syncQueue 中存放的就是更新操作,此时一一执行,释放更新队列
function flushSyncCallbackQueue(){
syncQueue.forEach(cb=>cb())
syncQueue.length = 0
}
4. 整体梳理
- 调用 setState时,会调用 enqueueSetState 方法创建一个包含 过期时间 和 优先级lane 以及最新state 的update更新器
- 调用 enqueueUpdate 方法将更新器存放到 fiber的更新队列中
- 调用 scheduleUpdateOnFiber 方法 schdule 更新
- 调用 ensureRootIsScheduled 方法创建一个 task 来 scheudle 更新
- 将更新任务 performWorkOnRoot 存放到 syncQueue 中等待调用
- 若是Legacy模式(非并发),不启用批量更新,直接调用更新方法 flushSyncCallbackQueue,执行更新,清空 syncQueue
- 若是Concurrent模式,则延迟调用 flushSyncCallbackQueue,等待所有同步任务执行结束后再调用 flushSyncCallbackQueue
- 在 performWorkOnRoot 方法中,会循环fiber身上的更新队列拿到最新的state,然后进行dom diff 以及 re-render
5. React.unstale_batchedUpdate
unstale_batchedUpdate方法可以在Legacy模式下,让 setState在 setTimeout中依旧是批量更新,其根本原理就是改变 excutionContext
export function batchedUpdates(fn){
let prevExecutionContext = excutionContext
excutionContext |= batchedContext
fn()
excutionContext = prevExecutionContext
}
6. 结语
以上是个人学习过程中的一些理解与总结,如果错误请指正