Recently during the code review, I was debating with my colleagues about extracting some functions as vue filters. Those functions were strictly related to text formatting. In addition, they were used in multiple components. For me, it seemed natural to move those functions to global Vue.filter
and leverage text formatting in a template.
The problem is that this move might come with a price. Let’s dive in and check how much it might cost!
First, a quick reminder of how to work with filters. You can create them in a component:
filters: {
firstLetter (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0)
}
}
Or globally before creating the Vue
instance:
Vue.filter('firstLetter', (value) => {
if (!value) return ''
value = value.toString()
return value.charAt(0)
})
new Vue({
//...
})
Later on, you can use them using the pipe symbol -> |
What is very important to remember: You cannot access the current component instance by this
!
In filters this
is bound to the global object (window), not to the component instance. This is not an accident because filters must not be used to generate side effects. Their only purpose is to perform text formatting.
Ok, so the first problem with filters is they lack any caching mechanism. It means that they work just like methods
in Vue. They need to reevaluate for every call, even when they have the same input.
Let’s look at this scenario:
In console, you’ll see only one console.log
from the computed property, and six from filter! That was expected. In such a case, it’s much more efficient to create computed property in the component.
In this scenario, we would like to trim and capitalize text. But we would like to have the ability to change length.
const _trimAndCapitalize = (text, length ) => {
console.log('doin work!')
if (!text) return "";
text = text.toString();
return text.charAt(0).toUpperCase() + text.slice(1,length);
}
Let’s imagine we have a lot of different edge cases in our component and creating a computed property for each case is not an option. So the question is:
Calling filter? Calling method? Assigning function reference to the computed property? Let’s find out!
export default {
data() {
return {
text1: "ullam velit quis eum aliquam illum numquam ipsa.",
text2: "amet consectetur, adipisicing elit.",
text3: "res sit, cupiditate sapiente",
text4: "adipisicing elit. Maiores sit, illum numquam ipsa."
};
},
filters: {
trimAndCapitalizeFilter(text, length) {
console.log("filter called");
return _trimAndCapitalize(text, length);
}
},
methods: {
trimAndCapitalizeMethod(text, length) {
console.log("method called");
return _trimAndCapitalize(text, length);
}
},
computed: {
trimAndCapitalizeComputed() {
console.log("computed called");
return _trimAndCapitalize;
}
}
}
This is our setup. In the template, we have the following code:
See the Pen filters vs computed vs methods Vue.js by Karol Świeca (@khazarr) on CodePen.
As expected, each time text formatting was needed, the _trimAndCapitalize()
function was invoked. But what is interesting, the computed property was called only once in this flow.
Before we dig deeper, let’s do some jsPerf tests, to determine which setup is the most efficient. You can it try yourself here!
What was surprising to me, storing a reference to a function in the computed property was the slowest one. This suggests that this caching mechanism might have more significant overhead than filters. But I think the most important fact is that the results are really close to each other. Given this data, we can assume that arguing which solution is better is actually bikeshedding (it doesn’t really matter xD).
Anyway, this made me curious. Let’s start our investigation by translating our template to render()
function.
At the begging, we will rewrite these cases:
// method
render(createElement) {
return createElement('p', this.trimAndCapitalizeMethod(this.text1, 1))
}
// computed
render(createElement) {
return createElement('p', this.trimAndCapitalizeComputed(this.text1, 1))
}
With method
and computed
property, this translation is rather straightforward. The tricky question is how to access filters using this
? Watch out, the following code doesn’t work!
render(createElement) {
return createElement('p', this.trimAndCapitalizeFilter(this.text1, 1))
}
this.trimAndCapitalizeFilter
has value of undefined
. Do you have an idea why? If not, no worries. We can use helper function Vue.compile()
. It will output the translation of a template as a render()
function call.
Yeah, we have something! But how to decode what those _
prefixed functions are? We won’t find the answer in docs (belive me, I tried xD). If you look through vue js source code you’ll find this:
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
Knowing this, we can rewrite it to something understandable:
createElement('p', [createTextVNode(toString(resolveFilter("trimAndCapitalizeFilter")(text1,2)))])
Perfect! Now we know that in render()
method we can access filters using this._f(filterName)
. We can write our working render function like this:
render(createElement) {
createElement('p', this._f('trimAndCapitalizeFilter')(this.text1, 2))
}
But can we do better? Let’s check in code what resolveFilter
actually stands for!
function resolveFilter (id) {
return resolveAsset(this.$options, 'filters', id, true) || identity
}
Bingo! We see that our filter is stored in $options
! So our final render()
function will look like this:
render(createElement) {
return createElement('p', this.$options.filters.trimAndCapitalizeFilter(this.text1, 2))
}
filters
are just methods that are stored in a different part of the Vue instance ($options
).this
to reference local properties.filters
because I really like that syntax ;)computed
properties if possible.
computed
properties needs and will be investigated later.