Vue 3 Reactivity Explained: A Deep Dive into the "ref" Function

Vue 3 Reactivity Explained: A Deep Dive into the "ref" Function

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?

  1. 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.

  2. 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 with ref.

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.

  1. Let's create a new file ourVue.js and add to it some of the functions we’ve built so far watchEffect, track, trigger, and ref.

      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);
     }
    
  2. In our main js file, let’s import the watchEffectand ref functions from ourVue.js

      import { watchEffect, ref } from './ourVue';
    
  3. Use our ref function to have a reactive variable

     const 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