Skip to content

Commit ec7b1fb

Browse files
committed
fix: workaround for memoryleak in destroyed hook
1 parent dc30544 commit ec7b1fb

File tree

1 file changed

+112
-106
lines changed

1 file changed

+112
-106
lines changed

src/shared/mixin.js

Lines changed: 112 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -29,130 +29,136 @@ export default function createMixin (Vue, options) {
2929
// Add a marker to know if it uses metaInfo
3030
// _vnode is used to know that it's attached to a real component
3131
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
32-
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
33-
if (!this.$root._vueMeta) {
34-
this.$root._vueMeta = { appId }
35-
appId++
36-
}
32+
if (isUndefined(this.$options[options.keyName]) || this.$options[options.keyName] === null) {
33+
return
34+
}
3735

38-
// to speed up updates we keep track of branches which have a component with vue-meta info defined
39-
// if _vueMeta = true it has info, if _vueMeta = false a child has info
40-
if (!this._vueMeta) {
41-
this._vueMeta = true
36+
if (!this.$root._vueMeta) {
37+
this.$root._vueMeta = { appId }
38+
appId++
39+
}
4240

43-
let p = this.$parent
44-
while (p && p !== this.$root) {
45-
if (isUndefined(p._vueMeta)) {
46-
p._vueMeta = false
47-
}
48-
p = p.$parent
41+
// to speed up updates we keep track of branches which have a component with vue-meta info defined
42+
// if _vueMeta = true it has info, if _vueMeta = false a child has info
43+
if (!this._vueMeta) {
44+
this._vueMeta = true
45+
46+
let p = this.$parent
47+
while (p && p !== this.$root) {
48+
if (isUndefined(p._vueMeta)) {
49+
p._vueMeta = false
4950
}
51+
p = p.$parent
5052
}
53+
}
5154

52-
// coerce function-style metaInfo to a computed prop so we can observe
53-
// it on creation
54-
if (isFunction(this.$options[options.keyName])) {
55-
if (!this.$options.computed) {
56-
this.$options.computed = {}
57-
}
58-
this.$options.computed.$metaInfo = this.$options[options.keyName]
59-
60-
if (!this.$isServer) {
61-
// if computed $metaInfo exists, watch it for updates & trigger a refresh
62-
// when it changes (i.e. automatically handle async actions that affect metaInfo)
63-
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
64-
ensuredPush(this.$options, 'created', () => {
65-
this.$watch('$metaInfo', function () {
66-
triggerUpdate(this, 'watcher')
67-
})
68-
})
69-
}
55+
// coerce function-style metaInfo to a computed prop so we can observe
56+
// it on creation
57+
if (isFunction(this.$options[options.keyName])) {
58+
if (!this.$options.computed) {
59+
this.$options.computed = {}
7060
}
61+
this.$options.computed.$metaInfo = this.$options[options.keyName]
7162

72-
// force an initial refresh on page load and prevent other lifecycleHooks
73-
// to triggerUpdate until this initial refresh is finished
74-
// this is to make sure that when a page is opened in an inactive tab which
75-
// has throttled rAF/timers we still immediately set the page title
76-
if (isUndefined(this.$root._vueMeta.initialized)) {
77-
this.$root._vueMeta.initialized = this.$isServer
78-
79-
if (!this.$root._vueMeta.initialized) {
80-
ensuredPush(this.$options, 'beforeMount', () => {
81-
// if this Vue-app was server rendered, set the appId to 'ssr'
82-
// only one SSR app per page is supported
83-
if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) {
84-
this.$root._vueMeta.appId = options.ssrAppId
85-
}
63+
if (!this.$isServer) {
64+
// if computed $metaInfo exists, watch it for updates & trigger a refresh
65+
// when it changes (i.e. automatically handle async actions that affect metaInfo)
66+
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
67+
ensuredPush(this.$options, 'created', () => {
68+
this.$watch('$metaInfo', () => {
69+
this.__metaInfo = undefined
70+
triggerUpdate(this, 'watcher')
8671
})
72+
})
73+
}
74+
}
8775

88-
// we use the mounted hook here as on page load
89-
ensuredPush(this.$options, 'mounted', () => {
90-
if (!this.$root._vueMeta.initialized) {
91-
// used in triggerUpdate to check if a change was triggered
92-
// during initialization
93-
this.$root._vueMeta.initializing = true
94-
95-
// refresh meta in nextTick so all child components have loaded
96-
this.$nextTick(function () {
97-
const { tags, metaInfo } = this.$root.$meta().refresh()
98-
99-
// After ssr hydration (identifier by tags === false) check
100-
// if initialized was set to null in triggerUpdate. That'd mean
101-
// that during initilazation changes where triggered which need
102-
// to be applied OR a metaInfo watcher was triggered before the
103-
// current hook was called
104-
// (during initialization all changes are blocked)
105-
if (tags === false && this.$root._vueMeta.initialized === null) {
106-
this.$nextTick(() => triggerUpdate(this, 'initializing'))
107-
}
108-
109-
this.$root._vueMeta.initialized = true
110-
delete this.$root._vueMeta.initializing
111-
112-
// add the navigation guards if they havent been added yet
113-
// they are needed for the afterNavigation callback
114-
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
115-
addNavGuards(this)
116-
}
117-
})
118-
}
119-
})
76+
// force an initial refresh on page load and prevent other lifecycleHooks
77+
// to triggerUpdate until this initial refresh is finished
78+
// this is to make sure that when a page is opened in an inactive tab which
79+
// has throttled rAF/timers we still immediately set the page title
80+
if (isUndefined(this.$root._vueMeta.initialized)) {
81+
this.$root._vueMeta.initialized = this.$isServer
82+
83+
if (!this.$root._vueMeta.initialized) {
84+
ensuredPush(this.$options, 'beforeMount', () => {
85+
// if this Vue-app was server rendered, set the appId to 'ssr'
86+
// only one SSR app per page is supported
87+
if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) {
88+
this.$root._vueMeta.appId = options.ssrAppId
89+
}
90+
})
12091

121-
// add the navigation guards if requested
122-
if (options.refreshOnceOnNavigation) {
123-
addNavGuards(this)
92+
// we use the mounted hook here as on page load
93+
ensuredPush(this.$options, 'mounted', () => {
94+
if (!this.$root._vueMeta.initialized) {
95+
// used in triggerUpdate to check if a change was triggered
96+
// during initialization
97+
this.$root._vueMeta.initializing = true
98+
99+
// refresh meta in nextTick so all child components have loaded
100+
this.$nextTick(function () {
101+
const { tags, metaInfo } = this.$root.$meta().refresh()
102+
103+
// After ssr hydration (identifier by tags === false) check
104+
// if initialized was set to null in triggerUpdate. That'd mean
105+
// that during initilazation changes where triggered which need
106+
// to be applied OR a metaInfo watcher was triggered before the
107+
// current hook was called
108+
// (during initialization all changes are blocked)
109+
if (tags === false && this.$root._vueMeta.initialized === null) {
110+
this.$nextTick(() => triggerUpdate(this, 'initializing'))
111+
}
112+
113+
this.$root._vueMeta.initialized = true
114+
delete this.$root._vueMeta.initializing
115+
116+
// add the navigation guards if they havent been added yet
117+
// they are needed for the afterNavigation callback
118+
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
119+
addNavGuards(this)
120+
}
121+
})
124122
}
123+
})
124+
125+
// add the navigation guards if requested
126+
if (options.refreshOnceOnNavigation) {
127+
addNavGuards(this)
125128
}
126129
}
130+
}
127131

128-
// do not trigger refresh on the server side
129-
if (!this.$isServer) {
130-
// no need to add this hooks on server side
131-
updateOnLifecycleHook.forEach((lifecycleHook) => {
132-
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
133-
})
132+
// do not trigger refresh on the server side
133+
if (this.$isServer) {
134+
return
135+
}
134136

135-
// re-render meta data when returning from a child component to parent
136-
ensuredPush(this.$options, 'destroyed', () => {
137-
// Wait that element is hidden before refreshing meta tags (to support animations)
138-
const interval = setInterval(() => {
139-
if (this.$el && this.$el.offsetParent !== null) {
140-
/* istanbul ignore next line */
141-
return
142-
}
137+
// no need to add this hooks on server side
138+
updateOnLifecycleHook.forEach((lifecycleHook) => {
139+
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
140+
})
141+
},
142+
// TODO: move back into beforeCreate when Vue issue is resolved
143+
destroyed () {
144+
// do not trigger refresh:
145+
// - on the server side
146+
// - when the component doesnt have a parent
147+
// - doesnt have metaInfo defined
148+
if (this.$isServer || !this.$parent || !hasMetaInfo(this)) {
149+
return
150+
}
143151

144-
clearInterval(interval)
152+
// Wait that element is hidden before refreshing meta tags (to support animations)
153+
const interval = setInterval(() => {
154+
if (this.$el && this.$el.offsetParent !== null) {
155+
return
156+
}
145157

146-
if (!this.$parent) {
147-
/* istanbul ignore next line */
148-
return
149-
}
158+
clearInterval(interval)
150159

151-
triggerUpdate(this, 'destroyed')
152-
}, 50)
153-
})
154-
}
155-
}
160+
triggerUpdate(this, 'destroyed')
161+
}, 50)
156162
}
157163
}
158164
}

0 commit comments

Comments
 (0)