Image

advanced-components

9 min read
Last update: December 19, 2021

Reactivity in Vue.js

Using the knowledge we learned building a reactivity system in our first lesson, we’ll dive into the Vue.js source code to find where the reactivity lives. Doing so will help us:

  • Get comfortable parsing though the Vue.js source
  • Learn Vue.js design patterns & architecture
  • Improve your debugging skills
  • Learn caveats to the reactivity system

It all will start with a simple Vue application.

<div id="app">
    <h1>{{ product }}</h1>
</div>

<script src="vue.js"></script>

<script>
    var app = new Vue({
        el: '#app',
        data: {
            product: "Socks"
        }
    })
</script>

Question: Where is Reactivity?

At some point the product property inside the data object gets reactive super powers. It’d be nice to know where and how this happens.

If we look in the documentation for this data object we find:

See how it talks about getters/setters to make it reactive? It also mentions that the object must be plain, just data. So somewhere in the source it’s making the properties in data reactive, and adding the ability to remember what needs to get updated when the property values changes. Remember, when data is GET we add a dependency, and when it’s SET we notify all the dependencies.

Answer: Tracing the Code Execution

I started by downloading the Vue.js source code so I could generate a local version of Vue. I wrote up an index.html file with the contents that you see above, and used the Chrome DevTools Debugger to watch the code execute one line at a time. It’s easier than you might think.

When our Vue application starts up, we’ll begin here:

/src/core/instance/index.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
... // ommitted code
this._init(options)
}
initMixin(Vue) // goes here next
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue

There’s our root Vue function that creates our instance when we call var app = new Vue({ .. }). Notice it calls _init, which is a prototype function that gets added to our Vue object.

Side Note: In the code above and the code that follows I’ve omitted code to simplify what you have to read.

/src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
    const vm: Component = this

    ... Normalizing options ...
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm) // defineReactive attrs and listeners
    callHook(vm, 'beforeCreate') // Notice this lifecycle call hook
    initInjections(vm) // resolve injections before data/props
    initState(vm) // <--- initProvide(vm) // resolve provide after data/props callHook(vm, 'created' ) // Notice this lifecycle call hook ... vm.$mount(vm.$options.el) } } ``` Inside init we do many things, but we’re interested in InitState. /src/core/instance/state.js ```javascript export function initState (vm: Component) { vm._watchers=[] const opts=vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) // <--- We do have data } else { observe(vm._data={}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !==nativeWatch) { initWatch(vm, opts.watch) } } ... ``` As you can see, we initialize Props, Methods, and then initData, which continues in the same file below. ```javascript ... function initData (vm: Component) { let data=vm.$options.data // The value here is { product: "Socks" } ... observe(data, true /* asRootData */) // <--- Here we go } ``` See how we call “observe” on our data? We’re getting closer. /src/core/observer/index.js ```javascript export function observe (value: any, asRootData: ?boolean): Observer | void { ... // Our value is still { product: "Socks" } ob=new Observer(value) return ob } ``` Okay, so our observer does something with our data. Let’s dive deeper. /src/core/observer/index.js ```javascript export class Observer { value: any; constructor (value: any) { this.value=value // { product: "Socks" } ... if (Array.isArray(value)) { // We don't have an array but if we did ... this.observeArray(value) // We would call observeArray (shown below) } else { this.walk(value) // Walk through each property } } /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys=Object.keys(obj) // keys=["product"] for (let i=0; i < keys.length; i++) { // Go through each key defineReactive(obj, keys[i], obj[keys[i]]) // <--- Going here next // Calling defineReactive(obj, 'product' , 'Socks' ) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) // Just call observe on each item } } } ``` See how if our data is an array we call `observe` on each of the items? If it’s not an array we go to the `walk` function, get all our keys, and then we call `defineReactive`? That’s where we go next. /src/core/observer/index.js ```javascript /** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, // <--- "product" val: any, // <--- "Socks" this will be our internalValue customSetter?: ?Function, shallow?: boolean ) { const dep=new Dep() // <--- There's our dependency class like from the last lesson. const property=Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable===false) { return // if property is not set as configurable, then return } // cater for pre-defined getter/setters const getter=property && property.get const setter=property && property.set Object.defineProperty(obj, key, { // <--- There's our defineProperty enumerable: true, configurable: true, get: function reactiveGetter () { // <--- There's our Get const value=getter ? getter.call(obj) : val // <--- If we have a defined getter, then use that; otherwise return value, like we did with our internalVal. if (Dep.target) { dep.depend() // <-- There's our depend function ... } return value // A Getter returns a value. }, set: function reactiveSetter (newVal) { const value=getter ? getter.call(obj) : val // <-- if custom getter // If the value is the same don't do anything. if (newVal===value || (newVal !==newVal && value !==value)) { return } if (setter) { setter.call(obj, newVal) } else { val=newVal // <--- Set the new value. } dep.notify() // <-- There's our notify } }) } ``` Aha, there’s our reactivity! Pretty similar to what we wrote, isn’t it? If we trace from the top down, here’s what our path down to Reactivity looks like! ![](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1578371488258_1.png?alt=media&token=65bdf4e2-d489-4602-922f-36f455b6571e) ## Question: But what’s inside that Dep class? I know I was really curious at this point to see what was inside my Dep class, but before we get there we need to understand more about Vue’s `Watcher class` . Remember our Watch Function from the last lesson? ![](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1578371494966_2.png?alt=media&token=a6a9b2df-b783-4d85-8401-d345befb0e2c) Well, Vue has a Watcher class which: * Receives as a parameter the code to watch (like above) * Stores the code inside a `getter` property * Has a `get` function (called directly in instantiation, or by the scheduler) which: * Runs `pushTarget(this)` to set `Dep.target` to this watcher object * Calls `this.getter.call` to run this code * Runs `popTarget(`) to remove current `Dep.target` * Has an `update` function to queue this `watcher` to run (using a scheduler) /src/core/observer/watcher.js ```javascript export default class Watcher { ... get() { pushTarget(this) // Set Dep.target to this watcher object ... value=this.getter.call(vm, vm) // run the code ... popTarget() // remove current Dep.target return value } update() { if (this.lazy) { this.dirty=true } else if (this.sync) { this.run() // shown below } else { queueWatcher(this) // queue this to run later } } run() { if (this.active) { const value=this.get() } } addDep(dep: Dep) { // This is called to start tracking a dep ... this.newDeps.push(dep) // The watcher also track's deps dep.addSub(this) // Calls back to the dep (below) } } ``` Now that you know what a `Watcher` looks like, the dep class should make a little more sense: /src/core/observer/dep.js ```javascript export default class Dep { ... subs: Array<Watcher>; // Notice our susbcribers are of class Watcher.
            constructor () {
            this.subs = [] // Our subscribers we need to keep track of
            }
            addSub(sub: Watcher) { // You can think of this sub Watcher as our target
            this.subs.push(sub)
            }
            ...
            depend() { // <-- There's our depend function if (Dep.target) { // If target exists Dep.target.addDep(this) // Add this as a dependency, which ends up calling addSub function above. Pushing this watcher. } } notify() { // <--- There's our notify function const subs=this.subs.slice() for (let i=0, l=subs.length; i < l; i++) { subs[i].update() // <-- Queue and run each subscriber } } } ``` Be sure to read through my code comments above. You can see how this is very similar to the reactivity engine we build in the previous level. If we look at our diagram again, it should make a little more sense: ![](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1578371498654_3.png?alt=media&token=f6e56d31-b373-4499-a749-321eecc38752) Some day you might need to dive into the source, and now perhaps it’ll be a little less scary. ## Problem: Caveats to the Reactivity System There are limitations to the reactivity system that you should get a little familiar with, and the Vue documentation is extremely well written on it. Some of these could cause some of those hair-pulling bugs someday, so trust me on taking a quick look. The first caveat involves [addition or deletion of reactive properties](https://vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats), like inside the data object. The second involves [the way you might make changes to an array that is reactive](https://vuejs.org/v2/guide/list.html#Array-Change-Detection). ## Stay Tuned Now that we have a good sense of how our Reactivity system works, in our next lesson we’ll jump into our component rendering system. When you see how these two systems work together you’ll have a deeper appreciation for the Component engine, and we’ll begin to look at techniques to make our components more scalable.