diff options
author | Roan Kattouw <roan.kattouw@gmail.com> | 2023-01-30 15:17:34 -0800 |
---|---|---|
committer | Roan Kattouw <roan.kattouw@gmail.com> | 2023-02-08 12:13:23 -0800 |
commit | c5d9e49b973d57072e0195a4c8de6d1c40da822f (patch) | |
tree | 11fd11ddcb44f1873f353f9c4065f0a3862ebb5e /resources/src/vue | |
parent | 9e8e8fd4086e485ca83a9f9ac3c1e1b7e39543e8 (diff) | |
download | mediawikicore-c5d9e49b973d57072e0195a4c8de6d1c40da822f.tar.gz mediawikicore-c5d9e49b973d57072e0195a4c8de6d1c40da822f.zip |
vue i18n: Prevent unnecessary rerender in v-i18n-html
When the v-i18n-html directive is used in a component, the directive
function is called every time anything changes in the component.
Unfortunately this is necessary for detecting when the value of the
directive changes, because Vue's directive API doesn't give us a better
way to detect that.
However, it does give us the old value. This change adds logic that
should have been part of this directive from the start, comparing the
old value to the new value and doing nothing if they're the same. This
avoids unnecessary rerenders when an unrelated bit of component state
changes. These rerenders were wasteful, and were also causing bugs like
T327229.
There's no binding.oldArg, so we can't detect changes in binding.arg.
Those can only happen when a dynamic argument is used, which is already
discouraged, so warn in the documentation that that isn't supported.
Change-Id: I71f1cc0d08bfd02e1a9edb6cbfd849f10f929f3c
Diffstat (limited to 'resources/src/vue')
-rw-r--r-- | resources/src/vue/i18n.js | 73 |
1 files changed, 55 insertions, 18 deletions
diff --git a/resources/src/vue/i18n.js b/resources/src/vue/i18n.js index 4e386d000472..7e2bb8be57e3 100644 --- a/resources/src/vue/i18n.js +++ b/resources/src/vue/i18n.js @@ -31,6 +31,29 @@ module.exports = { return mw.message( key, ...parameters ); }; + function renderI18nHtml( el, binding ) { + /* eslint-disable mediawiki/msg-doc */ + let message; + + if ( Array.isArray( binding.value ) ) { + if ( binding.arg === undefined ) { + // v-i18n-html="[ ...params ]" (error) + throw new Error( 'v-i18n-html used with parameter array but without message key' ); + } + // v-i18n-html:messageKey="[ ...params ]" + message = mw.message( binding.arg ).params( binding.value ); + } else if ( binding.value instanceof mw.Message ) { + // v-i18n-html="mw.message( '...' ).params( [ ... ] )" + message = binding.value; + } else { + // v-i18n-html:foo or v-i18n-html="'foo'" + message = mw.message( binding.arg || binding.value ); + } + /* eslint-enable mediawiki/msg-doc */ + + el.innerHTML = message.parse(); + } + /* * Add a custom v-i18n-html directive. This is used to inject parsed i18n message contents. * @@ -62,27 +85,41 @@ module.exports = { * styles (e.g. because the message key is dynamic, or contains unusual characters). * Note that you can use mw.message() in computed properties, but in template attributes * you have to use $i18n() instead as demonstrated above. + * + * WARNING: Do not use dynamic argument syntax, like <div v-i18n-html:[msgKeyVariable] /> + * If you do this, the message will not update when msgKeyVariable changes, due to + * limitations in Vue's directives API. Instead, use the $i18n style described + * above if you need a dynamic message key. */ - app.directive( 'i18n-html', function ( el, binding ) { - let message; - /* eslint-disable mediawiki/msg-doc */ - if ( Array.isArray( binding.value ) ) { - if ( binding.arg === undefined ) { - // v-i18n-html="[ ...params ]" (error) - throw new Error( 'v-i18n-html used with parameter array but without message key' ); + app.directive( 'i18n-html', { + mounted: renderI18nHtml, + updated( el, binding ) { + // This function is invoked often, every time anything in the component changes. + // We don't want to rerender unnecessarily, because that's wasteful and can cause + // strange issues like T327229. For each possible type of binding.value, compare it + // to binding.oldValue, and abort if they're equal. This does not account for + // changes in binding.arg; we can't detect those, so there's a warning in the + // documentation above explaining that using a dynamic argument is not supported. + + const areArraysEqual = ( arr1, arr2 ) => + Array.isArray( arr1 ) && Array.isArray( arr2 ) && + arr1.length === arr2.length && + arr1.every( ( val, index ) => arr2[ index ] === val ); + const areMessagesEqual = ( msg1, msg2 ) => + msg1 instanceof mw.Message && msg2 instanceof mw.Message && + msg1.key === msg2.key && + areArraysEqual( msg1.parameters, msg2.parameters ); + + if ( + binding.value === binding.oldValue || + areArraysEqual( binding.value, binding.oldValue ) || + areMessagesEqual( binding.value, binding.oldValue ) + ) { + return; } - // v-i18n-html:messageKey="[ ...params ]" - message = mw.message( binding.arg ).params( binding.value ); - } else if ( binding.value instanceof mw.Message ) { - // v-i18n-html="mw.message( '...' ).params( [ ... ] )" - message = binding.value; - } else { - // v-i18n-html:foo or v-i18n-html="'foo'" - message = mw.message( binding.arg || binding.value ); - } - /* eslint-enable mediawiki/msg-doc */ - el.innerHTML = message.parse(); + renderI18nHtml( el, binding ); + } } ); } }; |