2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-05-17 05:19:37 +03:00

fix: use timers instead of requestAnimationFrame

The issue with using requestAnimationFrame is that its meant to be used for visual effects. Therefore when a tab is hidden the browser might decide to not perform animation frame updates until the tab becomes visible, this is confirmed behaviour for Firefox. Due to this title updates would not be triggered while document titles are normally visible in the tabs title. For now we batch updates by setting/clearing timeouts with a 10ms interval

Resolves: #313
This commit is contained in:
pimlie
2019-04-23 10:58:34 +02:00
parent e80643b1a8
commit c040de7be7
5 changed files with 86 additions and 47 deletions
-24
View File
@@ -1,24 +0,0 @@
import { hasGlobalWindow } from '../utils/window'
// fallback to timers if rAF not present
const stopUpdate = (hasGlobalWindow ? window.cancelAnimationFrame : null) || clearTimeout
const startUpdate = (hasGlobalWindow ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
/**
* Performs a batched update. Uses requestAnimationFrame to prevent
* calling a function too many times in quick succession.
* You need to pass it an ID (which can initially be `null`),
* but be sure to overwrite that ID with the return value of batchUpdate.
*
* @param {(null|Number)} id - the ID of this update
* @param {Function} callback - the update to perform
* @return {Number} id - a new ID
*/
export default function batchUpdate(id, callback) {
stopUpdate(id)
return startUpdate(() => {
id = null
callback()
})
}
-14
View File
@@ -1,14 +0,0 @@
import batchUpdate from './batchUpdate'
// store an id to keep track of DOM updates
let batchId = null
export default function triggerUpdate(vm, hookName) {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchId = batchUpdate(batchId, () => {
vm.$meta().refresh()
batchId = null
})
}
}
+26
View File
@@ -0,0 +1,26 @@
// store an id to keep track of DOM updates
let batchId = null
export function triggerUpdate(vm, hookName) {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchUpdate(() => vm.$meta().refresh())
}
}
/**
* Performs a batched update.
*
* @param {(null|Number)} id - the ID of this update
* @param {Function} callback - the update to perform
* @return {Number} id - a new ID
*/
export function batchUpdate(callback, timeout = 10) {
clearTimeout(batchId)
batchId = setTimeout(() => {
callback()
}, timeout)
return batchId
}
+1 -1
View File
@@ -1,4 +1,4 @@
import triggerUpdate from '../client/triggerUpdate'
import { triggerUpdate } from '../client/update'
import { isUndefined, isFunction } from '../utils/is-type'
import { ensuredPush } from '../utils/ensure'
import { hasMetaInfo } from './meta-helpers'
+59 -8
View File
@@ -1,10 +1,8 @@
import triggerUpdate from '../../src/client/triggerUpdate'
import batchUpdate from '../../src/client/batchUpdate'
import { triggerUpdate, batchUpdate } from '../../src/client/update'
import { mount, vmTick, VueMetaBrowserPlugin, loadVueMetaPlugin } from '../utils'
import { defaultOptions } from '../../src/shared/constants'
jest.mock('../../src/client/triggerUpdate')
jest.mock('../../src/client/batchUpdate')
jest.mock('../../src/client/update')
jest.mock('../../package.json', () => ({
version: 'test-version'
}))
@@ -48,11 +46,16 @@ describe('plugin', () => {
})
test('updates can be paused and resumed', async () => {
const _triggerUpdate = jest.requireActual('../../src/client/triggerUpdate').default
const _batchUpdate = jest.requireActual('../../src/client/batchUpdate').default
const triggerUpdateSpy = triggerUpdate.mockImplementation(_triggerUpdate)
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
// so just recreate the triggerUpdate fn by copying its implementation
const triggerUpdateSpy = triggerUpdate.mockImplementation((vm, hookName) => {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchUpdateSpy(() => vm.$meta().refresh())
}
})
const Component = Vue.component('test-component', {
metaInfo() {
@@ -109,4 +112,52 @@ describe('plugin', () => {
const { metaInfo } = wrapper.vm.$meta().resume()
expect(metaInfo.title).toBe(title)
})
test('updates are batched', async () => {
jest.useFakeTimers()
const { batchUpdate: _batchUpdate } = jest.requireActual('../../src/client/update')
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
const refreshSpy = jest.fn()
// because triggerUpdate & batchUpdate reside in the same file we cant mock them both,
// so just recreate the triggerUpdate fn by copying its implementation
triggerUpdate.mockImplementation((vm, hookName) => {
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
// batch potential DOM updates to prevent extraneous re-rendering
batchUpdateSpy(refreshSpy)
}
})
const Component = Vue.component('test-component', {
metaInfo() {
return {
title: this.title
}
},
props: {
title: {
type: String,
default: ''
}
},
template: '<div>Test</div>'
})
let title = 'first title'
const wrapper = mount(Component, {
localVue: Vue,
propsData: {
title
}
})
await vmTick(wrapper.vm)
jest.clearAllMocks()
title = 'second title'
wrapper.setProps({ title })
jest.advanceTimersByTime(2)
expect(refreshSpy).not.toHaveBeenCalled()
jest.advanceTimersByTime(10)
expect(refreshSpy).toHaveBeenCalled()
})
})