Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions src/core/observer/dep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface DepTarget extends DebuggerOptions {
id: number
addDep(dep: Dep): void
update(): void
active?: boolean
}

/**
Expand All @@ -45,14 +46,11 @@ export default class Dep {
}

removeSub(sub: DepTarget) {
// #12696 deps with massive amount of subscribers are extremely slow to
// clean up in Chromium
// to workaround this, we unset the sub for now, and clear them on
// next scheduler flush.
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
const index = this.subs.indexOf(sub)
if (index > -1) {
this.subs.splice(index, 1)
// 🔧 STEP 4.6: defensive cleanup
sub.active = false
}
}

Expand Down
1 change: 1 addition & 0 deletions src/core/observer/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export default class Watcher implements DepTarget {
/**
* Remove self from all dependencies' subscriber list.
*/

teardown() {
if (this.vm && !this.vm._isBeingDestroyed) {
remove(this.vm._scope.effects, this)
Expand Down
50 changes: 7 additions & 43 deletions src/core/vdom/create-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ const componentVNodeHooks = {
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(
Expand All @@ -57,10 +56,10 @@ const componentVNodeHooks = {
const child = (vnode.componentInstance = oldVnode.componentInstance)
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
options.propsData,
options.listeners,
vnode,
options.children
)
},

Expand All @@ -72,14 +71,9 @@ const componentVNodeHooks = {
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
activateChildComponent(componentInstance, true)
}
}
},
Expand All @@ -90,7 +84,7 @@ const componentVNodeHooks = {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
deactivateChildComponent(componentInstance, true)
}
}
}
Expand All @@ -111,47 +105,36 @@ export function createComponent(

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor as typeof Component)
}

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (__DEV__) {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}

// async component
let asyncFactory
// @ts-expect-error
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
// return a placeholder node for async component, which is rendered
// as a comment node but preserves all the raw information for the node.
// the information will be used for async server-rendering and hydration.
return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
}

data = data || {}

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor as typeof Component)

// transform component v-model data into props & events
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data)
}

// extract props
// @ts-expect-error
const propsData = extractPropsFromVNodeData(data, Ctor, tag)

Expand All @@ -167,41 +150,28 @@ export function createComponent(
)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn

// @ts-expect-error
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot

// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

// install component management hooks onto the placeholder node
installComponentHooks(data)

// return a placeholder vnode
// @ts-expect-error
const name = getComponentName(Ctor.options) || tag
const vnode = new VNode(
// @ts-expect-error
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data,
undefined,
undefined,
undefined,
context,
// @ts-expect-error
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
Expand All @@ -210,17 +180,14 @@ export function createComponent(
}

export function createComponentInstanceForVnode(
// we know it's MountedComponentVNode but flow doesn't
vnode: any,
// activeInstance in lifecycle state
parent?: any
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
Expand All @@ -244,16 +211,13 @@ function installComponentHooks(data: VNodeData) {

function mergeHook(f1: any, f2: any): Function {
const merged = (a, b) => {
// flow complains about extra args which is why we use any
f1(a, b)
f2(a, b)
}
merged._merged = true
return merged
}

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel(options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
Expand Down
13 changes: 10 additions & 3 deletions src/core/vdom/create-functional-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export function FunctionalRenderContext(
Ctor: typeof Component
) {
const options = Ctor.options

// 🔹 NEW: store functional component watchers
this._functionalWatchers = []

// ensure the createElement function in functional components
// gets a unique context - this is necessary for correct named slot check
let contextVm
Expand All @@ -40,6 +44,7 @@ export function FunctionalRenderContext(
// @ts-ignore
parent = parent._original
}

const isCompiled = isTrue(options._compiled)
const needNormalization = !isCompiled

Expand Down Expand Up @@ -107,6 +112,7 @@ export function createFunctionalComponent(
const options = Ctor.options
const props = {}
const propOptions = options.props

if (isDef(propOptions)) {
for (const key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject)
Expand Down Expand Up @@ -157,12 +163,13 @@ function cloneAndMarkFunctionalResult(
options,
renderContext
) {
// #7817 clone node before setting fnContext, otherwise if the node is reused
// (e.g. it was from a cached normal slot) the fnContext causes named slots
// that should not be matched to match.
const clone = cloneVNode(vnode)
clone.fnContext = contextVm
clone.fnOptions = options

// 🔹 NEW: attach functional watchers to vnode
clone._functionalWatchers = renderContext._functionalWatchers

if (__DEV__) {
;(clone.devtoolsMeta = clone.devtoolsMeta || ({} as any)).renderContext =
renderContext
Expand Down
27 changes: 18 additions & 9 deletions src/core/vdom/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,18 +349,27 @@ export function createPatchFunction(backend) {
}

function invokeDestroyHook(vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
let i, j
const data = vnode.data
if (vnode._functionalWatchers) {
for (let i = 0; i < vnode._functionalWatchers.length; i++) {
vnode._functionalWatchers[i].teardown()
}
if (isDef((i = vnode.children))) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
vnode._functionalWatchers.length = 0
}

if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}

if (isDef((i = vnode.children))) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}


function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
Expand Down
23 changes: 23 additions & 0 deletions test/unit/features/component/component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,26 @@ describe('Component', () => {
).toHaveBeenWarned()
})
})



it('cleans up watchers for functional components on destroy', done => {
let vm = new Vue({
render(h) {
return h({
functional: true,
render(h, ctx) {
return h('div', ctx.props.msg)
}
}, { props: { msg: 'hello' }})
}
}).$mount()

const vnode = vm._vnode
expect(vnode._functionalWatchers.length).toBeGreaterThan(0)

vm.$destroy()

expect(vnode._functionalWatchers.length).toBe(0)
done()
})