共计 6575 个字符,预计需要花费 17 分钟才能阅读完成。
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 = null
let preFlushIndex = 0
// 后置更新相干
const pendingPostFlushCbs = []
let activePostFlushCbs = null
let postFlushIndex = 0
function 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 = null
function 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 狂想曲。您的关注就是对我创作的最大激励呐。