[Vuemastery] Vue 3 Reactivity

Code
Reactivity Source Code

Vue 3 Reactivity

In this course we will understand the new Vue 3 Reactivity system. Learning how this is built from the ground up will help you understand the design patterns used inside Vue, improve your Vue debugging skills, enable you to use the new Vue 3 modularized Reactivity library, and perhaps even contribute to the Vue 3 source code yourself.

In this lesson we will start building a simple reactivity system using the very same techniques you’ll find in the Vue 3 source code.

Understanding Reactivity

Vue’s reactivity system can look like magic when you see it working for the first time.

Take this simple app:

<div id="app">
  <div>Price: ${{ product.price }}</div>
  <div>Total: ${{ product.price * product.quantity }}</div>
  <div>Taxes: ${{ totalPriceWithTax }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      product: {
        price: 5.00,
        quantity: 2
      }
    },
    computed: {
      totalPriceWithTax() {
        return this.product.price * this.product.quantity * 1.03
      }
    }
  })
</script>

And somehow Vue’s Reactivity system just knows that if price changes, it should do three things:

  • Update the price value on our webpage.
  • Recalculate the expression that multiplies price * quantity , and update the page.
  • Call the totalPriceWithTax function again and update the page.

But wait, I hear you wonder, how does Vue’s Reactivity system know what to update when the price changes, and how does it keep track of everything?

This is not how JavaScript programming usually works

If it’s not obvious to you, programming usually doesn’t work this way. For example, if I run this code:

let product = { price: 5, quantity: 2 }
let total = product.price * product.quantity  // 10 right?
product.price = 20
console.log(`total is ${total}`)

What do you think it’s going to print? Since we’re not using Vue, it’s going to print 10.

>> total is 10

In Vue we want total to get updated whenever price or quantity get updated. We want:

>> total is 40

Unfortunately, JavaScript is procedural, not reactive, so this doesn’t work in real life. In order to make total reactive, we have to use JavaScript to make things behave differently.

For the rest of this lesson and the next 2 after this one, we will be building a Reactivity System from scratch using the same methodology as Vue 3 (which is very different than Vue 2). We will then look into the Vue 3 source code to discover these patterns we wrote from scratch.

Saving Code to Run Later

Problem

As you saw with the code above, in order to start building reactivity we need to save how we’re calculating the total , so we can re-run it when price or quantity changes.

Solution

First off, we need some way to tell our application, “Store the code (effect) I’m about to run, I may need you to run it at another time.” Then we’ll want to run the code, and if price or quantity variables get updated, run the stored code again.

We might do this by recording the function (effect) so we can run it again.

let product = { price: 5, quantity: 2 }
let total = 0

let effect = function () { 
  total = product.price * product.quantity
})

track() // Remember this in case we want to run it later
effect() // Also go ahead and run it

Notice that we store an anonymous function inside the effect variable, and then call a track function. Using the ES6 arrow syntax I could also write this as:

let effect = () => { total = product.price * product.quantity }

In order to define track , we need a place to store our effects, we may have many of them. We’ll create a variable called dep , as in dependency. We call it dependency because typically with the Observer design pattern a dependency has subscribers (in our case effects) which will get notified when an object changes state. We might make dependency a class with an array of subscribers, like we did in the Vue 2 version of this tutorial. However, since all it needs to store is a set of effects, we can simply create a Set .

let dep = new Set() // Our object tracking a list of effects

Then our track function can simply add our effects to this collection:

function track () {
  dep.add(effect) // Store the current effect
}

In case you’re not familiar, the difference between a JavaScript Array and Set, is that a Set cannot have duplicate values and it doesn’t use an index like arrays. Learn more about Set’s here if you’re not familiar.

We’re storing the effect (in our case the { total = price * quantity } ) so we can run it later. Here’s a visualization this dep Set:

Let’s write a trigger function that runs all the things we’ve recorded.

function trigger() { 
  dep.forEach(effect => effect()) 
}

This goes through all the anonymous functions we have stored inside the dep Set and executes each of them. Then in our code, we can just:

product.price = 20
console.log(total) // => 10
trigger()
console.log(total) // => 40

Simple enough, right? Here’s the code in its entirety if you need to read through and try to grasp it one more time.

let product = { price: 5, quantity: 2 }
let total = 0
let dep = new Set()

function track() {
  dep.add(effect)
}

function trigger() {
  dep.forEach(effect => effect())
}

let effect = () => {
  total = product.price * product.quantity
}

track()
effect()

product.price = 20
console.log(total) // => 10

trigger()
console.log(total) // => 40

Problem: Multiple Properties

We could go on tracking effects as needed, but our reactive objects are going to have different properties, and those properties each need their own dep (which is a set of effects ). Take a look at our object here:

let product = { price: 5, quantity: 2 }

Our price property needs it’s own dep (set of effects ) and our quantity needs it’s own dep (set of effects ). Let’s build out our solution to properly record these.

Solution: depsMap

When we call track or trigger we now need to know which property in our object we’re targeting ( price or quantity ). To do this we’ll create a depsMap , which is of type Map (think keys and values) . Here’s how we might visualize it:

Notice how the depsMap has a key which will be the property name we want to add (or track) a new effect on. So we’ll need to send in this key to the track function.

const depsMap = new Map()
function track(key) {
  // Make sure this effect is being tracked.
  let dep = depsMap.get(key) // Get the current dep (effects) that need to be run when this key (property) is set
  if (!dep) {
    // There is no dep (effects) on this key yet
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }
  dep.add(effect) // Add effect to dep
}
  }
function trigger(key) {
  let dep = depsMap.get(key) // Get the dep (effects) associated with this key
  if (dep) { // If they exist
    dep.forEach(effect => {
      // run them all
      effect()
    })
  }
}

let product = { price: 5, quantity: 2 }
let total = 0

let effect = () => {
  total = product.price * product.quantity
}

track('quantity')
effect()
console.log(total) // --> 10

product.quantity = 3
trigger('quantity')
console.log(total) // --> 40

Problem: Multiple Reactive Objects

This works great, until we have multiple reactive objects (more than just product) which need to track effects. Now we need a way of storing a depsMap for each object (ex. product). We need another Map, one for each object, but what would be the key? If we use a WeakMap we can actually use the objects themselves as the key. WeakMap is a JavaScript Map that uses only objects as the key. For example:

let product = { price: 5, quantity: 2 }
const targetMap = new WeakMap()
targetMap.set(product, "example code to test")
console.log(targetMap.get(product)) // ---> "example code to test"

Obviously this isn’t the code we’re going to use, but I wanted to show you how our targetMap uses our product object as the key. We call our WeakMap targetMap because we’ll consider target the object we’re targeting. There’s another reason it’s called target which will become more obvious in the next lesson. Here is what we have visualized:

When we call track or trigger we now need to know which object we’re targeting. So, we’ll send in both the target and the key when we call it.

const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated

function track(target, key) {
  // We need to make sure this effect is being tracked.
  let depsMap = targetMap.get(target) // Get the current depsMap for this target

  if (!depsMap) {
    // There is no map.
    targetMap.set(target, (depsMap = new Map())) // Create one
  }

  let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
  if (!dep) {
    // There is no dependencies (effects)
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }

  dep.add(effect) // Add effect to dependency map
}

function trigger(target, key) {
  const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
  if (!depsMap) {
    return
  }

  let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
  if (dep) {
    dep.forEach(effect => {
      // run them all
      effect()
    })
  }
}

let product = { price: 5, quantity: 2 }
let total = 0
let effect = () => {
  total = product.price * product.quantity
}

track(product, 'quantity')
effect()
console.log(total) // --> 10

product.quantity = 3
trigger(product, 'quantity')
console.log(total) // --> 15

So now we have a very effective way of tracking the dependencies on multiple objects, this is a big piece of the puzzle when building our reactivity system. Give yourself a pat on the back. The battle is half over. In the next lesson we will discover how to call track and trigger automatically using ES6 proxy.

Proxy and Reflect

In our last lesson we learned how Vue 3 keeps track of effects to re-run them when needed. However, we’re still having to manually call track and trigger . In this lesson we’ll learn how to use Reflect and Proxy to call them automatically.

Solution: Hooking onto Get and Set

We need a way to hook (or listen for) the get and set methods on our reactive objects.

GET property => We need to track the current effect

SET property => We need to trigger any tracked dependencies (effects) for this property

The first step to understanding how to do this, is to understand how in Vue 3 with ES6 Reflect and Proxy we can intercept GET and SET calls. Previously in Vue 2 we did this with ES5 Object.defineProperty .

Understanding ES6 Reflect

To print out an object property I can do this:

let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or 
console.log('quantity is ' + product['quantity'])

However, I can also GET values on an object by using Reflect . Reflect allows you to get a property on an object. It’s just another way to do what I wrote above:

console.log('quantity is ' + Reflect.get(product, 'quantity'))

Why use reflect ? Good question! Because it has a feature we’ll need later, hold that thought.

Understanding ES6 Proxy

A Proxy is a placeholder for another object, which by default delegates to the object. So if I run the following code:

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)

The proxiedProduct delegates to the product which returns 2 as the quantity. Notice the second argument on Proxy with {} ? This is called a handler and can be used to define custom behavior on the proxy object, like intercepting get and set calls. These interceptor methods are called traps and here’s how we would set a get trap on our handler :

let product = { price: 5, quantity: 2 }

let proxiedProduct = new Proxy(product, {
  get() {
    console.log('Get was called')
    return 'Not the value'
  }
})

console.log(proxiedProduct.quantity)

In the console I’d see:

Get was called

Not the value

We’ve re-written what get returns when the property value is accessed. We should probably return the actual value, which we can do like:

let product = { price: 5, quantity: 2 }

let proxiedProduct = new Proxy(product, {
  get(target, key) {  // <--- The target (our object) and key (the property name)
    console.log('Get was called with key = ' + key)
    return target[key]
  }
})

console.log(proxiedProduct.quantity)

Notice that the get function has two parameters, both the target which is our object ( product ) and the key we are trying to get, which in this case is quantity . Now we see:

Get was called with key = quantity

2

This is also where we can use Reflect and add an additional argument to it.

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
  get(target, key, receiver) {  // <--- notice the receiver
    console.log('Get was called with key = ' + key)
    return Reflect.get(target, key, receiver) // <----
  }
})

Notice our get has an additional parameter called receiver which we’re sending as an argument into Reflect.get . This ensures that the proper value of this is used when our object has inherited values / functions from another object. This is why we always use Reflect inside of a Proxy , so we can keep the original behavior we are customizing.

Now let’s add a setter method, there shouldn’t be any big surprises here:

let product = { price: 5, quantity: 2 }

let proxiedProduct = new Proxy(product, {
  get(target, key, receiver) {  
    console.log('Get was called with key = ' + key)
    return Reflect.get(target, key, receiver) 
  }
  set(target, key, value, receiver) {
    console.log('Set was called with key = ' + key + ' and value = ' + value)
    return Reflect.set(target, key, value, receiver)
  }
})

proxiedProduct.quantity = 4
console.log(proxiedProduct.quantity)

Notice that set looks very similar to get except that it’s using Reflect.set which receives the value to set the target (product). Our output as expected is:

Set was called with key = quantity and value = 4

Get was called with key = quantity

4

There’s another way we can encapsulate this code, which is what you see in the Vue 3 source code. First, we’ll wrap this proxying code in a reactive function which returns the proxy, which should look familiar if you’ve played with the Vue 3 Composition API. Then we’ll declare our handler with it’s traps separately and send them into our proxy.

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      console.log('Get was called with key = ' + key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      console.log('Set was called with key = ' + key + ' and value = ' + value)
      return Reflect.set(target, key, value, receiver)
    }
  }
  return new Proxy(target, handler)
}

let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)

This would return the same as above, but now we can easily create multiple reactive objects.

Combining Proxy + Effect Storage

If we take the code we have for creating reactive objects, and remember:

GET property => We need to track the current effect

SET property => We need to trigger any tracked dependencies ( effects ) for this property

We can start to imagine where we need to call track and trigger with the code above:

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
        // Track
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (oldValue != result) { // Only if the value changes 
        // Trigger
      } 
      return result
    }
  }
  return new Proxy(target, handler)
}

Now let’s put the two pieces of code together:

const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
  // We need to make sure this effect is being tracked.
  let depsMap = targetMap.get(target) // Get the current depsMap for this target
  if (!depsMap) {
    // There is no map.
    targetMap.set(target, (depsMap = new Map())) // Create one
  }
  let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
  if (!dep) {
    // There is no dependencies (effects)
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }
  dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
  const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
  if (!depsMap) {
    return
  }
  let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
  if (dep) {
    dep.forEach(effect => {
      // run them all
      effect()
    })
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
      track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (oldValue != result) {
        trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
      }
      return result
    }
  }
  return new Proxy(target, handler)
}

let product = reactive({ price: 5, quantity: 2 })
let total = 0

let effect = () => {
  total = product.price * product.quantity
}
effect()

console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)

Notice how we no longer need to call trigger and track because these are getting properly called inside our get and set methods. Running this code gives us:

before updated quantity total = 10

after updated quantity total = 15

Wow, we’ve come a long way! There’s only one bug to fix before this code is solid. Specifically, that we only want track to be called on a reactive object if it’s inside an effect . Right now track will be called whenever a reactive object property is get . We’ll polish this up in the next lesson.

activeEffect & ref

In this lesson we’ll continue to build out our reactivity code by fixing a small bug and then implementing reactive references, much like you might have seen in Vue 3. The bottom of our current code from the last lesson looks like this:

...
let product = reactive({ price: 5, quantity: 2 })
let total = 0

let effect = () => {
  total = product.price * product.quantity
}
effect()

console.log(total)

product.quantity = 3

console.log(total)

The problem arrives when we add code which GETs a property from our reactive object, like so:

console.log('Updated quantity to = ' + product.quantity)

The issue here is that track and all of it’s function will get called, even if we’re not inside an effect . We only want to look up and record the effect if get is called inside the active effect.

Solution: activeEffect

To solve this problem, we’ll first create an activeEffect , a global variable we’ll store the currently running effect in. We’ll then set this inside a new function called effect .

let activeEffect = null // The active effect running
...
function effect(eff) {
  activeEffect = eff  // Set this as the activeEffect
  activeEffect()      // Run it
  activeEffect = null // Unset it
}

let product = reactive({ price: 5, quantity: 2 })
let total = 0

effect(() => {
  total = product.price * product.quantity
})

effect(() => {
  salePrice = product.price * 0.9
})

console.log(
  `Before updated total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`
)

product.quantity = 3

console.log(
  `After updated total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`
)

product.price = 10

console.log(
  `After updated total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`
)

Notice that we no longer need to call the effect manually. It’s getting called automatically inside our new effect function. Notice I’ve also added a second effect , because why not :grin:. I’ve also updated our console.log s to look more like tests, so we can verify the proper output. You can try out all the code yourself by grabbing it off github.

So good so far, but there’s one more change we need to make, and that’s inside the track function. It needs to use our new activeEffect .

function track(target, key) {
  if (activeEffect) { // <------ Check to see if we have an activeEffect
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map())) 
    }
    let dep = depsMap.get(key) 
    if (!dep) {
      depsMap.set(key, (dep = new Set())) // Create a new Set
    }
    dep.add(activeEffect) // <----- Add activeEffect to dependency map
  }
}

Great, now if we run our code we properly get:

Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 15) = 15 salePrice (should be 4.5) = 4.5
After updated total (should be 30) = 30 salePrice (should be 9) = 9

If you want to walk through this code executing line by line, definitely check out the video.

The Need for Ref

When I was coding up this challenge I realized that the way I was calculating total might make a little more sense if it used the salePrice rather than price , like so:

effect(() => {
  total = salePrice * product.quantity
})

If we were creating a real store, we’d probably calculate the total based on the salePrice . However, this code wouldn’t work reactively. Specifically, when product.price is updated, it will reactively recalculate the salePrice with this effect:

effect(() => {
  salePrice = product.price * 0.9
})

But since salePrice isn’t reactive, the effect with total won’t get recalculated. Our first effect above won’t get re-run. We need some way to make salePrice reactive, and it’d be nice if we didn’t have to wrap it in another reactive object. If you’re familiar with the Composition API, which I teach in the Vue 3 Essentials Course, you might be thinking that I should use ref to create a Reactive Reference. Let’s do this:

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0

According to the Vue documentation, a reactive reference takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value that points to the inner value. So we’d need to change around our effects a little to use .value .

effect(() => {
  total = salePrice.value * product.quantity
})

effect(() => {
  salePrice.value = product.price * 0.9
})

Our code should work now, properly updating the total when salePrice is updated. However, we still need to define ref . There’s two ways we could do it.

1. Defining Ref with Reactive

First, we could simply use reactive as we’ve defined it:

function ref(intialValue) {
  return reactive({ value: initialValue })
}

However, this isn’t how Vue 3 defines ref with primitives, so let’s implement it differently.

Understanding JavaScript Object Accessors

In order to understand how Vue 3 defines ref , we first need to make sure we are familiar with object accessors. These are sometimes also known as JavaScript computed properties (not to be confused with Vue computed properties). Below you can see a simple example which uses Object Accessors:

let user = {
  firstName: 'Gregg',
  lastName: 'Pollack',

  get fullName() {
    return `${this.firstName} ${this.lastName}`
  },

  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ')
  },
}

console.log(`Name is ${user.fullName}`)
user.fullName = 'Adam Jahr'
console.log(`Name is ${user.fullName}`)

The get and set lines are object accessors to get fullName and set fullName accordingly. This is plain JavaScript, and is not a feature of Vue.

2. Defining Ref with Object Accessors

Using Object Accessors, along with our track and trigger actions, we can now define ref using:

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

That’s all there is to it. Now when we run the following code:

...
function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0

effect(() => {
  total = salePrice.value * product.quantity
})

effect(() => {
  salePrice.value = product.price * 0.9
})

console.log(
  `Before updated quantity total (should be 9) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
  `After updated quantity total (should be 13.5) = ${total} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
  `After updated price total (should be 27) = ${total} salePrice (should be 9) = ${salePrice.value}`
)

We get what we would expect:

Before updated total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated total (should be 27) = 27 salePrice (should be 9) = 9

Our salePrice is now reactive and total gets updated when it changes!

Computed Values & Vue 3 Source

As we’ve been building out our Reactivity example, you may have been wondering “why haven’t we used computed for our values where we’ve been using effect ?” When looking at our example:

let product = reactive({ price: 5, quantity: 2 })
let salePrice = ref(0)
let total = 0
effect(() => {
  salePrice.value = product.price * 0.9
})
effect(() => {
  total = salePrice.value * product.quantity
})

It seems clear to me that if I was coding up Vue I would write both salePrice and total as computed properties. If you’re familiar with the Vue 3 composition API, you’re probably familiar with the computed syntax. If you’re not, maybe take our Vue 3 Essentials course. We might use the computed syntax like so (even though we haven’t defined it yet):

let product = reactive({ price: 5, quantity: 2 })

let salePrice = computed(() => {
  return product.price * 0.9
})
let total = computed(() => {
  return salePrice.value * product.quantity
})

Makes sense right? And notice how my salePrice computed property is included inside my total computed property, and that we access it using .value . This is our first clue to implementation. It looks like we’re creating another reactive reference. Here’s how we create our computed function:

function computed(getter) {
  let result = ref()  // Create a new reactive reference

  effect(() => (result.value = getter())) // Set this value equal to the return value of the getter

  return result // return the reactive reference
}

That’s all there is to it. You can view / run the code in its entirety over on Github. Our code prints out:

Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9

Vue Reactivity without a Caveat

It’s worth mentioning that we can do something with our reactive objects that was impossible with Vue 2. Specifically, we can add new reactive properties. Like so:

...
let product = reactive({ price: 5, quantity: 2 })
...

product.name = 'Shoes'
effect(() => {
  console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'

As you might expect, it prints out:

Product name is now Shoes
Product name is now Socks

This was impossible with Vue 2 because of how Reactivity was implemented, adding getters and setters to individual object properties using Object.defineProperty . Now with Proxy we can add new properties no problem and they’re instantly reactive.

Testing our code against Vue 3 Source

You might be wondering, would this code work against Vue 3 source? So I cloned the vue-next repo (currently alpha 5), ran yarn install , then yarn build reactivity . This gave me a bunch of files in my packages/reactivity/dist/ . I then took the reactivity.cjs.js file I found there and moved it next to my example files, the ones on github, and wrote this up to use Vue’s Reactivity code:

var { reactive, computed, effect } = require('./reactivity.cjs')

// Exactly the same code here from before, without the definitions

let product = reactive({ price: 5, quantity: 2 })

let salePrice = computed(() => {
  return product.price * 0.9
})

let total = computed(() => {
  return salePrice.value * product.quantity
})

console.log(
  `Before updated quantity total (should be 9) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.quantity = 3
console.log(
  `After updated quantity total (should be 13.5) = ${total.value} salePrice (should be 4.5) = ${salePrice.value}`
)
product.price = 10
console.log(
  `After updated price total (should be 27) = ${total.value} salePrice (should be 9) = ${salePrice.value}`
)
product.name = 'Shoes'
effect(() => {
  console.log(`Product name is now ${product.name}`)
})
product.name = 'Socks'

Ran this with node 08-vue-reactivity.js , and as expected, I got all the same results!

Before updated quantity total (should be 9) = 9 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 13.5) = 13.5 salePrice (should be 4.5) = 4.5
After updated price total (should be 27) = 27 salePrice (should be 9) = 9
Product name is now Shoes
Product name is now Socks

Wow, so our reactivity system works as well as Vue! Well, at a basic level … yes, but in reality Vue’s version is MUCH more complex. Let’s take a look through the files that make up Vue 3’s Reactivity system, to begin to get familiar.

Vue 3 Reactivity Files

If we take a look inside the Vue 3 source, inside /packages/reactivity/src/ we’ll find the following files. Yes they are TypeScript (ts) files, but you should be able to read them (even if you don’t know TypeScript).

  • effect.ts - Defines the effect function to encapsulate code that may contain reactive references and objects. Contains track which is called from get requests and trigger which is called from set requests.
  • baseHandlers.ts - Contains the Proxy handlers like get and set , which call track and trigger (from effect.ts).
  • reactive.ts - Contains the functionality for the reactive syntax which creates an ES6 Proxy using get and set (from basehandlers.ts).
  • ref.ts - Defines how we create Reactive Ref erences, using object accessors (like we did). Also contains toRefs which converts reactive objects into a series of reactive references which access the original proxy.
  • computed.ts - Defines the computed function using effect and object accessors (a little different than we did).

There are a few more additional files, but these carry the core functionality. If you’re feeling like a challenge, you may want to dive into the source code. In the next two lessons we’ll speak to Evan You about how he implemented Reactivity in Vue 3 and give us a tour of the source himself.

1 Like

Hi, can you include the latest video?