How to Code a Framework – the First Lines of Vue.js

As a full-stack developer, I‘ve had the opportunity to work with a variety of frontend frameworks over the years. But one framework that has always stood out to me is Vue.js. With its intuitive API, excellent documentation, and supportive community, Vue has become one of my go-to tools for building dynamic, reactive user interfaces.

But have you ever wondered how Vue works under the hood? What design decisions and trade-offs were made in its creation? And what can we learn from studying the source code of one of the most popular JavaScript frameworks in use today?

In this article, we‘ll be taking a deep dive into the origins of Vue to answer these questions. We‘ll analyze the very first commits to the Vue codebase, examine the core reactivity system, and discuss how the framework has evolved over time. Along the way, we‘ll gain insights into the mind of Vue‘s creator, Evan You, and the guiding principles behind the framework.

So let‘s travel back in time to the year 2013, when Evan first started experimenting with the ideas that would eventually become Vue.

Setting the Stage

To understand the motivations behind Vue, we need to consider the state of web development in 2013. At the time, the dominant frameworks were Angular.js and Backbone.js. React had just been released and was still a newcomer in the field.

Angular and Backbone both offered powerful features for building complex single-page applications, but they also had a steep learning curve and imposed a lot of structure on the code. Developers coming from a jQuery background often struggled with the complex APIs and lack of flexibility.

Evan saw an opportunity to create a framework that combined the best ideas from Angular and React, while prioritizing simplicity and ease of use. He wanted to create a progressive framework that could be incrementally adopted and scaled up as needed.

With this vision in mind, Evan started experimenting with different approaches to templating and data binding. Let‘s take a look at one of his earliest prototypes to see how Vue began to take shape.

Parsing Templates

One of the key features of Vue is its declarative template syntax. By using directives like v-bind and v-on, developers can create dynamic bindings between the DOM and the underlying data model.

But how does Vue parse and compile these templates? Let‘s take a look at an early implementation in the first commits to the Vue repository.

In the src/exp/directive.js file, we can see a Directive class that represents a single directive in a template. The constructor takes in the raw directive string, parses it, and extracts the relevant information like the directive name, argument, and expression:

export default class Directive {
  constructor (raw, definition) {
    this.raw = raw
    this.definition = definition
    this.name = definition.name
    this.arg = definition.arg
    this.descriptor = parseDirective(this.name, raw)
    this.expression = this.descriptor.expression
    // ...
  }
  // ...
}

The parseDirective function uses a regular expression to parse the directive string and extract the necessary parts:

const DIRECTIVE_RE = /^v-|^@|^:/
const EXPRESSION_RE = /^[a-zA-Z_$][\w\.\-]*(?:\((?:[^)]*)\))?$/

function parseDirective (name, raw) {
  let expr, arg, filters

  const firstChar = raw[0]
  if (firstChar === ‘:‘) {
    arg = ‘bind‘
    expr = raw.slice(1)
  } else if (firstChar === ‘@‘) {
    arg = ‘on‘
    expr = raw.slice(1)
  } else if (raw.indexOf(‘|‘) > -1) {
    filters = raw.split(‘|‘).map(s => s.trim())
    expr = filters.shift()
  } else {
    expr = raw
  }

  if (DIRECTIVE_RE.test(name)) {
    name = name.replace(DIRECTIVE_RE, ‘‘)
  }

  return {
    expression: expr,
    arg,
    filters
  }
}

This is a great example of how Vue uses regular expressions and string manipulation to parse templates efficiently at runtime. By keeping the template syntax simple and limiting the number of possible directives, Vue can avoid a full-blown template compilation step and instead parse directives on the fly.

Once the directives have been parsed, Vue creates a tree of Directive instances that mirror the structure of the template. This tree is then used to generate a render function that can be used to efficiently update the DOM as the underlying data changes.

The Reactivity System

Another core feature of Vue is its reactivity system. Vue automatically tracks dependencies between data and the DOM, and updates the DOM efficiently whenever the data changes.

At the heart of Vue‘s reactivity system is the Observer class. An observer is responsible for walking through an object and converting its properties into reactive getters and setters.

Here‘s a simplified version of the Observer class from the early commits of Vue:

export default class Observer {
  constructor (value) {
    this.value = value
    this.walk(value)
  }

  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

function defineReactive (obj, key, val) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()
    }
  })
}

The Observer constructor takes an object and walks through its properties recursively, calling defineReactive on each one. defineReactive replaces the property with a reactive getter/setter pair that tracks dependencies and notifies subscribers when the value changes.

The reactivity system also includes a Dep class which maintains a list of subscribers that depend on a particular value. The dep.depend method is called from the reactive getter to register a subscriber, while the dep.notify method is called from the reactive setter to trigger updates in all the registered subscribers.

By connecting the reactivity system to the template rendering process, Vue is able to automatically update the DOM whenever the underlying data changes. This eliminates a lot of the manual work required to keep the view in sync with the data model.

Virtual DOM and Rendering

In addition to the template parser and reactivity system, another key part of Vue‘s architecture is its virtual DOM implementation.

Virtual DOM is a technique popularized by React for efficiently updating the browser DOM. Instead of making direct mutations to the DOM tree, changes are first applied to a lightweight JavaScript object that represents the ideal state of the DOM. This virtual DOM is then compared to the previous version, and the minimum number of mutations necessary to transform the previous version into the new one are calculated. Finally, these mutations are applied to the real browser DOM in a single batch update.

Vue adopts a similar approach, but with some unique twists. One key difference is that Vue performs much of the diff and patch logic at runtime, while React does most of it at build time using a compiler.

Let‘s take a look at a simplified version of Vue‘s patch function to see how this works:

function patch (oldVnode, vnode) {
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    createElm(vnode)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(‘server-rendered‘)) {
          oldVnode.removeAttribute(‘server-rendered‘)
          hydrate(oldVnode, vnode)
        } else {
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          replace(oldVnode, createElm(vnode))
        }
      } else {
        // replacing existing element
        replace(oldVnode.elm, createElm(vnode))
      }
    }
  }

  return vnode.elm
}

The patch function takes two arguments: the old virtual node and the new virtual node. It then performs a series of checks to determine the most efficient way to update the DOM.

If the old virtual node is undefined, that means we‘re mounting a new root node, so we can just create the DOM elements from scratch using the createElm function.

If the old and new virtual nodes represent the same node (determined by the sameVnode function), then we can do a direct comparison and only update the parts that have changed, using the patchVnode function.

If the old virtual node corresponds to a real DOM element, we need to check if we can hydrate the existing DOM tree using the new virtual node. Hydration is a technique for re-using the existing DOM generated by server-side rendering, rather than discarding it and re-creating it from scratch. If hydration is not possible or fails, we fall back to replacing the entire DOM tree.

Finally, if none of the above cases apply, we‘re dealing with a wholesale replacement of an existing element, so we create a new DOM tree from the virtual node and replace the old one.

By abstracting away the direct DOM manipulation logic and performing updates in batches, Vue is able to achieve excellent rendering performance across a wide range of browsers and devices.

Componentization and Ecosystem

Another key aspect of Vue‘s design is its component-based architecture. Components in Vue are self-contained, reusable units of code that encapsulate both the view logic and the data model.

Vue components can be authored in a variety of ways, but one of the most popular is the single-file component format. Single-file components use a *.vue file extension and include the HTML template, JavaScript logic, and scoped CSS styles all in one file:

<template>
  <div>

    <button @click="reverseMessage">Reverse Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ‘Hello Vue!‘
    }
  },
  methods: {
    reverseMessage() {
      this.message = this.message.split(‘‘).reverse().join(‘‘)
    }
  }
}
</script>

<style scoped>
h1 {
  color: blue;
}
</style>

Single-file components are compiled by Vue‘s build toolchain into standard JavaScript modules that can be easily bundled and deployed. This approach allows developers to take advantage of the latest JavaScript features and tooling, while still providing a familiar and intuitive authoring experience.

Over time, Vue has developed a rich ecosystem of plugins, dev tools, and companion libraries that extend its functionality and make it easier to build complex applications. Some of the most popular include:

  • Vue Router for declarative routing
  • Vuex for state management
  • Vue DevTools for debugging and profiling
  • Vue Test Utils for unit testing components
  • Vue CLI for scaffolding and building projects

Together, these tools and libraries form a comprehensive framework that can be used to build everything from simple prototypes to large-scale production applications.

Conclusion

In this article, we‘ve taken a deep dive into the origins and inner workings of the Vue.js framework. By studying the early commits to the codebase and analyzing the core modules like the template parser, reactivity system, and virtual DOM implementation, we‘ve gained insights into the design decisions and tradeoffs that have shaped Vue‘s development.

What stands out to me most is the clarity of vision and consistency of design that Evan You has maintained throughout the life of the project. From the very beginning, Vue has been guided by a set of core principles: simplicity, modularity, and a focus on the developer experience. These principles are evident in every aspect of the framework, from the elegance of the template syntax to the carefully crafted API surface.

At the same time, Vue has continuously evolved to keep pace with the rapidly changing landscape of web development. The framework has added support for cutting-edge features like the Composition API, TypeScript integration, and native mobile rendering, without sacrificing backwards compatibility or ease of adoption.

As a full-stack developer who has worked with a variety of frontend frameworks, I can confidently say that Vue strikes an ideal balance between power and simplicity. Its learning curve is gentle enough for beginners, while still providing enough flexibility and performance for even the most demanding applications.

Looking to the future, I‘m excited to see how Vue will continue to evolve and grow. With the release of Vue 3 and the continued expansion of the ecosystem, there‘s never been a better time to be a Vue developer. Whether you‘re building a simple prototype or a complex enterprise application, Vue provides a solid foundation that you can rely on.

So the next time you‘re working with Vue, take a moment to appreciate the thought and care that has gone into its design. And if you‘re curious to learn more, I encourage you to dive into the source code and see for yourself how Vue works under the hood. Who knows, you might just be inspired to contribute to the project yourself!

Similar Posts