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

This is the sixth post in a series examining the Vue.js source code line-by-line. When we last left off, we were in the midst of examining the parameters of themergeOptions function within Vue.prototype._init:

function initMixin (Vue) {
Vue.prototype._init = function (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
);

}
[. . . .]
}
}

Now, we turn to the code of the mergeOptions function, which the comments explain merge “ two option objects into a new one”:

/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
function mergeOptions (
parent,
child,
vm
) {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child);
}
if (typeof child === 'function') {
child = child.options;
}
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
var extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}

First, mergeOptions checks the current environment to determine whether it is not in production:

function mergeOptions(
parent,
child,
vm) {
[. . . .]
if (process.env.NODE_ENV !== 'production') {
checkComponents(child);
}
[. . . .]
}

If not in production, mergeOptions calls checkComponents and passes the child parameter:

function mergeOptions(
parent,
child,
vm) {
[. . . .]
if (process.env.NODE_ENV !== 'production') {
checkComponents(child);
}
[. . . .]
}

The checkComponents function validates component names by enumerating through the components property on the passed options parameter and calling validateComponentName on each key:

/**
* Validate component names
*/
function checkComponents (options) {
for (var key in options.components) {
validateComponentName(key);
}
}

The validateComponentName function takes a component name and calls the warn function if the component name is invalid.

function validateComponentName (name) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}

First, validateComponentName checks whether the name parameter starts with a letter and only contains alphanumeric letters and the hyphen.The test() method is called on a regular expression object and takes a string:

function validateComponentName (name) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}

If not, validateComponentName calls the warn function and passes a concatenated string as a parameter:

function validateComponentName (name) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);

}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}

validateComponent then checks if the name parameter is a built in tag or reserved tag (calling two utility functions):

function validateComponentName (name) {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'can only contain alphanumeric characters and the hyphen, ' +
'and must start with a letter.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}

The isBuiltInTag and isReservedAttribute variables are both set to the results of the makeMap function (with different parameters):

/**
* Check if a tag is a built-in tag.
*/
var isBuiltInTag = makeMap('slot,component', true);

/**
* Check if a attribute is a reserved attribute.
*/
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');

makeMap is a function that “make[s] a map and return[s] a function for checking whether a key is in that map.” The function:

  • takes a string and a boolean as parameters,
  • creates an empty object,
  • splits the string at each comma,
  • and loops through the split string setting each item as a key on the map to true.

If expectsLowerCase is true, makeMap returns a function that expects a value and returns a check of whether the value key, lowercased, exists as a property on the map object. Otherwise, makeMap returns a function that expects a value and returns a check of whether the value key exists as a property on the map object.

Let’s take that in pieces. First, makeMap takes a string and a boolean as parameters:

function makeMap (
str,
expectsLowerCase

) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}

Next, makeMap creates a completely empty object:

function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}

makeMap then splits the str parameter on each comma in the comma separated list.

function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}

makeMap then loops through the list variable setting properties/keys on the map object to true.

function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}

return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}

Finally, makeMap returns a function (creating a closure) for checking whether a key is in the map. If expectsLowerCase is true, it looks for the lowercase key by calling toLowerCase(). Otherwise, it just looks for the key passed as a parameter.

Next time, we will continue working through the mergeOptions 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: