refactor: properly cleanup invalidated jobs
This commit is contained in:
parent
d39eb6cdbc
commit
a95532495a
@ -25,6 +25,7 @@ import {
|
||||
} from './errorHandling'
|
||||
import { warn } from './warning'
|
||||
import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
|
||||
import { stop } from '@vue/observer'
|
||||
|
||||
let currentVNode: VNode | null = null
|
||||
let currentContextVNode: VNode | null = null
|
||||
@ -150,9 +151,6 @@ export function renderFunctionalRoot(vnode: VNode): VNode {
|
||||
}
|
||||
|
||||
export function teardownComponentInstance(instance: ComponentInstance) {
|
||||
if (instance._unmounted) {
|
||||
return
|
||||
}
|
||||
const parentComponent = instance.$parent && instance.$parent._self
|
||||
if (parentComponent && !parentComponent._unmounted) {
|
||||
parentComponent.$children.splice(
|
||||
@ -160,6 +158,7 @@ export function teardownComponentInstance(instance: ComponentInstance) {
|
||||
1
|
||||
)
|
||||
}
|
||||
stop(instance._updateHandle)
|
||||
teardownComputed(instance)
|
||||
teardownWatch(instance)
|
||||
}
|
||||
|
@ -5,7 +5,13 @@ import {
|
||||
immutable,
|
||||
AutorunOptions
|
||||
} from '@vue/observer'
|
||||
import { queueJob, handleSchedulerError, nextTick } from '@vue/scheduler'
|
||||
import {
|
||||
queueJob,
|
||||
handleSchedulerError,
|
||||
nextTick,
|
||||
queuePostCommitCb,
|
||||
flushPostCommitCbs
|
||||
} from '@vue/scheduler'
|
||||
import { VNodeFlags, ChildrenFlags } from './flags'
|
||||
import { EMPTY_OBJ, reservedPropRE, isString } from '@vue/shared'
|
||||
import {
|
||||
@ -110,22 +116,6 @@ export function createRenderer(options: RendererOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle Hooks -----------------------------------------------------------
|
||||
|
||||
const lifecycleHooks: Function[] = []
|
||||
const vnodeUpdatedHooks: Function[] = []
|
||||
|
||||
function queuePostCommitHook(fn: Function) {
|
||||
lifecycleHooks.push(fn)
|
||||
}
|
||||
|
||||
function flushHooks() {
|
||||
let fn
|
||||
while ((fn = lifecycleHooks.pop())) {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
|
||||
// mounting ------------------------------------------------------------------
|
||||
|
||||
function mount(
|
||||
@ -197,12 +187,12 @@ export function createRenderer(options: RendererOptions) {
|
||||
insertOrAppend(container, el, endNode)
|
||||
}
|
||||
if (ref) {
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
ref(el)
|
||||
})
|
||||
}
|
||||
if (data != null && data.vnodeMounted) {
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
data.vnodeMounted(vnode)
|
||||
})
|
||||
}
|
||||
@ -238,8 +228,17 @@ export function createRenderer(options: RendererOptions) {
|
||||
mountComponentInstance(vnode, container, isSVG, endNode)
|
||||
} else {
|
||||
queueJob(() => {
|
||||
mountComponentInstance(vnode, container, isSVG, endNode)
|
||||
}, flushHooks)
|
||||
const instance = mountComponentInstance(
|
||||
vnode,
|
||||
container,
|
||||
isSVG,
|
||||
endNode
|
||||
)
|
||||
// cleanup if mount is invalidated before committed
|
||||
return () => {
|
||||
teardownComponentInstance(instance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -279,7 +278,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
const subTree = (handle.prevTree = vnode.children = renderFunctionalRoot(
|
||||
vnode
|
||||
))
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
vnode.el = subTree.el as RenderNode
|
||||
})
|
||||
mount(subTree, container, vnode as MountedVNode, isSVG, endNode)
|
||||
@ -300,7 +299,13 @@ export function createRenderer(options: RendererOptions) {
|
||||
if (__COMPAT__) {
|
||||
doMount()
|
||||
} else {
|
||||
queueJob(doMount)
|
||||
queueJob(() => {
|
||||
doMount()
|
||||
// cleanup if mount is invalidated before committed
|
||||
return () => {
|
||||
stop(handle.runner)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,7 +318,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
const nextTree = (handle.prevTree = current.children = renderFunctionalRoot(
|
||||
current
|
||||
))
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
current.el = nextTree.el
|
||||
})
|
||||
patch(
|
||||
@ -349,7 +354,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
const { children, childFlags } = vnode
|
||||
switch (childFlags) {
|
||||
case ChildrenFlags.SINGLE_VNODE:
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
vnode.el = (children as MountedVNode).el
|
||||
})
|
||||
mount(children as VNode, container, contextVNode, isSVG, endNode)
|
||||
@ -360,7 +365,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
vnode.el = placeholder.el
|
||||
break
|
||||
default:
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
vnode.el = (children as MountedVNode[])[0].el
|
||||
})
|
||||
mountArrayChildren(
|
||||
@ -397,7 +402,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
)
|
||||
}
|
||||
if (ref) {
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
ref(target)
|
||||
})
|
||||
}
|
||||
@ -529,9 +534,10 @@ export function createRenderer(options: RendererOptions) {
|
||||
)
|
||||
|
||||
if (nextData != null && nextData.vnodeUpdated) {
|
||||
vnodeUpdatedHooks.push(() => {
|
||||
nextData.vnodeUpdated(nextVNode, prevVNode)
|
||||
})
|
||||
// TODO fix me
|
||||
// vnodeUpdatedHooks.push(() => {
|
||||
// nextData.vnodeUpdated(nextVNode, prevVNode)
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@ -611,7 +617,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
// then retrieve its next sibling to use as the end node for patchChildren.
|
||||
const endNode = platformNextSibling(getVNodeLastEl(prevVNode))
|
||||
const { childFlags, children } = nextVNode
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
switch (childFlags) {
|
||||
case ChildrenFlags.SINGLE_VNODE:
|
||||
nextVNode.el = (children as MountedVNode).el
|
||||
@ -1192,7 +1198,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
container: RenderNode | null,
|
||||
isSVG: boolean,
|
||||
endNode: RenderNode | null
|
||||
) {
|
||||
): ComponentInstance {
|
||||
if (__DEV__) {
|
||||
pushWarningContext(vnode)
|
||||
}
|
||||
@ -1212,12 +1218,8 @@ export function createRenderer(options: RendererOptions) {
|
||||
$options: { beforeMount, renderTracked, renderTriggered }
|
||||
} = instance
|
||||
|
||||
if (beforeMount) {
|
||||
callLifecycleHookWithHandler(beforeMount, $proxy, ErrorTypes.BEFORE_MOUNT)
|
||||
}
|
||||
|
||||
const queueUpdate = (instance.$forceUpdate = () => {
|
||||
queueJob(instance._updateHandle, flushHooks)
|
||||
queueJob(instance._updateHandle)
|
||||
})
|
||||
|
||||
const autorunOptions: AutorunOptions = {
|
||||
@ -1254,10 +1256,18 @@ export function createRenderer(options: RendererOptions) {
|
||||
if (instance._mounted) {
|
||||
updateComponentInstance(instance, isSVG)
|
||||
} else {
|
||||
if (beforeMount) {
|
||||
callLifecycleHookWithHandler(
|
||||
beforeMount,
|
||||
$proxy,
|
||||
ErrorTypes.BEFORE_MOUNT
|
||||
)
|
||||
}
|
||||
|
||||
// this will be executed synchronously right here
|
||||
instance.$vnode = renderInstanceRoot(instance) as MountedVNode
|
||||
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
vnode.el = instance.$vnode.el
|
||||
if (__COMPAT__) {
|
||||
// expose __vue__ for devtools
|
||||
@ -1283,6 +1293,8 @@ export function createRenderer(options: RendererOptions) {
|
||||
if (__DEV__) {
|
||||
popWarningContext()
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
function updateComponentInstance(
|
||||
@ -1312,7 +1324,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
instance
|
||||
) as MountedVNode)
|
||||
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
const el = nextVNode.el as RenderNode
|
||||
if (__COMPAT__) {
|
||||
// expose __vue__ for devtools
|
||||
@ -1337,15 +1349,15 @@ export function createRenderer(options: RendererOptions) {
|
||||
nextVNode
|
||||
)
|
||||
}
|
||||
if (vnodeUpdatedHooks.length > 0) {
|
||||
const vnodeUpdatedHooksForCurrentInstance = vnodeUpdatedHooks.slice()
|
||||
vnodeUpdatedHooks.length = 0
|
||||
queuePostCommitHook(() => {
|
||||
for (let i = 0; i < vnodeUpdatedHooksForCurrentInstance.length; i++) {
|
||||
vnodeUpdatedHooksForCurrentInstance[i]()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO fix me
|
||||
// if (vnodeUpdatedHooks.length > 0) {
|
||||
// const vnodeUpdatedHooksForCurrentInstance = vnodeUpdatedHooks.slice()
|
||||
// vnodeUpdatedHooks.length = 0
|
||||
// for (let i = 0; i < vnodeUpdatedHooksForCurrentInstance.length; i++) {
|
||||
// vnodeUpdatedHooksForCurrentInstance[i]()
|
||||
// }
|
||||
// }
|
||||
})
|
||||
|
||||
const container = platformParentNode(prevVNode.el) as RenderNode
|
||||
@ -1363,7 +1375,6 @@ export function createRenderer(options: RendererOptions) {
|
||||
const {
|
||||
$vnode,
|
||||
$proxy,
|
||||
_updateHandle,
|
||||
$options: { beforeUnmount, unmounted }
|
||||
} = instance
|
||||
if (beforeUnmount) {
|
||||
@ -1376,7 +1387,6 @@ export function createRenderer(options: RendererOptions) {
|
||||
if ($vnode) {
|
||||
unmount($vnode)
|
||||
}
|
||||
stop(_updateHandle)
|
||||
teardownComponentInstance(instance)
|
||||
instance._unmounted = true
|
||||
if (unmounted) {
|
||||
@ -1402,7 +1412,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
if (__DEV__) {
|
||||
popWarningContext()
|
||||
}
|
||||
queuePostCommitHook(() => {
|
||||
queuePostCommitCb(() => {
|
||||
callActivatedHook(instance, true)
|
||||
})
|
||||
}
|
||||
@ -1486,7 +1496,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
}
|
||||
}
|
||||
if (__COMPAT__) {
|
||||
flushHooks()
|
||||
flushPostCommitCbs()
|
||||
return vnode && vnode.flags & VNodeFlags.COMPONENT_STATEFUL
|
||||
? (vnode.children as ComponentInstance).$proxy
|
||||
: null
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { queueJob, nextTick } from '../src/index'
|
||||
import { queueJob, queuePostCommitCb, nextTick } from '../src/index'
|
||||
|
||||
describe('scheduler', () => {
|
||||
it('queueJob', async () => {
|
||||
@ -32,13 +32,15 @@ describe('scheduler', () => {
|
||||
expect(calls).toEqual(['job1', 'job2'])
|
||||
})
|
||||
|
||||
it('queueJob w/ postFlushCb', async () => {
|
||||
it('queueJob w/ postCommitCb', async () => {
|
||||
const calls: any = []
|
||||
const job1 = () => {
|
||||
calls.push('job1')
|
||||
queuePostCommitCb(cb1)
|
||||
}
|
||||
const job2 = () => {
|
||||
calls.push('job2')
|
||||
queuePostCommitCb(cb2)
|
||||
}
|
||||
const cb1 = () => {
|
||||
calls.push('cb1')
|
||||
@ -46,21 +48,24 @@ describe('scheduler', () => {
|
||||
const cb2 = () => {
|
||||
calls.push('cb2')
|
||||
}
|
||||
queueJob(job1, cb1)
|
||||
queueJob(job2, cb2)
|
||||
queueJob(job1)
|
||||
queueJob(job2)
|
||||
await nextTick()
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
|
||||
// post commit cbs are called in reverse!
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1'])
|
||||
})
|
||||
|
||||
it('queueJob w/ postFlushCb while flushing', async () => {
|
||||
const calls: any = []
|
||||
const job1 = () => {
|
||||
calls.push('job1')
|
||||
queuePostCommitCb(cb1)
|
||||
// job1 queues job2
|
||||
queueJob(job2, cb2)
|
||||
queueJob(job2)
|
||||
}
|
||||
const job2 = () => {
|
||||
calls.push('job2')
|
||||
queuePostCommitCb(cb2)
|
||||
}
|
||||
const cb1 = () => {
|
||||
calls.push('cb1')
|
||||
@ -68,10 +73,10 @@ describe('scheduler', () => {
|
||||
const cb2 = () => {
|
||||
calls.push('cb2')
|
||||
}
|
||||
queueJob(job1, cb1)
|
||||
queueJob(job1)
|
||||
expect(calls).toEqual([])
|
||||
await nextTick()
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1'])
|
||||
})
|
||||
|
||||
it('should dedupe queued tasks', async () => {
|
||||
@ -91,26 +96,28 @@ describe('scheduler', () => {
|
||||
expect(calls).toEqual(['job1', 'job2'])
|
||||
})
|
||||
|
||||
it('queueJob inside postFlushCb', async () => {
|
||||
it('queueJob inside postCommitCb', async () => {
|
||||
const calls: any = []
|
||||
const job1 = () => {
|
||||
calls.push('job1')
|
||||
queuePostCommitCb(cb1)
|
||||
}
|
||||
const cb1 = () => {
|
||||
// queue another job in postFlushCb
|
||||
calls.push('cb1')
|
||||
queueJob(job2, cb2)
|
||||
queueJob(job2)
|
||||
}
|
||||
const job2 = () => {
|
||||
calls.push('job2')
|
||||
queuePostCommitCb(cb2)
|
||||
}
|
||||
const cb2 = () => {
|
||||
calls.push('cb2')
|
||||
}
|
||||
|
||||
queueJob(job1, cb1)
|
||||
queueJob(job2, cb2)
|
||||
queueJob(job1)
|
||||
queueJob(job2)
|
||||
await nextTick()
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2', 'job2', 'cb2'])
|
||||
expect(calls).toEqual(['job1', 'job2', 'cb2', 'cb1', 'job2', 'cb2'])
|
||||
})
|
||||
})
|
||||
|
@ -1,8 +1,11 @@
|
||||
// TODO infinite updates detection
|
||||
|
||||
import { Op, setCurrentOps } from './patchNodeOps'
|
||||
|
||||
interface Job extends Function {
|
||||
ops: Op[]
|
||||
post: Function | null
|
||||
post: Function[]
|
||||
cleanup: Function | null
|
||||
expiration: number
|
||||
}
|
||||
|
||||
@ -12,6 +15,8 @@ const enum Priorities {
|
||||
|
||||
type ErrorHandler = (err: Error) => any
|
||||
|
||||
let currentJob: Job | null = null
|
||||
|
||||
let start: number = 0
|
||||
const getNow = () => window.performance.now()
|
||||
const frameBudget = __JSDOM__ ? Infinity : 1000 / 60
|
||||
@ -85,10 +90,10 @@ export function handleSchedulerError(handler: ErrorHandler) {
|
||||
|
||||
let hasPendingFlush = false
|
||||
|
||||
export function queueJob(rawJob: Function, postJob?: Function | null) {
|
||||
export function queueJob(rawJob: Function) {
|
||||
const job = rawJob as Job
|
||||
job.post = postJob || null
|
||||
job.ops = job.ops || []
|
||||
job.post = job.post || []
|
||||
// 1. let's see if this invalidates any work that
|
||||
// has already been done.
|
||||
const commitIndex = commitQueue.indexOf(job)
|
||||
@ -118,6 +123,24 @@ export function queueJob(rawJob: Function, postJob?: Function | null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function queuePostCommitCb(fn: Function) {
|
||||
if (currentJob) {
|
||||
currentJob.post.push(fn)
|
||||
} else {
|
||||
postCommitQueue.push(fn)
|
||||
}
|
||||
}
|
||||
|
||||
export function flushPostCommitCbs() {
|
||||
// post commit hooks (updated, mounted)
|
||||
// this queue is flushed in reverse becuase these hooks should be invoked
|
||||
// child first
|
||||
let job
|
||||
while ((job = postCommitQueue.pop())) {
|
||||
job()
|
||||
}
|
||||
}
|
||||
|
||||
function flush(): void {
|
||||
let job
|
||||
while (true) {
|
||||
@ -139,14 +162,12 @@ function flush(): void {
|
||||
// all done, time to commit!
|
||||
while ((job = commitQueue.shift())) {
|
||||
commitJob(job)
|
||||
if (job.post && postCommitQueue.indexOf(job.post) < 0) {
|
||||
postCommitQueue.push(job.post)
|
||||
if (job.post) {
|
||||
postCommitQueue.push(...job.post)
|
||||
job.post.length = 0
|
||||
}
|
||||
}
|
||||
// post commit hooks (updated, mounted)
|
||||
while ((job = postCommitQueue.shift())) {
|
||||
job()
|
||||
}
|
||||
flushPostCommitCbs()
|
||||
// some post commit hook triggered more updates...
|
||||
if (patchQueue.length > 0) {
|
||||
if (!__COMPAT__ && getNow() - start > frameBudget) {
|
||||
@ -174,7 +195,9 @@ function patchJob(job: Job) {
|
||||
// job with existing ops means it's already been patched in a low priority queue
|
||||
if (job.ops.length === 0) {
|
||||
setCurrentOps(job.ops)
|
||||
job()
|
||||
currentJob = job
|
||||
job.cleanup = job()
|
||||
currentJob = null
|
||||
setCurrentOps(null)
|
||||
commitQueue.push(job)
|
||||
}
|
||||
@ -190,4 +213,9 @@ function commitJob({ ops }: Job) {
|
||||
|
||||
function invalidateJob(job: Job) {
|
||||
job.ops.length = 0
|
||||
job.post.length = 0
|
||||
if (job.cleanup) {
|
||||
job.cleanup()
|
||||
job.cleanup = null
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,9 @@ describe('2.x compat build', async () => {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
const mounted = jest.fn()
|
||||
const updated = jest.fn()
|
||||
|
||||
const instance = new Vue({
|
||||
data() {
|
||||
return { count: 0 }
|
||||
@ -18,15 +21,19 @@ describe('2.x compat build', async () => {
|
||||
},
|
||||
render(h: any) {
|
||||
return h('div', this.count)
|
||||
}
|
||||
},
|
||||
mounted,
|
||||
updated
|
||||
}).$mount(root)
|
||||
|
||||
expect(instance.count).toBe(0)
|
||||
expect(root.textContent).toBe('0')
|
||||
expect(mounted).toHaveBeenCalled()
|
||||
|
||||
instance.change()
|
||||
expect(instance.count).toBe(1)
|
||||
await Vue.nextTick()
|
||||
expect(root.textContent).toBe('1')
|
||||
expect(updated).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user