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

This is the fourth post in a series taking a deep dive in the Vue.js source code. In the last post, we continued our look at the initMixin function which adds a ._init method to the prototype of the Vue object constructor function. When we left off, we were examining the resolveModifiedOptions function.

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
}

As you will recall, the resolveModifiedOptions function:

  • takes a constructor as a parameter;
  • declares a variable named modified;
  • 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;
  • extendOptions is set to whatever parameter is passed in to the Vue.extendmethod. 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.
  • 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 initializes 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.

As the comments explain, the dedupe function “compares latest and sealed to ensure lifecylce hooks won’t be duplicated between merges”:

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
}
}

First, the dedupe function uses an if statement to determine whether latest is an array by calling Array.isArray and passing latest as a parameter.

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
}
}

Then, the dedupe function initializes a variable named res as an empty array.

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

Next, the dedupe function sets sealed using a ternary operator. Array.isArray() is called and passed sealed as a parameter to determine whether sealed is an array. If so, sealed is set to sealed (i.e, the parameter that was passed to the dedupe function). If not, sealed is wrapped in an array.

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

Similarly, the dedupe function sets extended using a ternary operator. Array.isArray() is called and passed extended as a parameter to determine whether extended is an array. If so, extended is set to extended(i.e, the parameter that was passed to the dedupe function). If not, extended is wrapped in an array.

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

Next, the dedupe function loops through thelatest array.

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

As it loops through the array, the dedupe function function calls indexOf to determine whether each element of the latest array is contained in the extended array:

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

Or whether any elements on the latest array are not contained in the sealed array:

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

If so, these elements are pushed to the res array:

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

After looping through all of the elements in the latest array, the dedupe function returns theres array:

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

If Array.isArray(latest) is false, however, the dedupe function returns latest:

function dedupe (latest, extended, sealed) {
[. . . .]
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
}
}

Jumping back out to the resolveModifiedOptions function, we now have the background to understand that the function loops through the constructor’s options property, determines whether any options have been modified, and if so, maps an object with the modifications and returns the object.

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
}

Finally, we are able to turn back to the resolveConstructorOptions which led down this winding road when it initialized a variable set to the results of passing the constructor to theresolveModifiedOptions function:

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
}

Thus, modifiedOptions will be an object with modified options set as properties or keys on the object.

The resolveConstructorOptions function next checks whether any options have been modified and, if so, calls the extend function we discussed earlier and passes the constructor’s extendOptions property and modifiedOptions. As you will recall, the extend function loops through the keys of the second parameter, here modifiedOptions, and sets keys on the first parameter corresponding to each key on the second parameter.

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
}

mergeOptions is called again to set options and Ctor.options:

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 resolveConstructorOptions then checks whether options.name coerces to true. If so, the options name on the options.component property is set to the constructor.

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
}

Finally, the function returns the options variable:

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
}

With this background, we are finally able to turn back to the the ._init method. As you may remember, the ._init 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).

Vue.prototype._init = function (options) {
[. . . .]
// merge options
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
);

}
}
}

Based on the discussion above, we now understand the parameters that are passed to the mergeOptions function. In the next post, we will discuss the code in themergeOptions function in more detail. 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: