Writing Clean and Lean Code with Vue
In this section, we'll be investigating how a Vue.js instance works at a lower level by looking at how this is handled by Vue. We'll also be looking at the various properties on our instance such as data, computed, watch, as well as best practices when using each one. Furthermore, we'll be looking at the various lifecycle hooks available within our Vue instance, allowing us to call particular functions at various stages of our application. Finally, we'll be investigating theDocument Object Model(DOM) and why Vue implements a Virtual DOM for enhanced performance.
By the end of this chapter you will:
- Have a greater understanding of how this keyword works within JavaScript
- Understand how Vue proxies this keyword within Vue instances
- Use data properties to create reactive bindings
- Use computed properties to create declarative functions based on our data model
- Use watched properties to access asynchronous data and build upon the foundations of computed properties
- Use lifecycle hooks to activate functionality at particular stages of the Vue lifecycle
- Investigate the DOM and Virtual DOM for an understanding of how Vue renders data to the screen
To begin, let's start off by looking into how this works within JavaScript and how this relates to the context within our Vue instances.
Proxying
So far, you may have interacted with a Vue application and thought to yourself: How doesthiswork the way it does? Before looking into how Vue.js handlesthis, let's have a look at how it works within JavaScript.
How 'this' works within JavaScript
Within JavaScript,thishas varying contexts that range from the global window context to eval, newable, and function contexts. As the default context for this relates to the global scope, this is our window object:
/**
* Outputting the value of this to the console in the global context returns the Window object
*/
console.log(this);
/**
* When referencing global Window objects, we don't need to refer to them with this, but if we do, we get the same behavior
*/
alert('Alert one');
this.alert('Alert two');
The context of this changes depending on where we are in scope. This means, that if we had aStudentobject with particular values, such asfirstName,lastName,grades, and so on, the context ofthiswould be related to the object itself:
/**
* The context of this changes when we enter another lexical scope, take our Student object example:
*/
const Student = {
firstName: 'Paul',
lastName: 'Halliday',
grades: [50, 95, 70, 65, 35],
getFullName() {
return `${this.firstName} ${this.lastName}`
},
getGrades() {
return this.grades.reduce((accumulator, grade) =
>
accumulator + grade);
},
toString() {
return `Student ${this.getFullName()} scored ${this.getGrades()}/500`;
}
}
When we run the preceding code withconsole.log(Student.toString()), we get this:Student Paul Halliday scored 315/500as the context of this is now scoped to the object itself rather than the global window scope.
If we wanted to display this in the document we could do it like so:
let res = document.createTextNode(Student.toString());
let heading = document.createElement('h1');
heading.appendChild(res);
document.body.appendChild(heading);
Notice that, with the preceding code, once again we don't have to usethisas it isn't needed with the global context.
Now that we have an understanding of howthisworks at a basic level, we can investigate how Vue proxiesthisinside of our instances to make interacting with the various properties much easier.
How Vue handles 'this'
You may have noticed up to this point that we're able to reference values inside of our data, methods, and other objects usingthissyntax, but the actual structure of our instance isthis.data.propertyNameorthis.methods.methodName; all of this is possible due to the way Vue proxies our instance properties.
Let's take a very simple Vue application that has one instance. We have adataobject that has amessagevariable and a method namedshowAlert; how does Vue know how to access our alert text withthis.message?
<
template
>
<
button @click="showAlert"
>
Show Alert
<
/button
>
<
/template
>
<
script
>
export default {
data() {
return {
message: 'Hello World!',
};
},
methods: {
showAlert() {
alert(this.message);
},
},
};
<
/script
>
Vue proxies the instance properties to the top level object, allowing us to access these properties via this. If we were to log out the instance to the console (with the help of Vue.js devtools), we'd get the following result:
Console logout
The key properties to look at within the preceding screenshot aremessageandshowAlert, both of which are defined on our Vue instance yet proxied to the root object instance at initialization time.
Data properties
When we add a variable to our data object, we're essentially creating a reactive property that updates the view any time it changes. This means that, if we had a data object with a property namedfirstName, that property would be re-rendered on the screen each time the value changes:
<
!DOCTYPE html
>
<
html
>
<
head
>
<
title
>
Vue Data
<
/title
>
<
script src="https://unpkg.com/vue"
>
<
/script
>
<
/head
>
<
body
>
<
div id="app"
>
<
h1
>
Name: {{ firstName }}
<
/h1
>
<
input type="text" v-model="firstName"
>
<
/div
>
<
script
>
const app = new Vue({
el: '#app',
data: {
firstName: 'Paul'
}
});
<
/script
>
<
/body
>
<
/html
>
This reactivity does not extend to objects added to our Vue instance after the instance has been created outside of the data object. If we had another example of this, but this time including appending another property such asfullNameto the instance itself:
<
body
>
<
div id="app"
>
<
h1
>
Name: {{ firstName }}
<
/h1
>
<
h1
>
Name: {{ name }}
<
/h1
>
<
input type="text" v-model="firstName"
>
<
/div
>
<
script
>
const app = new Vue({
el: '#app',
data: {
firstName: 'Paul'
}
});
app.fullName = 'Paul Halliday';
<
/script
>
<
/body
>
Even though this item is on the root instance (the same as ourfirstNamevariable),fullNameis not reactive and will not re-render upon any changes. This does not work because, when the Vue instance is initialized, it maps over each one of the properties and adds a getter and setter to each data property, thus, if we add a new property after initialization, it lacks this and is not reactive.
How does Vue achieve reactive properties? Currently, it usesObject.definePropertyto define a custom getter/setter for items inside of the instance. Let's create our own property on an object with standardget/setfeatures:
const user = {};
let fullName = 'Paul Halliday';
Object.defineProperty(user, 'fullName', {
configurable: true,
enumerable: true,
get() {
return fullName;
},
set(v) {
fullName = v;
}
});
console.log(user.fullName); //
>
Paul Halliday
user.fullName = "John Doe";
console.log(user.fullName); //
>
John Doe
As the watchers are set with a custom property setter/getter, merely adding a property to the instance after initialization doesn't allow for reactivity. This is likely to change within Vue 3 as it will be using the newer ES2015+ Proxy API (but potentially lacking support for older browsers).
There's more to our Vue instance than a data property! Let's use computed to create reactive, derived values based on items inside of our data model.
Computed properties
In this section, we'll be looking at computed properties within our Vue instance. This allows us to create small, declarative functions that return a singular value based on items inside of our data model. Why is this important? Well, if we kept all of our logic inside of our templates, both our team members and our future self would have to do more work to understand what our application does.
Therefore, we can use computed properties to vastly simplify our templates and create variables that we can reference instead of the logic itself. It goes further than an abstraction; computed properties are cached and will not be recalculated unless a dependency has changed.
Let's create a simple project to see this in action:
# Create a new Vue.js project
$ vue init webpack-simple computed
# Change directory
$ cd computed
# Install dependencies
$ npm install
# Run application
$ npm run dev
Interpolation is powerful; for example, inside of our Vue.js templates we can take a string (for example,firstName) and reverse this using thereverse()method:
<
h1
>
{{ firstName.split('').reverse().join('') }}
<
/h1
>
We'll now be showing a reversed version of ourfirstName, soPaulwould becomeluaP. The issue with this is that it's not very practical to keep logic inside of our templates. If we'd like to reverse multiple fields, we have to then add anothersplit(),reverse(), andjoin()on each property. To make this more declarative, we can use computed properties.
Inside ofApp.vue, we can add a new computed object, that contains a function namedreversedName; this takes our logic for reversing our string and allows us to abstract this into a function containing logic that would otherwise pollute the template:
<
template
>
<
h1
>
Name: {{ reversedName }}
<
/h1
>
<
/template
>
<
script
>
export default {
data() {
return {
firstName: 'Paul'
}
},
computed: {
reversedName() {
return this.firstName.split('').reverse().join('')
}
}
}
<
/script
>
We could then see more information about our computed properties within Vue.js devtools:
Using devtools to display data
In our simple example, it's important to realize that, while we could make this a method, there are reasons why we should keep this as a computed property. Computed properties are cached and are not re-rendered unless their dependency changes, which is especially important if we have a larger data-driven application.
Watched properties
Computed properties are not always the answer to our reactive data problems, sometimes we need to create our own custom watched properties. Computed properties can only be synchronous, must be pure (for example, no observed side-effects), and return a value; this is in direct contrast to a watched property, which is often used to deal with asynchronous data.
A watched property allows us to reactively execute a function whenever a piece of data changes. This means that we can call a function every time an item from our data object changes, and we'll have access to this changed value as a parameter. Let's take a look at this with a simple example:
Note:
Axios
is a library that will need to be added to the project. To do so, head to
https://github.com/axios/axios
and follow the installation steps provided.
<
template
>
<
div
>
<
input type="number" v-model="id" /
>
<
p
>
Name: {{user.name}}
<
/p
>
<
p
>
Email: {{user.email}}
<
/p
>
<
p
>
Id: {{user.id}}
<
/p
>
<
/div
>
<
/template
>
<
script
>
import axios from 'axios';
export default {
data() {
return {
id: '',
user: {}
}
},
methods: {
getDataForUser() {
axios.get(`https://jsonplaceholder.typicode.com/users/${this.id}`)
.then(res =
>
this.user = res.data);
}
},
watch: {
id() {
this.getDataForUser();
}
}
}
<
/script
>
In this example, any time our text box changes with a newid(1-10), we get information about that particular user, like so:
This is effectively watching for any changes on theidand calling thegetDataForUsermethod, retrieving new data about this user.
Although watched properties do have a lot of power, the benefits of computed properties on performance and ease of use should not be understated; therefore wherever possible, favor computed properties over watched properties.
Lifecycle hooks
We have access to a variety of lifecycle hooks that fire at particular points during the creation of our Vue instance. These hooks range from prior to creation withbeforeCreate, to after the instance ismounted,destroyed, and many more in between.
As the following figure shows, the creation of a new Vue instance fires off functions at varying stages of the instance lifecycle.
We'll be looking at how we can activate these hooks within this section:
Vue.js instance lifecycle hooks
Taking advantage of the lifecycle hooks(https://vuejs.org/v2/guide/instance.html) can be done in a similar way to any other property on our Vue instance. Let's take a look at how we can interact with each one of the hooks, starting from the top; I'll be creating another project based on the standardwebpack-simpletemplate:
// App.vue
<
template
>
<
/template
>
<
script
>
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
}
}
<
/script
>
Notice how we've simply added these functions to our instance without any extra imports or syntax. We then get two different log statements in our console, one prior to the creation of our instance and one after it has been created. The next stage for our instance is thebeforeMountedandmountedhooks; if we add these, we'll be able to see a message on the console once again:
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
}
If we then modified our template so it had a button that updated one of our data properties, we'd be able to fire abeforeUpdatedandupdatedhook:
<
template
>
<
div
>
<
h1
>
{{msg}}
<
/h1
>
<
button @click="msg = 'Updated Hook'"
>
Update Message
<
/button
>
<
/div
>
<
/template
>
<
script
>
export default {
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
beforeCreate() {
console.log('beforeCreate');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
},
beforeUpdated() {
console.log('beforeUpdated');
},
updated() {
console.log('updated');
}
}
<
/script
>
Whenever we select theUpdate Messagebutton, ourbeforeUpdatedandupdatedhooks both fire. This allows us to perform an action at this stage in the lifecycle, leaving us only withbeforeDestroyand destroyed yet to cover. Let's add a button and a method to our instance that call$destroy, allowing us to trigger the appropriate lifecycle hook:
<
template
>
<
div
>
<
h1
>
{{msg}}
<
/h1
>
<
button @click="msg = 'Updated Hook'"
>
Update Message
<
/button
>
<
button @click="remove"
>
Destroy instance
<
/button
>
<
/div
>
<
/template
>
We can then add theremovemethod to our instance, as well as the functions that allow us to capture the appropriate hooks:
methods: {
remove(){
this.$destroy();
}
},
// Other hooks
beforeDestroy(){
console.log("Before destroy");
},
destroyed(){
console.log("Destroyed");
}
When we select thedestroyinstance button, thebeforeDestroyanddestroylifecycle hooks will fire. This allows us to clean up any subscriptions or perform any other action(s) when destroying an instance. In a real-world scenario, lifecycle control should be left up to data-driven directives, such asv-ifandv-for. We'll be looking at these directives in more detail in the next chapter.
Vue.js and the Virtual DOM
On the topic of performance improvements, let's consider why Vue.js makes extensive use of the Virtual DOM to render our elements on the screen. Before looking at the Virtual DOM, we need to have a foundational understanding of what the DOM actually is.
DOM
The DOM is what is used to describe the structure of an HTML or XML page. It creates a tree-like structure that provides us with the ability to do everything from creating, reading, updating, and deleting nodes to traversing the tree and many more features, all within JavaScript. Let's consider the following HTML page:
<
!DOCTYPE html
>
<
html lang="en"
>
<
head
>
<
title
>
DOM Example
<
/title
>
<
/head
>
<
body
>
<
div
>
<
p
>
I love JavaScript!
<
/p
>
<
p
>
Here's a list of my favourite frameworks:
<
/p
>
<
ul
>
<
li
>
Vue.js
<
/li
>
<
li
>
Angular
<
/li
>
<
li
>
React
<
/li
>
<
/ul
>
<
/div
>
<
script src="app.js"
>
<
/script
>
<
/body
>
<
/html
>
We're able to look at the HTML and see that we have onediv, twop, oneul, andlitags. The browser parses this HTML and produces the DOM Tree, which at a high level looks similar to this:
We can then interact with the DOM to get access to these elements byTagNameusingdocument.getElementsByTagName(), returning a HTML collection. If we wanted to map over these collection objects, we could create an array of these elements usingArray.from. The following is an example:
const paragraphs = Array.from(document.getElementsByTagName('p'));
const listItems = Array.from(document.getElementsByTagName('li'));
paragraphs.map(p =
>
console.log(p.innerHTML));
listItems.map(li =
>
console.log(li.innerHTML));
This should then log theinnerHTMLof each item to the console inside of our array(s), thus showing how we can access items inside of the DOM:
Virtual DOM
Updating DOM nodes is computationally expensive and depending on the size of your application, this can substantially slow down the performance of your application.The Virtual DOM takes the concept of the DOM and provides us an abstraction, which allows for a diffing algorithm to be used to update DOM nodes. To fully take advantage of this, these nodes are no longer accessed with the document prefix and instead are often represented as JavaScript objects.
This allows Vue to work out exactly_what_changed and only re-render items in the actual DOM that is different from the previous.
Summary
In this chapter, we learned more about the Vue instance and how we can take advantage of a variety of property types such as data, watchers, computed values, and more. We've learned about howthisworks in JavaScript and the differences when using it inside of a Vue instance. Furthermore, we've investigated the DOM and why Vue uses the Virtual DOM to create performant applications.
In summary, data properties allow for reactive properties within our templates, computed properties allow us to take our template and filtering logic and separate it into performant properties that can be accessed within our templates, and watched properties allow us to work with the complexities of asynchronous operations.
In the next chapter, we'll be taking an in-depth look at Vue directives, such asv-if,v-model,v-for, and how they can be used to create powerful reactive applications.