scheduler
任务调度器
前置铺垫:schedler
的源码尽管只有二百多行,并且与组件更新前后、更新中的所有执行的【工作】无关。【工作】在这里有比拟形象,了解起来比拟艰难。好在有一点就是咱们在上一篇的文章中有了解到一个工作:watch Effect
。咱们就能够联合这个job
,对scheduler
进行剖析。在后续的文章中,当咱们解说到update
阶段的时候,会回来再看下scheduler
。到时候就能明确不少啦。
上面间接进入正题:
调度器在执行工作的过程中,次要将工作分为三个阶段,每个阶段两种状态:
- 前置刷新阶段
- 刷新阶段
- 后置刷新阶段
每个阶段各有两种状态:
- 正在期待刷新
- 正在刷新
每次刷新的时候,通过Promise.resolve
启动一个微工作,调用flushJo
b函数,先进行前置刷新工作,直至前置回调工作池为空,在刷新当前任务队列,当前任务队列刷新完结,最初刷新后置回调工作池,如此周而复始,直至三个工作池中的回调都刷新完结。
在解说watch
的时候,咱们说过,watch effect
会在组件update
之前执行。这与用户定义的副作用函数 配置项fulsh
无关。
flush: pre
,默认值。watch Effect
的flush
就是pre- 在创立
watch
的时候通过调用queuePreFlushCb(job)
,将副作用函数push
至pendingPreFlushCbs
- 当组件须要进行
update
的时候,会先遍历执行pendingPreFlushCbs
池中的回调 - 从而做到在组件
update
前进行刷新。
- 在创立
fulsh: post
。可选 但不举荐- 当设置
watch effect
的flush
为post
的时候就会调用queuePostFlushCb
函数,将副作用函数push
至pendingPostFlushCbs
- 当queue中的工作执行完之后,就会遍历执行
pendingPostFlushCbs
中的工作 - 从而做到在组件
update
后进行刷新
- 当设置
上面咱们一起看下这块相干的代码:
// 前置更新相干const pendingPreFlushCbs = []let activePreFlushCbs = nulllet preFlushIndex = 0// 后置更新相干const pendingPostFlushCbs = []let activePostFlushCbs = nulllet postFlushIndex = 0function queuePreFlushCb(cb) { queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)}function queuePostFlushCb(cb) { queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)}function queueCb(cb, activeQueue, pendingQueue, index) { if (!isArray(cb)) { // cb不是数组 if ( !activeQueue || !activeQueue.includes( cb, (cb as SchedulerJob).allowRecurse ? index + 1 : index ) ) { // activeQueue不存在 || 从index+1地位开始activeQueue不蕴含cb // watch job 会进来 pendingQueue.push(cb) } } else { // 如果cb是一个数组,则它是一个组件生命周期挂钩,只能由一个作业触发, // 该作业已在主队列中打消反复,sowe能够在此处跳过反复查看以进步性能 pendingQueue.push(...cb) } queueFlush()}
从下面的代码中能够复制往各阶段工作池中,push
工作的次要是queueCb
函数,queueCb
函数次要负责对工作进行判断,当工作是数组时,会间接解构至待执行队列中,当工作非数组的时候,须要对工作进行判断,push的工作不能在正在执行的工作队列中存在,或者以后没有正在执行的工作队列。最初会调用queueFlush
函数。
queueFlush
函数会依据以后的状态进行判断,只有非正在刷新且非正在期待刷新的状态下。才会通过Promise.resolve
启动微工作,刷新队列。
看下queueFlush
的代码:
// 冲刷队列function queueFlush() { // 如果没有正在刷新的 && 正在期待刷新的 // 则执行 flushJobs if (!isFlushing && !isFlushPending) { // 正在期待刷新 isFlushPending = true // 启动微工作,开始刷新工作队列。 // flushJobs执行完结 将promise赋值给 currentFlushPromise currentFlushPromise = resolvedPromise.then(flushJobs) }}
当启动微工作刷新队列的时候,会将isFlushPending = true
,示意开始期待刷新。当以后宏工作执行完结后,会执行相应的微工作队列,这时就会调用flushJobs
函数。开始刷新队列。
以后宏工作有哪些,咱们先不关注。首先要晓得每个宏工作都会对应一个微工作队列,宏工作执行完结才会执行相应的微工作队列。
这也就是
Vue
所提到的【防止同一个“tick
” 中多个状态扭转导致的不必要的反复调用,并异步刷新用户副作用函数】
function flushJobs(seen?: CountMap) { // 期待刷新完结,开始刷新 isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || new Map() } // 前置刷新开始 jobs flushPreFlushCbs(seen) // 前置刷新完结 // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child so its render effect will have smaller // priority number) // 2. If a component is unmounted during a parent component's update, // its update can be skipped. // 在刷新前对队列排序 // 1. 保障组件更新程序是从父组件到子组件(因为父组件总是在子组件之前创立,所以其渲染副作用的优先级将更小) // 2.如果一个子组件在父组件更新期间卸载了,能够跳过该子组件的更新。 queue.sort((a, b) => getId(a) - getId(b)) try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { if (__DEV__ && checkRecursiveUpdates(seen!, job)) { continue } // 执行 job 函数 callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { // 重置正在刷新队列 flushIndex = 0 queue.length = 0 // 刷新后置刷新jobs flushPostFlushCbs(seen) // 刷新完结 isFlushing = false // 重置以后刷新的promise // 最初再nextTick中会用到 currentFlushPromise = null // some postFlushCb queued jobs! // keep flushing until it drains. // 如果还有当前任务或者,期待的估算新工作,或者期待的后刷新工作,则递归刷新 if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) { // 递归刷新 flushJobs(seen) } }}
flushJobs
函数就是切入口,次要负责所有工作队列的刷新工作,前置工作的刷新次要是在该函数中调起flushPreFlushCbs(seen)
函数,先去刷新前置工作池中的所有工作。
flushPreFlushCbs(seen)
函数代码:
export function flushPreFlushCbs(seen ,parentJob) { if (pendingPreFlushCbs.length) { currentPreFlushParentJob = parentJob // 去重 activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 置预刷jobs array 为空 pendingPreFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() } for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++ ) { if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex]) ) { // 递归刷新查看 continue } // 执行job eg: watch job // watch 会在这里执行 activePreFlushCbs[preFlushIndex]() } // 重置 activePreFlushCbs = null preFlushIndex = 0 currentPreFlushParentJob = null // recursively flush until it drains // 递归刷新预刷新jobs flushPreFlushCbs(seen, parentJob) }}
对于flushPreFlushCbs
函数,咱们把次要关注点先放在:
- 前置更新状态的切换,由
pending
到active
- 遍历执行前置工作池中的每个工作
- 当遍历完结会重置以后状态及
index
- 递归调用
flushPreFlushCbs
,直至pendingPreFlushCbs
工作池为空。 - 次要是保障所有正在期待的队列会被执行到
有的同学可能会有疑难:既然曾经 通过
pendingPreFlushCbs.length = 0
,将待执行工作池清空了,为什么还须要递归持续。这个其实与遍历执行的工作无关,有的工作中,还会持续创立待执行工作,这时就会将创立的待执行工作持续
push
至待执行工作池。故须要递归遍历执行
当flushPreFlushCbs
函数执行完结后,就会进行以后遍历。即进入了flushing
阶段,这时存在于queue
的update
函数就会执行,组件就会进行更新。然而在执行queue
中的工作的时候,须要对工作去重 排序,这些工作实现之后,才会遍历执行queue
中的工作。
当queue
中的工作执行完结后,会通过 flushIndex = 0
,queue.length = 0
,对以后队列进行重置。
随后就会调用flushPostFlushCbs
函数,该函数会刷新后置刷新队列,同样的主逻辑:扭转后置刷新阶段状态,遍历执行后置刷新阶段工作池中的所有工作。
当watch Effect
的flush: post
的时候,这时就会遍历执行到watch effect
。
flushPostFlushCbs
与后面两个函数不一样的是:没有进行递归刷新。次要目标是为了保障各阶段中工作能按:前置➡以后➡后置阶段的程序进行刷新!
flushPostFlushCbs
函数的代码:
export function flushPostFlushCbs(seen?: CountMap) { // 如果存在后置刷新工作 if (pendingPostFlushCbs.length) { // 去重job const deduped = [...new Set(pendingPostFlushCbs)] // 正在期待的工作池 状况 pendingPostFlushCbs.length = 0 // #1947 already has active queue, nested flushPostFlushCbs call if (activePostFlushCbs) { // 如果曾经有沉闷的队列,嵌套的flushPostFlushCbs调用 activePostFlushCbs.push(...deduped) return } // 将期待的作为以后的工作 activePostFlushCbs = deduped if (__DEV__) { seen = seen || new Map() } // 对后置工作进行排序 activePostFlushCbs.sort((a, b) => getId(a) - getId(b)) for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) { if ( __DEV__ && checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex]) ) { continue } // 执行后置工作 activePostFlushCbs[postFlushIndex]() } // 重置正在执行的工作池 activePostFlushCbs = null postFlushIndex = 0 }}
当flushPostFlushCbs
函数执行完结的时候,就会回到flushJobs
函数,通过 isFlushing = false
重置刷新状态。
最初通过各个阶段工作池中时候有工作,再持续递归调用flushJobs
函数。
如此往返,直至所有阶段的工作执行完结。
nextTick
原理
咱们晓得nextTick API
会将回调提早到下次 DOM
更新循环之后执行。并会返回一个Promise
。
通过理解flushJobs
函数,flushJobs
函数次要就是通过Promsie.resolve
执行的,当flushJobs
函数执行完结,也就是Promsie.resolve
更改状态的时候。
首先flushJobs
函数会置空 currentFlushPromise
。最初才会通过Promsie.resolve
赋值给currentFlushPromise
。
当调用nextTick
的时候,返回的promise
,其实就是currentFlushPromise
。
能够再下来看下flushJobs
函数中的代码。
nextTick
代码:
const resolvedPromise: Promise<any> = Promise.resolve()let currentFlushPromise: Promise<void> | null = nullfunction nextTick( this: ComponentPublicInstance | void, fn?: () => void): Promise<void> { const p = currentFlushPromise || resolvedPromise return fn ? p.then(this ? fn.bind(this) : fn) : p}
其实明确了scheduler
的调度过程,nextTick
很好了解。
总结
通过剖析咱们晓得在Vue3
中scheduler
任务调度器,在执行工作的过程中,次要分为三个阶段,前置刷新阶段、后置刷新阶段、以后刷新阶段(update阶段),每个阶段都有两种状态:期待刷新 & 正在刷新,每个阶段发生变化后,状态都会进行重置。并且是按 前置➡以后➡后置➡前置...的过程进行的,如此往返,直到各阶段工作池中的所有工作完结。nextTick
是等所有阶段的刷新工作完结后返回的一个Promise.resolve
。
最初上一张图,总结下整个过程。
最初还是很(bu
)真(yao
)诚(lian
)的举荐下我的公众号:coder狂想曲。您的关注就是对我创作的最大激励呐。