Skip to content

Commit

Permalink
fix: workaround for memoryleak in destroyed hook
Browse files Browse the repository at this point in the history
  • Loading branch information
pimlie committed Aug 30, 2019
1 parent dc30544 commit ec7b1fb
Showing 1 changed file with 112 additions and 106 deletions.
218 changes: 112 additions & 106 deletions src/shared/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,130 +29,136 @@ export default function createMixin (Vue, options) {
// Add a marker to know if it uses metaInfo
// _vnode is used to know that it's attached to a real component
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
if (!this.$root._vueMeta) {
this.$root._vueMeta = { appId }
appId++
}
if (isUndefined(this.$options[options.keyName]) || this.$options[options.keyName] === null) {
return
}

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

let p = this.$parent
while (p && p !== this.$root) {
if (isUndefined(p._vueMeta)) {
p._vueMeta = false
}
p = p.$parent
// to speed up updates we keep track of branches which have a component with vue-meta info defined
// if _vueMeta = true it has info, if _vueMeta = false a child has info
if (!this._vueMeta) {
this._vueMeta = true

let p = this.$parent
while (p && p !== this.$root) {
if (isUndefined(p._vueMeta)) {
p._vueMeta = false
}
p = p.$parent
}
}

// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
if (isFunction(this.$options[options.keyName])) {
if (!this.$options.computed) {
this.$options.computed = {}
}
this.$options.computed.$metaInfo = this.$options[options.keyName]

if (!this.$isServer) {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
ensuredPush(this.$options, 'created', () => {
this.$watch('$metaInfo', function () {
triggerUpdate(this, 'watcher')
})
})
}
// coerce function-style metaInfo to a computed prop so we can observe
// it on creation
if (isFunction(this.$options[options.keyName])) {
if (!this.$options.computed) {
this.$options.computed = {}
}
this.$options.computed.$metaInfo = this.$options[options.keyName]

// force an initial refresh on page load and prevent other lifecycleHooks
// to triggerUpdate until this initial refresh is finished
// this is to make sure that when a page is opened in an inactive tab which
// has throttled rAF/timers we still immediately set the page title
if (isUndefined(this.$root._vueMeta.initialized)) {
this.$root._vueMeta.initialized = this.$isServer

if (!this.$root._vueMeta.initialized) {
ensuredPush(this.$options, 'beforeMount', () => {
// if this Vue-app was server rendered, set the appId to 'ssr'
// only one SSR app per page is supported
if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) {
this.$root._vueMeta.appId = options.ssrAppId
}
if (!this.$isServer) {
// if computed $metaInfo exists, watch it for updates & trigger a refresh
// when it changes (i.e. automatically handle async actions that affect metaInfo)
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
ensuredPush(this.$options, 'created', () => {
this.$watch('$metaInfo', () => {
this.__metaInfo = undefined
triggerUpdate(this, 'watcher')
})
})
}
}

// we use the mounted hook here as on page load
ensuredPush(this.$options, 'mounted', () => {
if (!this.$root._vueMeta.initialized) {
// used in triggerUpdate to check if a change was triggered
// during initialization
this.$root._vueMeta.initializing = true

// refresh meta in nextTick so all child components have loaded
this.$nextTick(function () {
const { tags, metaInfo } = this.$root.$meta().refresh()

// After ssr hydration (identifier by tags === false) check
// if initialized was set to null in triggerUpdate. That'd mean
// that during initilazation changes where triggered which need
// to be applied OR a metaInfo watcher was triggered before the
// current hook was called
// (during initialization all changes are blocked)
if (tags === false && this.$root._vueMeta.initialized === null) {
this.$nextTick(() => triggerUpdate(this, 'initializing'))
}

this.$root._vueMeta.initialized = true
delete this.$root._vueMeta.initializing

// add the navigation guards if they havent been added yet
// they are needed for the afterNavigation callback
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
addNavGuards(this)
}
})
}
})
// force an initial refresh on page load and prevent other lifecycleHooks
// to triggerUpdate until this initial refresh is finished
// this is to make sure that when a page is opened in an inactive tab which
// has throttled rAF/timers we still immediately set the page title
if (isUndefined(this.$root._vueMeta.initialized)) {
this.$root._vueMeta.initialized = this.$isServer

if (!this.$root._vueMeta.initialized) {
ensuredPush(this.$options, 'beforeMount', () => {
// if this Vue-app was server rendered, set the appId to 'ssr'
// only one SSR app per page is supported
if (this.$root.$el && this.$root.$el.hasAttribute && this.$root.$el.hasAttribute('data-server-rendered')) {
this.$root._vueMeta.appId = options.ssrAppId
}
})

// add the navigation guards if requested
if (options.refreshOnceOnNavigation) {
addNavGuards(this)
// we use the mounted hook here as on page load
ensuredPush(this.$options, 'mounted', () => {
if (!this.$root._vueMeta.initialized) {
// used in triggerUpdate to check if a change was triggered
// during initialization
this.$root._vueMeta.initializing = true

// refresh meta in nextTick so all child components have loaded
this.$nextTick(function () {
const { tags, metaInfo } = this.$root.$meta().refresh()

// After ssr hydration (identifier by tags === false) check
// if initialized was set to null in triggerUpdate. That'd mean
// that during initilazation changes where triggered which need
// to be applied OR a metaInfo watcher was triggered before the
// current hook was called
// (during initialization all changes are blocked)
if (tags === false && this.$root._vueMeta.initialized === null) {
this.$nextTick(() => triggerUpdate(this, 'initializing'))
}

this.$root._vueMeta.initialized = true
delete this.$root._vueMeta.initializing

// add the navigation guards if they havent been added yet
// they are needed for the afterNavigation callback
if (!options.refreshOnceOnNavigation && metaInfo.afterNavigation) {
addNavGuards(this)
}
})
}
})

// add the navigation guards if requested
if (options.refreshOnceOnNavigation) {
addNavGuards(this)
}
}
}

// do not trigger refresh on the server side
if (!this.$isServer) {
// no need to add this hooks on server side
updateOnLifecycleHook.forEach((lifecycleHook) => {
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
})
// do not trigger refresh on the server side
if (this.$isServer) {
return
}

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

clearInterval(interval)
// Wait that element is hidden before refreshing meta tags (to support animations)
const interval = setInterval(() => {
if (this.$el && this.$el.offsetParent !== null) {
return
}

if (!this.$parent) {
/* istanbul ignore next line */
return
}
clearInterval(interval)

triggerUpdate(this, 'destroyed')
}, 50)
})
}
}
triggerUpdate(this, 'destroyed')
}, 50)
}
}
}

0 comments on commit ec7b1fb

Please sign in to comment.