Welcome to our journey into the world of Vue 3's reactivity system! If you've been following along, you already know that we've been piecing together some vital components in the previous articles.
In the first part of this series, we crafted the trigger
function, which set the foundation for reactivity. Then, in the second part, we built the track
, watchEffect
, and reactive
functions, adding more power to our system.
And now, in this part, we're going to implement the ref
function. It's an important piece of the puzzle in building Vue 3's reactivity system. Let's dive in and get this ref
function up and running!
What Is Vue’s ref
?
The ref
function is a great way to get a reactive version of a single value. It works with all kinds of data, whether it's primitive values like numbers or more complex values like objects.
To access the reactive value that ref
returns, you can simply use the .value
property on the returned object.
const count = ref(0);
console.log(count); // 0
count.value++;
console.log(count); // 1
ref
vs. reactive
: What To Choose?
ref
:The
ref
function is used to create a single reactive reference to a value, whether it's a primitive type (like a number, string, etc.) or an object.When you create a reactive reference with
ref
, the returned value is an object with a.value
property that holds the actual value. You need.value
to access or modify the underlying data.It is particularly useful when you want to create a reactive reference to a simple value and avoid unnecessary reactivity for complex objects.
reactive
:The
reactive
function is used to create a fully reactive object. It takes an object as an argument and converts all its properties into reactive properties.When you use
reactive
, you can directly access and modify the properties of the returned object without any need for.value
like withref
.
In summary, ref
is used for individual reactive values, where you need .value
to access the underlying data. On the other hand, reactive
is used for creating reactive objects, and you can directly access and modify their properties without the need for .value
.
It's Not Just a Wrapper for reactive
When we first encounter the ref
function, it might seem like it's just a simple shortcut for calling the reactive
function with an object containing only one property value
, which is the value passed to the ref
in the first place.
function ref(value){
return reactive({ value });
}
And actually, it works!
Let's have a look at this simple example that shows the reactivity between price
and isExpensive
variables.
import { ref, computed } from "vue";
const price = ref(100);
const isExpensive = computed(() => price.value > 1000);
console.log(isExpensive); // false
price.value = 2000;
console.log(isExpensive); // true
If we decided to use our version myRef
instead of vue's ref
function in the previous example, Everything will work just as before, and we won't need to make any changes.
import { reactive, computed } from "vue";
const myRef = (value) => reactive({ value });
const price = myRef(100);
const isExpensive = computed(() => price.value > 1000);
console.log(isExpensive); // false
price.value = 2000;
console.log(isExpensive); // true
However, this is not how Vue 3 has implemented the ref
function.
Why Vue Said No to Wrappers?
Because by definition, the ref
function should expose a single property .value
that points to the inner value. On the other hand, the reactive
function returns a proxy, as we've explained before. And Technically, this proxy allows attaching additional properties beyond .value
, which goes against the main idea of ref
, which should only wrap an inner value.
Another important consideration is performance. The reactive
function does more than what we usually do with ref
. For instance, with reactive
, we check if there are other reactive copies of the object and if it's read-only. These extra checks are not always necessary when creating refs.
Vue 3 Way of Implementing ref
Function
Vue 3 has decided to implement the ref
function based on JavaScript object accessors.
What Are Javascript Classes Accessor fields?
Accessor fields in JavaScript are special methods inside a class that allow us to control how we read or modify certain properties of objects created from that class. Getters fetch values when you read a property, while setters handle updates when you set a property. They make your code cleaner and more efficient.
Let's have a simple example where we have a class Person
with private property _name
that stores the name. We then define a getter and setter for the name
property. When we access .name
, the getter function is automatically called and returns the _name
value. When we set a new name, the setter function is automatically called, updating the _name
value.
class Person {
constructor(name) {
this._name = name;
}
// Getter for name
get name() {
console.log('Getting name...');
return this._name;
}
// Setter for name
set name(newName) {
console.log('Setting name...');
this._name = newName;
}
}
// Creating an instance of the Person class
const person = new Person('Nasser');
// Accessing the name property using the getter
console.log(person.name); // Output: Getting name... \n Nasser
// Setting a new name using the setter
person.name = 'Ahmed'; // Output: Setting name...
// Accessing the updated name using the getter
console.log(person.name); // Output: Getting name... \n Ahmed
Difference between Object Accessors And Javascript Proxy
The main difference between object accessors and JavaScript Proxy is that object accessors are used to define custom behavior for individual properties of an object (.value
in case of ref
), while Proxy allows you to define custom behavior for the entire object or a group of properties at once. Proxy is more powerful and flexible, as it can intercept multiple operations and apply custom logic globally, whereas getters and setters are limited to specific properties.
The Implementation
Just like how we made the reactive function in the last article, we're doing something similar here with class accessor fields instead of a JavaScript proxy. In the previous article, we used the proxy getter to track changes to properties and the setter to trigger effects accordingly. Now, with class accessor fields, we're still doing the same thing, but the way we set it up is a bit different.
The ref
method takes a value we want to make reactive, it gives us an instance of the RefImpl
class. This special instance has some cool abilities. It comes with a property called value
. When we read this property, it keeps track of all the things that depend on that value. And when we change the value
property, it automatically triggers all the effects that rely on that value. It's like having a smart assistant who takes care of things for us behind the scenes!
class RefImpl {
constructor(value) {
this._value = value;
}
get value() {
track(this, 'value');
return this._value;
}
set value(newValue) {
this._value = newValue;
trigger(this, 'value');
}
}
function ref(rawValue) {
return new RefImpl(rawValue);
}
Wrapping It All Up
Alright, let's put everything we've made together! We'll use all the functions we've created to build a simple example that behaves just like Vue 3's reactivity system.
You can find all the code we've written below and the file structure we used by following this link. Everything is conveniently organized for you to explore and understand the example with ease.
Let's create a new file
ourVue.js
and add to it some of the functions we’ve built so farwatchEffect
,track
,trigger
, andref
.const targetMap = new WeakMap(); const depsMap = new Map(); let activeEffect = null; class RefImpl { constructor(value) { this._value = value; } get value() { track(this, 'value'); return this._value; } set value(newValue) { this._value = newValue; trigger(this, 'value'); } } export function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } function track(target, property) { if (activeEffect) { // We get the correct depsMap using the target (reactive object) let depsMap = targetMap.get(target) if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } // Get list of effects of dependency let dep = depsMap.get(property) if (!dep) { dep = new Set(); depsMap.set(property, dep) } dep.add(activeEffect); } } function trigger(target, property) { // We get the correct depsMap using the target (reactive object) let depsMap = targetMap.get(target) if (!depsMap) return // Get list of effects of dependency let dep = depsMap.get(property) if (!dep) return // Run all the effects of this dependency dep.forEach(effect => { effect() }) } export function ref(rawValue) { return new RefImpl(rawValue); }
In our main js file, let’s import the
watchEffect
andref
functions fromourVue.js
import { watchEffect, ref } from './ourVue';
Use our
ref
function to have a reactive variableconst count = ref(1); let doubleCount; watchEffect(() => (doubleCount = count.value * 2)); console.log(doubleCount); // 2 count.value = 2; console.log(doubleCount); // 4
Next Steps
In the next parts of this series, we'll explore how Vue 3 manages updates and changes on the web page. We'll see how to link the functions we created earlier, which create reactivity, with the DOM elements on the web page. This means that when our data changes, the web page will automatically update itself without us needing to do anything extra! It's like magic! 🪄
We'll also get into some cool concepts like the virtual DOM, which makes updating the page more efficient. Plus, we'll explore other interesting things like Vue's compiler and render function.
I'll make sure it's all simple and fun to understand! Stay tuned for more learning and exploring together!
References
Vue Offical Docs - How Reactivity Works in Vue