July 24, 2017

Mithril.js: Understanding the hyperscript function

Earlier this year at work we re-wrote an internal framework we used to create SPA e-learning courses. After briefly trying out React, Angular 2, Ember and Vue, we settled on Mithril. If I were to compare it to the frameworks we tried out, I would say it’s more like React but with a simpler, smaller codebase. By the way, if you like geeking out on code articles, the articles from Mithril’s old site have some real nuggets of gold. A few months after the re-write was done, I dug into Mithril’s codebase to gain a deeper understanding and this is what I found…


The main entry point into Mithril’s source code is the m() function, which is a hyperscript function that, according to the docs, represents an element in a Mithril view. It’s demonstrated below as:

m("div", {id: "box"}, "hello")
// equivalent HTML:
// <div id="box">hello</div>

And the full hyperscript function is below (as of July 24, 2017):

function hyperscript(selector) {
	// Because sloppy mode sucks
	var attrs = arguments[1], start = 2, children
	if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
		throw Error("The selector must be either a string or a component.");
	}
	if (typeof selector === "string") {
		var cached = selectorCache[selector] || compileSelector(selector)
	}
	if (attrs == null) {
		attrs = {}
	} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
		attrs = {}
		start = 1
	}
	if (arguments.length === start + 1) {
		children = arguments[start]
		if (!Array.isArray(children)) children = [children]
	} else {
		children = []
		while (start < arguments.length) children.push(arguments[start++])
	}
	var normalized = Vnode.normalizeChildren(children)
	if (typeof selector === "string") {
		return execSelector(cached, attrs, normalized)
	} else {
		return Vnode(selector, attrs.key, attrs, normalized)
	}
}

The function takes three arguments:

The first part of the function is as follows:

function hyperscript(selector) {
  // Because sloppy mode sucks
  var attrs = arguments[1], 
  start = 2, 
  children

  if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
    throw Error("The selector must be either a string or a component.");
}

The variable attrs holds the attributes object (html attributes or element properties). It appears start will be used to check for the third argument later and then this will be assigned to children.

Before anything happens you always want to check the arguments given are what the function expects. The if statement is asking the following questions:

If we have been given what we expected, let us continue…

if (typeof selector === "string") {
    var cached = selectorCache[selector] || compileSelector(selector)
}

If we’re given a string, the following happens:

if (attrs == null) {
    attrs = {}
  } else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
    attrs = {}
    start = 1
}

The code above creates an empty attributes object in the event that an attributes object is not passed as the second argument in the first place. As a side note: !== checks if the variables given hold the same value and same type, whilst != only checks if the variables given hold the same values. Also notice that start is assigned the value 1 if the attributes object does not exist.

if (arguments.length === start + 1) {
    children = arguments[start]
    if (!Array.isArray(children)) 
      children = [children]
  } else {
    children = []
    while (start < arguments.length) 
      children.push(arguments[start++])
}

This if statement is doing the following:

var normalized = Vnode.normalizeChildren(children)

The code above does the following:

if (typeof selector === "string") {
	return execSelector(cached, attrs, normalized)
} else {
	return Vnode(selector, attrs.key, attrs, normalized)
}

The last part of the hyperscript function does the following:

Reflections:

  1. Reading soure code is incredibly difficult BUT rewarding. I still have much to learn but being able to dive into the guts of a framework you use frequently is empowering
  2. Reading source code exposes your blind spots and gives you more material to add to your self-study curriculum. Some of the things I didn’t fully understand that I plan to look up include:
    • Splat arguments
    • The difference between throw Error and throw new Error (Edit: According to the JS spec, there is no difference)
  3. When reading source code the first time round, worry more about the what instead of the why. When I came to the normalized variable assignment I ended up doing a bit of a deep dive into the Vnode.normalizeChildren and Vnode.normalize functions by spinning up some example code and logging the results of different statements. I should have simply stated what those functions did and moved on because it was clear enough.
  4. What I learnt that I did not know before:
    • !== checks if the variables given hold the same value and are the same type, whilst != only checks if the variables given hold the same values (comparing reference values adds some further subtlety which I hope to tackle some other time. H/t to @pakx for pointing this out in the Mithril gitter chat)
    • The source of this error message: "The selector must be either a string or a component."