vue3源码】6.schedulerscheduler是vue3中一个比较重要的概念。通过调度器调度任务(jobs),保证了Vue中相关API、生命周期函数、组件渲染顺序的正确性。我们知道,使用watchEffect监听数据源时,监听器会在组件渲染之前执行;使用watchSyncEffect监听数据源时,监听器会在依赖发生变化后立即执行;而当watchPostEffect用于监听数据源时,监听器直到组件渲染后才会执行。不同监听器的执行顺序是通过调度器的统一调度来实现的。调度器的实现主要是通过调度器中的三个队列来实现任务调度。这三对是:pendingPreFlushCbs:组件更新前任务队列queue:组件更新任务队列pendingPostFlushCbs:组件更新后任务队列如何使用这些队列?vue中有3个方法分别入队pendingPreFlushCbs、queue、pendingPostFlushCbs。//入队前任务队列导出函数queuePreFlushCb(cb:SchedulerJob){queueCb(cb,activePreFlushCbs,pendingPreFlushCbs,preFlushIndex)}//入队后任务队列导出函数queuePostFlushCb(cb:SchedulerJobs){queueFlushCb(cb,CactivebPost,pendingPostFlushCbs,postFlushIndex)}functionqueueCb(cb:SchedulerJobs,activeQueue:SchedulerJob[]|null,pendingQueue:SchedulerJob[],index:number){//如果cb不是数组if(!isArray(cb)){//active队列为空或者cb不在active队列中,需要将cb添加到对应的队列中.push(cb)}}//cb是一个数组else{//如果cb是一个数组,那么它是一个组件生命周期钩子//它已经被去重了,所以我们可以在这里跳过重复检查来提高性能pendingQueue.push(...cb)}queueFlush()}//queuequeueexportfunctionqueueJob(job:SchedulerJob){//只有当满足以下条件之一//1.队列长度为0//2。队列中没有作业(如果作业是watch()回调,则从flushIndex+1开始查找,否则从flushIndex开始查找),作业不等于currentPreFlushParentJobif((!queue.length||!queue.includes(job,isFlushing&&job.allowRecurse?flushIndex+1:flushIndex))&&job!==currentPreFlushParentJob){//job.id为null直接入队if(job.id==null){queue.push(job)}else{//跳入队列,队列索引范围[flushIndex+1,end]中的job.id跳入队列后不递减//findInsertionIndex方法使用二分法查找大于等于范围在[flushIndex+1,end]job.id的第一个indexqueue.splice(findInsertionIndex(job.id),0,job)}queueFlush()}}三个队列的入队分别是几乎相似,queue中不允许有重复作业(从队列的flushIndex或flushIndex+1开始查找),不同的是queue允许跳队。queueFlushjob加入队列后会调用一个queueFlush函数:或者此时等待执行队列,则需要设置isFlushPending为true,表示队列在等待执行。//同时在下一个微任务队列中执行flushJobs,即在下一个微任务队列中执行队列if(!isFlushing&&!isFlushPending){isFlushPending=truecurrentFlushPromise=resolvedPromise.then(flushJobs)}}为什么您需要将flushJobs放入下一个微任务队列而不是宏任务队列?首先,微任务比宏任务有更高的优先级。当宏任务和微任务同时存在时,会先执行所有微任务,然后再执行宏任务。这说明通过microtasks,flushJobs可以尽可能的提前。实施。如果使用宏任务,如果queueJob之前有多个宏任务,必须等这些宏任务都执行完了再执行queueJob,所以flushJobs的执行会很晚。在flushJobs中,flushJobs会依次执行pendingPreFlushCbs、queue、pendingPostFlushCbs中的任务。如果此时还有剩余作业,则继续执行flushJobs,直到三个队列中的任务全部执行完。functionflushJobs(seen?:CountMap){//设置isFlushPending为false,isFlushing为true//因为此时队列即将执行isFlushPending=falseisFlushing=trueif(__DEV__){seen=seen||newMap()}//执行前驱任务队列flushPreFlushCbs(seen)//队列按照job.id升序排列//这样保证://1.组件先从父组件更新,再从父组件更新子组件。(因为parent总是先于child创建,所以它的redner效果会有更高的优先级)//2.如果在父组件更新的过程中卸载了组件,可以跳过它的更新queue.sort((a,b)=>getId(a)-getId(b))//用来检测是否是无限递归,最多递归100层,否则会报错,只会在开发模式下检查。常量检查=__DEV__?(job:SchedulerJob)=>checkRecursiveUpdates(seen!,job):NOOPtry{//执行队列中的任务for(flushIndex=0;flushIndexgetId(a)-getId(b))//循环执行jobfor(postFlushIndex=0;postFlushIndex(this:T,fn?:(this:T)=>void):Promise{constp=currentFlushPromise||resolvedPromise返回fn?p.then(this?fn.bind(this):fn):p}nextTick会在flushJobs执行完之后执行,组件更新和onUpdated,onMounted等生命周期钩子会在nextTick之前执行,所以可以得到最新的DOM在nextTick.then中获得。哪些操作会交给调度器进行调度?watchEffect和watchPostEffect会将监听器的执行分别加入到任务前队列和任务后队列中。functiondoWatch(){//...constjob:SchedulerJob=()=>{if(!effect.active){return}if(cb){//watch(source,cb)constnewValue=effect.run()if(deep||forceTrigger||(isMultiSource?(newValueasany[]).some((v,i)=>hasChanged(v,(oldValueasany[])[i])):hasChanged(newValue,oldValue))||(__COMPAT__&&isArray(newValue)&&isCompatEnabled(DeprecationTypes.WATCH_ARRAY,instance))){//在再次运行cb之前进行清理if(cleanup){cleanup()}callWithAsyncErrorHandling(cb,instance,ErrorCodes.WATCH_CALLBACK,[newValue,//首次更改时将undefined作为旧值传递oldValue===INITIAL_WATCHER_VALUE?undefined:oldValue,onCleanup])oldValue=newValue}}else{//watchEffecteffect.run()}}if(flush==='sync'){scheduler=jobasany//调度函数被直接调用}elseif(flush==='post'){scheduler=()=>queuePostRenderEffect(job,instance&&instance.suspense)}else{//默认值:'pre'scheduler=()=>queuePreFlushCb(job)}consteffect=newReactiveEffect(getter,scheduler)//...}组件更新函数:constsetupRenderEffect=()=>{//...constcomponentUpdateFn=()=>{//...}consteffect=(instance.effect=newReactiveEffect(componentUpdateFn,()=>queueJob(更新),instance.scope))const更新:SchedulerJob=(instance.update=()=>effect.run())update.id=instance.uid//...}onMounted,onUpdated,onUnmounted,TransitionSomeenter钩子等钩子函数将被放置在后任务队列中exportconstqueuePostRenderEffect=__FEATURE_SUSPENSE__?queueEffectWithSuspense:queuePostFlushCbconstmountElement=()=>{//...if((vnodeHook=props&&props.onVnodeMounted)||needCallTransitionHooks||dirs){queuePostRenderEffect(()=>{vnodeHook&&invokeVNodeHook(vnodeHook,parentComponent,vnode)needCallTransitionHooks&&transition!.enter(el)dirs&&invokeDirectiveHook(vnode,null,parentComponent,'mounted')},parentSuspense)}}constpatchElement=()=>{//...if((vnodeHook=newProps.onVnodeUpdated)||dirs){queuePostRenderEffect(()=>{vnodeHook&&invokeVNodeHook(vnodeHook,parentComponent,n2,n1)dirs&&invokeDirectiveHook(n2,n1,parentComponent,'updated')},parentSuspense)}}总结调度器是通过三个队列实现的,在vue中,调用即可queuePreFlushCb、queuePostFlushCb、queueJob方法将作业添加到对应的队列中,无需手动控制作业的执行时机,将作业的执行时机完全交给调度器进行调度三个队列的特点:pendingPreFlushCbsqueuependingPostFlushCbs执行时机DOM更新前队列中的作业包含组件的更新。DOM更新后,是否允许跳转。执行作业。确保父子组件的更新顺序按照job.id升序执行。在jobscheduler中,队列中的job(即flushJobs)的执行通过Promise.resolve()被放入下一个microtask队列,而nextTick.then中回调的执行则被放入下一个microtask队列。当执行nextTick.then中的回调时,队列中的作业已经执行完毕,此时DOM已经更新,所以在nextTick.then中可以获取到更新后的DOM。