During a conversation with a fellow developer, I saw some code that I didn’t think should work based on the fundamental this
scoping/binding rules.
What I didn’t realise was I had hand-waved my understanding of Vue components, their methods, and how they work.
So, given I saw something I didn’t understand, I decide I should do some googling and check stack overflow (you know, the standard investigation process the every developer does).
But alas, no answer! So I dove into VueJS source code and eventually found the function that explained my WTF moment, and as always, it seems so obvious in retrospect.
To fully appreciate it, I’ll replicate the behaviour in the form of a magic trick. Let’s start by revisiting some of the fundamental rules of JS, and slowly move into Vue-land.
A simple JS example.
Consider the following code:
What is the result of console.log(thing.message)
?
As most would expect, the code prints “changed!”. (See example on CodePen).
Easy. Let’s move on!
Knowing “this”.
Now, what happens if we create a reference to the method and call it like so:
In this case, what is the result of console.log(thing.message)
?
The code prints “unchanged”. (See example on CodePen).
If you didn’t expect that result, you might want to review the rules around how this
works when methods are called, and to do that, I highly recommend Kyle Simpson’s “You don’t know JS” book series.
Effectively, changeMessage
was detached from thing
and invoked at the global scope. So, during the execution of detachedChangeMessage
(the detached reference), this
will point to the window
object.
To verify this, you can type window.message
into the console, and you’ll see that window
now has an attribute of message
set to the string “changed!”.
Still with me? Fundamental rules still in tact? Great!
Into Vue-land!
Now, let’s see what happens when we write similar code again, but in Vue:
How about the log statement in this code above? We’re calling a reference, right? So what does the log message say?
It prints “changed!”. (See example on Codepen).
Okay, so it wasn’t a great magic trick. But did you expect that? Or were you like me and this dog?
Uhh… Vue? What are you doing?
Following from our previous rules, this.message should still have the value of “unnchanged”, right?
Well, you’re correct, but Vue adds a little bit of magic to make this possible. I’ll show you where Vue does this, but first, I’ll explain what it’s doing.
It turns out: during component instantiation, Vue binds the component object to all declared methods.
If you don’t understand what that means, have you heard of the bind() function? It’s very powerful with many other uses, but we only need to consider one particular usage.
A little bit of a .bind()
Consider anotherFunction
below:
const anotherFunction = someFunction.bind(someObject);
In the code above, anotherFunction
will effectively be the same function as someFunction
, but whenever the this
keyword is used within anotherFunction
, it will point at someObject
, due to our bind()
call.
Okay, so where does Vue do this binding magic?
As I mentioned earlier, during component instantiation.
Into the core
As a side-note, never be afraid of looking into how frameworks work and fit together. You’ll always find something interesting, even if it’s just more questions.
Our quick journey starts in src/core/index.js where we export the Vue
object. This is the object that is used when you run the familiar new Vue({…})
line.
But it actually pulls the Vue
object from another file:
import Vue from ‘./instance/index’
So, let’s look into instance/index.js. Here we find that Vue
is a very simple function that first ensures we checks we wrote new Vue(…)
instead of just Vue(…)
(I’ll explain why that’s important later), and then just calls this._init(options)
.
That seems simple enough, and I promise you, we aren’t far from getting to the point of this article.
Where _init is added?
So, where does the_init
function from this._init(options)
come from?
It is added by the initMixin(Vue)
line that sits below the Vue()
function, and that initMixin
function comes from the import at the top of the file:
import { initMixin } from './init'
Alright, so taking a look at initMixin
in core/instance/init.js, it looks like the main point of that function is to add the _init
function we saw earlier. As you can see, _init
is quite large, so below I’ve created a slimmed down version with only the parts we actually care about.
Basically, we only care about the initState
method. But it’s very helpful to know what the vm
is.
vm
can be thought of as your component instance. When we write new
in front of our Vue()
component instantiation, we create an empty object, and Vue adds on all the attributes needed to make that object into a component.
So, what does initState
(from core/instance/state.js) do to the vm
object?
It retreives an $options
object from the vm
, checks if it has a methods
attribute, and if so, passes it along with our vm
to initMethods.
But what is $options?
vm.$options
vm.$options
is whatever you passed to your new Vue()
call.
So, for example, if you write:
var app = new Vue({
el: '#app',
data: { message: 'Hello Vue!' },
methods: {
printLog() { console.log("Hi"); }
},
})
$options
will be the following object
{
el: '#app',
data: { message: 'Hello Vue!' },
methods: {
printLog() { console.log("Hi"); }
},
}
So when we write $options.methods
we’re actually getting the object that we declared our methods inside, ie. the following object:
{
printLog() { console.log("Hi"); }
}
Everything still making sense? Great. Back to the Vue code!
initMethods
So vm
(the component) and our methods
were being passed to initMethods
. Once again I’ll remove logging, so initMethods
actually becomes very short.
All the code does is loop over each attribute of your methods
object, and if they’re actually functions, it will bind the vm
(the component) to that method, and then add the method onto the vm
object.
Did you catch that?
Yep, that’s where the magic happens. Because that function has the vm
bound to it, we’re able to later detach the method from the component, and call it with the binding still there.
So our earlier example:
… will print “changed!”, without us doing any binding ourselves.
This all seems super obvious to me in retrospect, but if I had this gap in my knowledge around how Vue makes its component work, I know many others will probably have the same gap.
Please note that I am not suggesting you should or will ever need to detach methods in this way, but as we’ve seen, it’s definitely possible.
So now, go forth, and use this knowledge only for good!
Thanks for reading!
I can’t believe it’s been 7 whole months since my last technical blog post. I ran into the issue described in this post at the end of last year, but other life events have popped up since then which kept pushing it to the bottom of my todo list.
Speaking of things that have been taking up my time, I’ve spent the past four months learning React Native and have just launched my first app 😮 into the App Store + Google Play Store.
My app “Potent Playbooks” is a simple tool to help keep you on track with any set of tasks you might have, whether it’s your work todo list, your morning routine, a workout, or for parents to help keep their kids get ready.
Give it a try, and I’d love to hear some feedback (via the app or twitter @potentapps).
Anyhoo, glad I’ve got it out now, and I hope it can help you be more productive! 🔥
If you’re interested in React Native let me know! I’m still pretty new to it but I might write be able to write something about it if given the right topic. Cheers!