A deep dive in the Vue.js source code (#3)

This is the third post in our series examining the source code of Vue.js. In the last post, we started looking at the initMixin function. As you will remember, the initMixin function is called immediately after the Vue object constructor function is declared and passed the Vue object constructor function as a parameter:

initMixin(Vue)

You will also recall that theinitMixin function is a simple function that takes the Vue object constructor function as a parameter and sets the ._init method on its prototype.

function initMixin (Vue) {
Vue.prototype._init
= function (options) {
[. . . .]
}
}

Last time, we examined how the ._init method is added to Vue.prototype and:

  • Sets a helper variable vm to this
  • Adds a property ._uid to the Vue instance and increments a top level variable, uid$3, each time the ._init method is called.
  • If appropriate to do so, adds a performance check that returns a timestamp in the browser’s performance entry buffer with a unique tag,based on the unique ._uid property, as the name.
  • Calls the initInternalComponent function if an options object is passed to the Vue object constructor function and the ._isComponent property of the options object is set to true.

The mergeOptions Function

When we last left off, if an options object is not passed to the Vue object constructor function or if the ._isComponent property is set to false, initMixin sets a $options property to the results of calling the mergeOptions function and passing three parameters: (1) a function called resolveConstructorOptions with vm.constructor (the constructor of the Vue instance) as a parameter; (2) options or an empty object; and (3) vm(the Vue instance).

} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}

The resolveConstructorOptions function takes the constructor of the Vue instance as the parameter:

resolveConstructorOptions(vm.constructor)

We find the resolveConstructorOptions function declared elsewhere in the code:

function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
// check if there are any late-modified/attached options (#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
}

The parameter name — Ctor — is just a shorthand for “constructor”:

function resolveConstructorOptions (Ctor)

The resolveConstructorOptions function first sets a variable options to the constructor’s options property.

function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
[. . . .]
}

Next, if Ctor.super exists, the function sets a superOptions variable to the results of calling resolveConstructorOptions on the constructor’s super property.

function resolveConstructorOptions (Ctor) {
[. . . .]
var superOptions = resolveConstructorOptions(Ctor.super)
[. . . .]
}

The super property is set within a function namedinitExtend which is set out elsewhere in the code. initExtend takes Vue as a parameter and sets a method named extend. Extend provides for inheritance:

function initExtend (Vue) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0;
var cid = 1;
/**
* Class inheritance
*/
Vue.extend = function (extendOptions) {
extendOptions = extendOptions || {};
var Super = this;
var SuperId = Super.cid;
var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
var name = extendOptions.name || Super.options.name;
if ("development" !== 'production' && name) {
validateComponentName(name);
}
var Sub = function VueComponent (options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = mergeOptions(
Super.options,
extendOptions
);
Sub['super'] = Super;
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps$1(Sub);
}
if (Sub.options.computed) {
initComputed$1(Sub);
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type];
});
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub;
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// cache constructor
cachedCtors[SuperId] = Sub;
return Sub
};
}

We will return to the initExtend function in a later post. For now, it suffices to know that the super property provides easy access up the chain for inheritance purposes.

The resolveConstructorOptions function next sets a variable cachedSuperOptions to the constructor’s superOptions property.

function resolveConstructorOptions (Ctor) {
[. . . .]
var cachedSuperOptions = Ctor.superOptions;
[. . . .]
}

The superOptions property is also set in the Vue.extend method:

Vue.extend = function (extendOptions) {
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
};

As the comment explains, superOptions is a copy of the options inherited from Super set at extension (i.e., when Vue.extend is called). Thus, the next comparison checks for changes from extension to instantiation.

if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
// check if there are any late-modified/attached options (#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}

If the options have changed from extension to instantiation, new options are resolved by setting the constructor’s superOptions property to the superOptions variable (i.e., the properties at instantiation).

function resolveConstructorOptions (Ctor) {
[. . . .]
var superOptions = resolveConstructorOptions(Ctor.super)
[. . . .]
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
[. . . .]
}
}

Next, the resolveConstructorOptions function checks if there are any late-modified or attached options. To do this, the modifiedOptions variable is set to the results of calling the resolveModifiedOptions function and passing the constructor as a parameter.

function resolveConstructorOptions (Ctor) {
[. . . .]
var modifiedOptions = resolveModifiedOptions(Ctor);
[. . . .]
}

This check was added to the code base to fix a bug labeled issue #4976. In order to understand what is occurring, let’s look at the relevant bug report. According to the issue, “I find there is a bug in core library that accidentally drop late-injected options of constructors. That means, if we use vue-hot-reload-api or vue-loader, they inject some options into component options object after creating component constructors, then the component is instantiated by using constructor with $createElement and the injected options are dropped in resolveComponentOptions function.”

The resolveModifiedOptions function takes a constructor as a parameter:

function resolveModifiedOptions (Ctor) {
var modified;
var latest = Ctor.options;
var extended = Ctor.extendOptions;
var sealed = Ctor.sealedOptions;
for (var key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) { modified = {}; }
modified[key] = dedupe(latest[key], extended[key], sealed[key]);
}
}
return modified
}

The resolveModifiedOptions function then declares a variable named modified. You may note at the bottom of the function that the modified variable is the variable returned by the function.

function resolveModifiedOptions (Ctor) {
var modified;
[. . . .]
return modified
}

Next, theresolveModifiedOptionsfunction initializes three variables: (1) latest is set to the constructor’soptions property; (2) extended is set to the constructor’s extendOptions property; and (3) sealed is set to the constructor’s sealedOptions property:

function resolveModifiedOptions (Ctor) {
var modified;
var latest = Ctor.options;
var extended = Ctor.extendOptions;
var sealed = Ctor.sealedOptions;

for (var key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) { modified = {}; }
modified[key] = dedupe(latest[key], extended[key], sealed[key]);
}
}
return modified
}

The extendOptions property and sealedOptions property are both set in the Vue.extend method:

Vue.extend = function (extendOptions) {
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);

}

extendOptions is set to whatever parameter is passed in to the Vue.extend method. sealedOptions is set to the results of calling the extend function (as opposed to the Vue.extend method) and passing an empty object and Sub.options as parameters. The extend function is declared elsewhere:

function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

The function takes two parameters:

function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

Then loops through the keys of the _from parameter:

function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

And sets keys on to for each key on _from:

function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

Before finally returning to:

function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

With that background on the extend function, we can understand what is happening when the sealedOptions property is set to the results of calling extend and passing an empty object and Sub.options:

Vue.extend = function (extendOptions) {
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);

}

The extend function loops through all of the keys in Sub.options setting each as a key on the passed object. For our purposes, it is important to note that this happens at the time of extension.

Now, we can understand the for loop in resolveModifiedOptions:

function resolveModifiedOptions (Ctor) {
var modified;
var latest = Ctor.options;
var extended = Ctor.extendOptions;
var sealed = Ctor.sealedOptions;
for (var key in latest) {
if (latest[key] !== sealed[key]) {
if (!modified) { modified = {}; }
modified[key] = dedupe(latest[key], extended[key], sealed[key]);
}
}

return modified
}

The function loops through each key in the constructor’s options property checking to see whether it differs from the sealed key — that is, the key that was created at the time of extension. If any key is different, the function creates an object named modified and sets modified[key] to the result of calling the dedupe function and passing latest[key], extended[key], and sealed[key] as parameters.

The dedupe function is declared elsewhere:

function dedupe (latest, extended, sealed) {
// compare latest and sealed to ensure lifecycle hooks won't be duplicated
// between merges
if (Array.isArray(latest)) {
var res = [];
sealed = Array.isArray(sealed) ? sealed : [sealed];
extended = Array.isArray(extended) ? extended : [extended];
for (var i = 0; i < latest.length; i++) {
// push original options and not sealed options to exclude duplicated options
if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
res.push(latest[i]);
}
}
return res
} else {
return latest
}
}

In the next post, we will dive in to the dedupe function. If you like the series and want to motivate me to keep working through it, please feel free to clap, follow, respond, or share to your heart’s content.

Next Post: