Javascript

React setState 源码解析

本文主要是介绍React setState 源码解析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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执行流程分析(伪代码)

  1. 首先,调用 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. 整体梳理

  1. 调用 setState时,会调用 enqueueSetState 方法创建一个包含 过期时间 和 优先级lane 以及最新state 的update更新器
  2. 调用 enqueueUpdate 方法将更新器存放到 fiber的更新队列中
  3. 调用 scheduleUpdateOnFiber 方法 schdule 更新
  4. 调用 ensureRootIsScheduled 方法创建一个 task 来 scheudle 更新
  5. 将更新任务 performWorkOnRoot 存放到 syncQueue 中等待调用
  6. 若是Legacy模式(非并发),不启用批量更新,直接调用更新方法 flushSyncCallbackQueue,执行更新,清空 syncQueue
  7. 若是Concurrent模式,则延迟调用 flushSyncCallbackQueue,等待所有同步任务执行结束后再调用 flushSyncCallbackQueue
  8. 在 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. 结语

以上是个人学习过程中的一些理解与总结,如果错误请指正

这篇关于React setState 源码解析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!