In this second part of the series, let's look into how Vue 3's reactivity works with ref
and watchEffect
.
Vue 3's reactivity
Within the scope of this article, we'll only explore ref
and watchEffect
for now. The other reactivity functions will be explored in the next part of the series.
We're talking about reactivity a lot here, so what is it really? It's actually not a new paradigm. The typical example is an Excel spreadsheet:
In the example above, cell B1 is defined as = A1 + A2
. When you update A1 or A2, B1 will also be reactively updated.
However, in JavaScript, variables don't work that way:
let A1 = 1
let A2 = 4
let B1 = A1 + A2
console.log(B1) // 5
A1 = 3
console.log(B1) // still 5
If our purpose is only to log the sum of A1 and A2 when either A1 or A2 changes, we can write something like this in Vue 3:
const A1 = ref(1)
const A2 = ref(4)
watchEffect(() => {
console.log('B1 =', A1.value + A2.value)
})
But how does it work? How does watchEffect
magically know when A1 or A2 changes? To answer this question, let's build ref
from scratch. It begins with an object with only 1 property - value
- with its getter and setter:
export function ref(value) {
return new RefImpl(value)
}
class RefImpl {
private _value
constructor(value) {
this._value = value
}
get value() {
return this._value
}
set value(newVal) {
this._value = newVal
}
}
Nothing happens here yet. The magic trick we're going to use here is when the value
property of the ref
object is read (the getter method is called), we will automatically add the caller as the subscriber of the ref
object. We also call this subscriber an effect (short for side effect). The ref
object now becomes a dependency of the effect.
export function ref(value) {
return new RefImpl(value)
}
class RefImpl {
private _value
public dep = undefined // Ironically, in the Vue's codebase, the subscribers/effects of a ref object seem to also be called its dependencies.
constructor(value) {
this._value = value
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
this._value = newVal;
}
}
export function trackRefValue(ref) {
if (!activeEffect) return;
if (!ref.dep) ref.dep = new Set() // We must use Set here to avoid duplication
ref.dep.add(activeEffect) // Add the active (currently running) effect as one of the ref object's subscribers
}
Now there's 2 problems left:
- Where does the value of
activeEffect
come from? In other words, how do we know which effect is currently running? - We need to trigger/inform all of a
ref
object's subscribers/dependencies when itsvalue
property changes.
Let's deal with the second problem first because it's quite straightforward:
export function ref(value) {
return new RefImpl(value)
}
class RefImpl {
private _value
public dep = undefined // Ironically, in the Vue's codebase, the subscribers/effects of a ref object seem to also be called its dependencies.
constructor(value) {
this._value = value
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
this._value = newVal
triggerRefValue(this)
}
}
export function trackRefValue(ref) {
if (!activeEffect) return;
if (!ref.dep) ref.dep = new Set() // We must use Set here to avoid duplication
ref.dep.add(activeEffect) // Add the active (currently running) effect as one of the ref object's subscribers
}
export function triggerRefValue(ref) {
if (!ref.dep) return;
for (const effect of ref.dep) {
effect() // run the effect
}
}
Now the most important question left is: Where does the value of activeEffect
come from? Let's take a look at the rawest implementation of watchEffect
:
export let activeEffect = undefined
export function watchEffect(effectHandler) {
const effect = () => {
activeEffect = effect
effectHandler()
// The above function call will read the value property of any ref object inside it
// and trigger the getter method of the value property,
// which in turn adds this effect as one of the subscribers of that ref object.
activeEffect = undefined
}
effect() // watchEffect triggers the effect (handler) immediately
}
Because watchEffect
always runs immediately, the first time it runs will always trigger the getter methods of the value
property of ref
objects inside it, and registers the effect as a subscriber of these ref
objects. This is why you don't need to explicitly specify the dependencies for watchEffect
.
Here's a working code example of ref
and watchEffect
:
For now, we have a roughly working version of ref
and watchEffect
. It is nowhere near usable because we've left out too many cases where it might fail, plus there's no batching, and the flow of control is a little bit messed up here. But it serves its purpose as an oversimplified example of what's happening behind the scenes, hopefully.
Conclusion
In this part of the series, we've tapped into the gist of Vue 3's ref
and watchEffect
. In the next part, we'll continue to improve the oversimplified version of ref
and watchEffect
to match the real implementation more closely, and we'll also explore reactive
, computed
, and watch
.
I hope that you find this useful somehow. If you have any suggestion or advice, please don't hesitate to reach out to me in the comment section.