Image

advanced-components

5 min read
Last update: December 19, 2021

Evan You on Vue Core

In our last lesson we walked through the code base to find reactivity. I showed Evan the following diagram and asked him about any design choices he made along the way.

Constructor: the place where it all begins

When we create a new Vue instance using new Vue({…}) it calls the Vue constructor function. This function is pretty basic. It just calls the _init prototype method of Vue passing the options as the argument.

//src/core/instance/index.js
function Vue(options){ this._init(options) }

Little tweaks for performance

As Evan mentions refactors that were made to rule out some edge cases and improve performance. One such place is where a different strategy is used to merge all the options into vm.$options of an instance that is instantiating many components.

//src/core/instance/init.js

if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}

This is a common pattern we can observe in the code base where such optimization tweaks were made over time.

Question: Are proxies already used?

This isn’t a simple answer, Yes and No.

  • No: proxies are not used in the reactivity system yet.
  • Yes: proxies are used to improve developer experience for browsers that support them.
//src/core/instance/init.js
if (process.env.NODE_ENV !== 'production') {
//used during development
initProxy(vm)
} else {
vm._renderProxy = vm
}

If a developer calls a method that doesn’t exist in our component options, a proxy’s [has()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/has) trap is used to catch the mistake and throw a warning. This looks like:

The sequence of code execution

Evan shows us the sequence in which the Vue instance is set up and how each lifecycle hook is executed. He points out that understanding this code makes debugging easy.

//src/core/instance/init.js
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
  • initLifecycle(vm): this sets up some initial properties like $parent, $refs etc on the vue instance.

  • initEvents(vm): sets up methods related to custom events like vm.$emit, vm.$on, vm.$once etc.

  • initRender(vm): sets up the render() and update() methods on the instance.

  • callHook(vm, 'beforeCreate'): the beforeCreate() lifecycle hook is called. Also, plugins like vuex add their properties to the instance in this hook.

  • initInjections(vm): this is where all the dependency-injections are set that are provided using the provide option by the parent.

  • initState(vm): this is where all the props, methods, data, computed and watchers are initialized. You can see the sequence they are initialized below:

//src/core/instance/state.js
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}

The sequence of initialization explains why data properties can be accessed in computed properties and not vice-versa.

  • initProvide(vm): the [provide](https://vuejs.org/v2/api/#provide-inject) option is resolved

  • callHook(vm, 'created'): this is where the created() lifecycle hook is called. As you see from the sequence initState() is called before calling this hook. So all the data, props, methods and computed are already initialized. That’s the reason we are able to use them in created().

Where our data option is initialized and made reactive

//src/core/instance/state.js
export function initState (vm: Component) {
...
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
...
}

initData(vm) is where the data option is checked. If it is a function then the data option should return an object otherwise a warning will be displayed in the console.

We now loop through the data properties and proxy them onto the instance. This means that we can access our data property named foo using this.foo instead of this.$data.foo. It also performs a check to not have conflicting names in our data, props and methods.

function initData (vm: Component) {
let data = vm.$options.data
// checks if data is function or object
...
//proxies the data properties onto the instance
...

// observe data
observe(data, true /* asRootData */)
}

Finally the observe(data,true) is called. This is where our normal data properties become reactive as we have discussed in the previous lessons.

The Observer class

The observe(data) function uses the Observer class. This Observer class is used to make code easier to organize and its main purpose is to convert the data properties into getters/setters. This is where dep.depend() and dep.notify() are called as we have seen in the previous lessons.

Evan elaborates on how the relationship between a particular dependency and its computation works. A computation is collected as a subscriber to that particular dependency of which it is a part of.

Why a separate Observer class?

Evan clarifies that in the future this Observer class could be its own stand-alone package. This allows for the flexibility of creating a separate Observer class that uses proxies API. The two observers can be used in an interchangeable fashion so Vue can support both IE11 and evergreen browsers. This is projected for the Vue version 3.x as shown below:

Up Next

Now that we have a greater understanding of the Vue source & reactivity, we can begin our understanding of the Template Rendering process in the next lesson.