Vue-源码解析-2
Vue Scheduler
Vue中的Scheduler(任务调度系统),主要责任就是收集Vue在程序执行过程中产生的Jobs(任务),让每个Job按照固定的顺序执行,避免资源的浪费和程序中的循环依赖导致的死循环。
Vue Scheduler中的核心分为以下几个部分:
- 任务队列(queue、pendingPostFlushCbs、activePostFlushCbs)
- 任务入队程序(queueJob, queuePostFlushCb)
- 任务执行状态(isFlushing、isFlushPending)
- 当前正在执行的任务Promise(currentFlushPromise)
- 任务执行程序(flushJobs、flushPostFlushCbs、flushPreFlushCbs)
- 任务优先级调配程序(comparator)
源码位置:packages/runtime-core/src/scheduler.ts
任务队列
在Vue Scheduler中的所有任务分为pre、普通任务、post三种显式优先级,以及根据id进行排序的隐式优先级(主要用作将父组件任务前置,确保组件的任务链不会乱序),如下所示:
pre任务和普通任务放在queue中post任务放在pendingPostFlushCbs以及activePostFlushCbs中
flowchart TB
subgraph queue
pre任务
普通任务
end
subgraph pendingPostFlushCbs
post任务
end
subgraph activePostFlushCbs
正在执行的post任务
end
任务的添加
在Vue执行的不同阶段都有任务产生,一部分任务会根据当时的配置立即执行或某些条件下随parent.update执行,另一部分会被添加到任务调度系统中进行统一的调度,以下为Vue中的入队方法:
1 | // packages/runtime-core/src/scheduler.ts |
queueJob
在任务进入queueJob时会进行查重的判断,不存在才会将任务push或insert进queue,同时由watch进入的Job是允许递归调用自身的,这时需要开发者自己思考好自己的代码是否会无限执行。
在renderer.ts的
setupRenderEffect函数(处理组件渲染的核心函数)中会创建update对象,在update执行时任务会被收集到queue中,如下:1
2
3
4
5
6
7
8
9
10
11
12
13// packages/runtime-core/src/renderer.ts
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
// ...
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
// ...
update()在this.$forceUpdate()执行时会被放入
queue中,如下为Vue在this上拓展的属性其中就包含$forceUpdate:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// packages/runtime-core/src/componentPublicInstance.ts
export const publicPropertiesMap: PublicPropertiesMap =
// Move PURE marker to new line to workaround compiler discarding it
// due to type annotation
/*#__PURE__*/ extend(Object.create(null), {
$: i => i,
$el: i => i.vnode.el,
$data: i => i.data,
$props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
$attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
$slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
$refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
$parent: i => getPublicInstance(i.parent),
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)异步组件加载时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// packages/runtime-core/src/apiAsyncComponent.ts
// ...
load()
.then(() => {
loaded.value = true
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// parent is keep-alive, force update so the loaded component's
// name is taken into account
queueJob(instance.parent.update)
}
})
.catch(err => {
onError(err)
error.value = err
})
// ...Watch执行时
1
2
3
4
5
6
7
8
9
10
11
12
13
14// packages/runtime-core/src/apiWatch.ts
// ...
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
// ...hmr(开发环境热更新)时也会执行
queueJob
queuePostFlushCb
在Job进入queuePostFlushCb时也会进行判断,此时判断的是任务是否在activePostFlushCbs队列中,即是否将要执行,如果不存在即将执行的任务队列则添加到pendingPostFlushCbs队列
在组件的mount、update、unmount等生命周期时都会执行
queuePostFlushCb添加后置任务点太多了,都粘过来有点多余了,具体详见源码: packages/runtime-core/src/renderer.ts
在
Suspense组件中父组件全部加载完之后1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// packages/runtime-core/src/components/Suspense.ts
// ...
// flush buffered effects
// check if there is a pending parent suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
if (parent.pendingBranch) {
// found a pending parent suspense, merge buffered post jobs
// into that parent
parent.effects.push(...effects)
hasUnresolvedAncestor = true
break
}
parent = parent.parent
}
// no pending parent suspense, flush all jobs
if (!hasUnresolvedAncestor) {
queuePostFlushCb(effects)
}hmr(热更新之后)
1
2
3
4
5
6
7
8
9// packages/runtime-core/src/hmr.ts
// 5. make sure to cleanup dirty hmr components after update
queuePostFlushCb(() => {
for (const instance of instances) {
hmrDirtyComponents.delete(
normalizeClassComponent(instance.type as HMRComponent)
)
}
})
ssr 相关的没列
任务优先级
Vue中的scheduler通过comparator给queue队列中的pre任务和普通任务进行排序,而pendingPostFlushCbs队列则存放post任务一定在queue队列执行之后才会执行
1 | // packages/runtime-core/src/scheduler.ts |
flowchart LR a(pre Job1) --> a2(Job1) --> a3(Job2) --> a4(post Job1)
任务执行分析
任务通过以下三个函数执行:
flushJobs
这个函数最初由queueJob开始触发执行queueFlush,包装一层Promise.resolve后执行flushJobs
此处主要分析
flushJobs函数,其余源码请看:packages/runtime-core/src/scheduler.ts
1 | function flushJobs(seen?: CountMap) { |
flushPreFlushCbs
这个函数只会在组件render和updateComponentPreRender时才会执行
1 | export function flushPreFlushCbs( |
flushPostFlushCbs
这个函数会执行后置任务即post任务,除在flushJobs中执行完queue队列后执行外,在组件render阶段也会被执行
1 | export function flushPostFlushCbs(seen?: CountMap) { |
nextTick
作为Vue中的一个全局API,它的作用是等待下一次 DOM 更新刷新的工具方法.
它同样写在了packages/runtime-core/src/scheduler.ts文件中,它是通过什么才能确保在下一次更新时才执行回调的呢,是不是有这个疑问,下面就是它的源码:
1 | export function nextTick<T = void, R = void>( |
它就是在任务调度系统中维护的那个currentFlushPromise当前任务的Promise之后加了个.then的微任务,由于任务队列中的任务都会在currentFlushPromise的第一个.then中执行,所以nextTick才能在那些任务之后执行。
总结
scheduler的作用就是接收其他的函数丢过来的任务,然后把他们执行掉
别人只管丢任务给总部,不用管任务什么时候做,就好比我们的排期,产品只管丢给actionView,actionView会安排任务什么时候执行。。
